JVM概述与类的加载机制
JVM 内存模型
对象逃逸分析、JVM 内存分配和回收策略
垃圾回收算法详解、垃圾收集器全解
JVM 调优
对象逃逸分析、JVM 内存分配和回收策略
4 对象逃逸分析
4.1 什么是逃逸分析
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他地方中(jdk 默认是开启的)。
(开启:-XX:+DoEscapeAnalysis;关闭:–XX:-DoEscapeAnalysis)
有如下代码:
public User test1(){
User user = new User();
return user;
}
public void test2(){
User user = new User();
user.setName("cdd");
}
上面代码中 test1()方法实例化了一个 User 对象,并且把该对象返回了。对象逃逸分析时,发现可能其他地方也在用该对象,这个对象就放在堆里了。然而 test2()方法中,创建了一个 User 对象,只是在该方法中使用了,那么经过对象逃逸分析,可能这个 User 类对象就栈帧的局部变量表里。
4.2 JVM 三种运行模式
解释模式(Interpreted Mode):只使用解释器(-Xint 强制 jvm 使用解释模式),执行一行 jvm 字节码就编译一行为机器码。
编译模式(Compiled Mode):只使用编译器(-Xcomp jvm 使用编译模式),先将 jvm 字节码一次性编译为机器码,然后一次性执行所有机器码。
混合模式(Mixed Mode):依然使用解释模式执行代码,但是对于一些”热点“代码采用编译模式执行,jvm 一般采用混合模式执行代码。(热点代码可以理解为多次执行的代码段)
5 JVM 内存分配和回收策略
5.1 堆简述
5.1.1 堆空间分配情况
对象优先在 Eden 去分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC
Minor GC 和 Full GC 有什么不同呢?
Minor GC/Young GC:指新生代的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般回收老年代,年轻代,方法区的垃圾,Major GC 的速度一般比 Minor GC 的慢 10 倍以上。
堆空间分配情况如下图:
堆空间整个老年代占 2/3;年轻代占 1/3,其中 Eden 区占年轻代的 8/10,from 区占年轻代的 1/10,to 区占年轻代的 1/10
5.1.2 查看堆空间使用情况
如下代码:
// 添加运行 jvm 参数:-XX:+PrintGCDetails
public static void main(String[] args) throws InterruptedException {
byte[] allocation1;
allocation1 = new byte[29500 * 1024];
}
运行该段代码时添加 vm 参数,例如在 IDEA 中这样配置(后面不在赘述),如下图:
配置完成后,运行该段代码,控制台打印如下:
5.1.3 最大堆空间
那么虚拟机的默认最大堆空间是多少呢?这个是由电脑的硬件决定,比如我们来看下当前电脑的默认最大堆空间。
运行下面的代码:
public class GCTest {
// 添加运行 jvm 参数:-XX:+PrintGCDetails
public static void main(String[] args) throws InterruptedException {
byte[] allocation1;
allocation1 = new byte[29500 * 1024];
Thread.sleep(Integer.MAX_VALUE);
}
}
该代码执行后,我们再 dos 窗口,执行命令:jps,如下图:
接着执行命令:jmap -heap 8392。其中 8392 是进程号
5.1.4 GC 示例
有如下代码:
public class GCTest {
// 添加运行 jvm 参数:-XX:+PrintGCDetails
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[59000 * 1024];
}
}
设置vm参数(-XX:+PrintGCDetails)打印堆内存分配情况,运行如下:
我们把代码改下,如下:
public class GCTest {
// 添加运行 jvm 参数:-XX:+PrintGCDetails
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[59000 * 1024];
allocation2 = new byte[29500 * 1024];
}
}
运行上面程序,控制台打印如下:
从控制台打印信息可以看到堆内存执行了一次 Minor GC。
5.2 分配策略
5.2.1 长期存活的对象直接进入老年代
既然虚拟机采用了分代收集思想来管理内存,那么内存回收时就必须识别哪些对象应放放在新生代,哪些对象应该放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 中每熬过一次 Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认为 15),就会被移入到老年代。对象晋升到老年代的年龄阈值,可以通过参数:-XX:MaxTenuringThreshold 来设置。
5.2.2 Minor GC 后存活的对象 Survivor 区放不下
这种情况下会把存活的对象部分挪到老年代,部分可能还放在 Survivor 区
5.2.3 大对象直接进老年代
大对象就是需要大量连续内存空间的对象(比如:字符串,数组)
jvm 参数-XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和 ParNew 两个收集器下有效。
例如:-XX:PretenureSizeThreshold=100000 -XX:+UseSerialGC(100000 的单位是 KB)
为什么要这么做呢?
为了避免为大对象分配内存时复制操作而降低效率。
我们来看下周志明老师的《深入理解 java 虚拟机》中类似的例子,有代码如下:
public class GCTest {
// 添加运行 jvm 参数:-XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[4 * 1024 * 1024];
}
}
设置 VM 参数为:
-XX:+PrintGCDetails
-XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
运行后打印日志为:
可以看到 Eden 区存了一部分数据,from 和 to 区空间使用为 0%,老年代空间使用了 2%。
5.2.4 对象动态年龄判断
当前对象的 Survivor 区域里(其中一块区域,放对象的那块 Survivor 区),一批对象的总大小大于这块 Survivor 区域内存大小的 50%,那么此时大于等于这批对象年龄最大值的对象就可以直接进入老年代了。
例如:Survivor 区域里现在有一批对象,年龄 1+年龄 2+…+年龄 n 的多个年龄对象总和超过了 Survivor 区域的 50%,此时,就会把年龄 n(n 为这批对象年龄中最大)以上的对象都放入老年代。
有代码如下:与预期不符
/**
* -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
public static void main(String[] args) {
int _1MB = 1024 * 1024;
byte[] allocation1, allocation2, allocation3, allocation4;
// allocation1+allocation2 大于 survivo 空间一半
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
参数说明:
堆的最小/大值:-Xms20M -Xmx20M
年轻代的大小:-Xmn10M
用于设置 Eden 和其中一个 Survivor 的比值:-XX:SurvivorRatio=8
运行上面代码,控制台打印如下:
在 GC 开始的时候,对象只会存在于 Eden 区和名为“From”的 Survivor 区,Survivor 区的“To”区是空的。
Survivor 区有两个 from 和 to 区,from 区的一半是 1/2M,当 allocation1 和 allocation2 创建时已经大于 Survivor 的 from 区的一半了。创建的对象是同年龄的,的大对象就会直接存到老年代了。
5.2.5 老年代空间分配担保机制
年轻代每次 minor GC 之前,jvm 都会计算下老年代剩余可用空间。
如果这个可用空间小于年轻代里现有所有对象大小之和(包括垃圾对象),就会看一个“
-XX:HandlePromotionFailure"(JDK1.8 默认就设置了)的参数是否设置了,如果有这个参数,就会看看老年代的内存大小,是否大于之前每次 minor gc 后进入老年代的对象平均大小。
如果上一的结果是小于或者之前的参数没有设置,那么就会触发一次 Full gc,对老年代和年轻代一起回收一次垃圾。
如果回收完还是没有足够的空间存放新的对象,就会发生 OOM。
如果 minor gc 之后剩余存活的需要挪动老年代的对象大小还是大于老年代的可用空间,那么也会触发 Full GC,Full GC 完之后如果还是没有空间存放 minor GC 之后的存活对象,也会发生 OOM.
上面文字比较难理解,具体流程如下: