Junhc

岂止于博客

深入理解Java虚拟机

1. 运行时数据区域

1.1. 程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理执行事件的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存
为“线程私有”的内存。(此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域)
1.2. Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的。它的生命周期与线程相同。
每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,
它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(
指向了一条字节码指令的地址)。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部
变量表的大小。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果虚拟机可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.3. 本地方法栈
与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法
服务。

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
1.4. Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,也是被所有线程共享的一块内存区域。
所有的对象实例以及数组都要在堆上分配。

Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
1.5. 方法区
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.6. 运行时常量池
也是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息常量池,用于存放编译期生成的各种
字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
1.7. 直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

2. 垃圾收集算法
2.1. 标记 - 清楚算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;
另一个是空间问题,标记清除之后回产生大量不连续的内存碎片,
空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,
无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.2. 复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还有存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

2.3. 标记 - 整理算法

复制收集算法在对象存活效率较高时就要进行较多的复制操作,效率将会变低。
标记过程与“标记 - 清除”算法一样,但后续步骤不是直接怼可回收对象进行清理,而是让所有存活的对象都向一段移动,
然后直接清理掉端边界以外的内存。

2.4. 分代收集算法

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适应的收集算法。
在新生代中,每次垃圾收集时都发现大堆对象死去,只有少量存活,那就选用复制算法。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记 - 清理”或者“标记 - 整理”算法进行回收。

3. 垃圾收集器
3.1. Serial 收集器

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作。
更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

3.2. ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本

3.3. Parallel Scavenge 收集器
3.4. Serial Old 收集器
3.5. Parallel Old 收集器
3.6. CMS 收集器
3.7. G1 收集器
3.8. 理解GC日志
[GC [PSYoungGen: 7301K->632K(9216K)] 7301K->6776K(19456K), 0.0033336 secs] [Times: user=0.01 sys=0.02, real=0.00 secs]
[Full GC [PSYoungGen: 632K->0K(9216K)] [ParOldGen: 6144K->6701K(10240K)] 6776K->6701K(19456K) [PSPermGen: 2851K->2850K(21504K)], 0.0140722 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
 PSYoungGen      total 9216K, used 2541K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 31% used [0x00000000ff600000,0x00000000ff87b6b0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 6701K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 65% used [0x00000000fec00000,0x00000000ff28b6f8,0x00000000ff600000)
 PSPermGen       total 21504K, used 2879K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 13% used [0x00000000f9a00000,0x00000000f9ccfe10,0x00000000faf00000)

GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。
如果有“Full”,说明这次GC是发生了“Stop The World”的。

接下来的“[PSYoungGen”、“[ParOldGen”、“[PSPermGen”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。
后面方括号内部的“7301K->632K(9216K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”
而在方括号之外的“7301K->6776K(19456K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”

再往后,“0.0033336 secs”表示该内存区域GC所占用的时间,单位是秒。更具体的时间数据“[Times: user=0.01 sys=0.02, real=0.00 secs]”,
这里的user、sys和real与Linux的time命令所输出的时间含义一致,分别表示用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过
的墙钟时间。

CPU时间和墙钟时间的区别是:墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU
或者多核的话,多线程操作会叠加这些CPU时间,所以user和sys时间超过real时间是完全正常的。
  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器策略里就有直接进行Major GC的策略选择过程)。
4. HotSopt虚拟机主要参数
4.1. 内存管理参数
-Xss10M 栈容量
-Xms20M 堆容量最小值
-Xmx20M 堆容量最大值
-Xmn10M 新生代容量
-XX:SurvivorRatio=8 新生代中Eden区域与Survivor区域的比值
-XX:PermSize=64M 方法区容量最小值
-XX:MaxPermSize=64M 方法区容量最大值
-XX:PertenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
-XX:MaxTenuringThreshold 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就会进入老年代。
(最小值与最大值设置为一样即可避免自动扩展)
4.2. 调式参数
-XX:+HeapDumpOnOutOfMemoryError 在发生内存溢出异常时是否生成堆转储快照,关闭不生成。默认关闭。
-XX:HeapDumpPath=存储文件/目录  
-XX:OnOutOfMemoryError 允许用户当出现oom时,指定执行某个脚本
-XX:+PrintGCDetails 打印GC详细信息
4.3. 查看Java汇编指令

首先安装一个支持库,hsdis,步骤如下

  • OS X系统下载hsdis-amd64.dylib,拷贝至$JAVA_HOME/jre/lib/目录下,
  • Linux系统下载hsdis-amd64.so,拷贝至$JAVA_HOME/jre/lib/amd64/目录下
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly your_main_class_file_name