1. 对象进入老年代的条件
在前面的文章中,已经整理过了很多次 对象进入老年代的时机了,在这里就再次进行巩固,并用代码模拟对象进入老年代并分析GC日志。
对象进入老年代的时机:
分配担保规则:新生代GC过后,存活对象太多,Survivor区放不下了,这个时候就需要通过分配担保进入老年代;
达到年龄阈值:对象在新生代熬过了15次(-XX:MaxTenuringThreshold)GC,达到了年龄阈值,会晋升到老年代;(这种对象一般很少,只有在系统中的确需要长期存在的核心组件等,它们一般不能被回收)
动态年龄判断:在新生代Survivor区的对象,如果:年龄1 + 年龄2 + 年龄3 + 年龄N 的对象大小占比大于了Survivor区的50%以上,那年龄N及以上的对象就会晋升到老年代;
大对象直接进入老年代;(-XX:PretenureSizeThreshold)
默认值为0,当不主动设置值时,不管多大的对象都会先在新生代分配内存;
当手动设置了这个值时,如果生成一个大于这个大小的对象(比如一个超大的数组或者其他对象),就会直接在老年代中为这个对象分配内存;
G1收集器中有专门的大对象Region,大对象不存在老年代;
2. 代码模拟-对象进入老年代
2.1 通过分配担保规则进入老年代
JVM运行参数
-Xmx20m -Xms20m -Xmn10m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=10m
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:log/demo1.log
示例代码:
public class Demo1 {
public static void main(String[] args) {
byte[] array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];
byte[] array2 = new byte[128 * 1024];
array2 = null;
byte[] array3 = new byte[2 * 1024 * 1024];
}
}
这篇文章中就不再对代码执行流程和GC日志做很详细的分析了,详细的分析步骤可以参考上一篇文章,这里就只对重点进行分析。
2.1.1 代码执行流程
首先在代码中创建了三个2MB的byte数组对象放到Eden区,并且array1最终只指向了最后一个对象,也就是说这个时候前面两个2MB的对象已经成为了垃圾对象,但是最后一个2MB的对象还存在引用;
然后又创建了一个128KB的byte数组对象放到Eden区,并且也把array2指向了null,这个时候这个128KB的对象也成为了垃圾对象;
再继续创建一个2MB对象,但是这个时候,还能成功创建吗?
来看JVM参数,-Xmn10m -XX:SurvivorRatio=8
,新生代10M,按照比例Eden区有8M的内存空间;我们前面创建了3个2M的对象,和一个128K的对象(这个小对象的作用就是在这里,超过8M,其实不要也可以的,因为会有一些元数据对象),再加上这里想要创建的array3的2MB的对象,已经超过了Eden区的8M;
所以这里不能直接创建,需要先进行一次新生代GC(Young GC)。
2.1.2 GC日志分析
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for linux-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 00:56:38 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 1863076k(242112k free), swap 2097148k(2097148k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
0.045: [GC (Allocation Failure) 0.045: [ParNew: 6651K->264K(9216K), 0.0093427 secs] 6651K->2314K(19456K), 0.0094041 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
Heap
par new generation total 9216K, used 2476K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 27% used [0x00000000fec00000, 0x00000000fee290e0, 0x00000000ff400000)
from space 1024K, 25% used [0x00000000ff500000, 0x00000000ff5422d0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 2050K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2474K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 267K, capacity 386K, committed 512K, reserved 1048576K
[ParNew: 6651K->264K(9216K), 0.0093427 secs] 6651K->2314K(19456K):
264K:本次GC过后,新生代还有264K的对象,但是我们的代码中还有一个 array1指向的2M的对象是存活的,不应该被垃圾回收掉,到哪里去了呢?
根据前面的JVM参数,Survivor区只有1M的大小,这里存活的对象大小为2M,所以Survivor区并不能存放这个对象;所以它应该通过分配担保,进入老年代;
程序运行结束时:
-
par new generation total 9216K, used 2476K:
-
2476K:新生代使用了2476K,它等于下面的两个之和;
-
eden space 8192K, 27% used:
-
Eden区使用了:8192K * 27% = 2211K;
-
from space 1024K, 25% used:
-
Survivor from区使用了:1024K * 25% = 256K;(Eden区的2211K + 25K = 新生代的 2467K)
-
这里也说明了,在发生分配担保需要把存活对象放到老年代的时候,不是把所有的对象都放进老年代,也会有部分对象留在Survivor区;
-
concurrent mark-sweep generation total 10240K, used 2050K:
-
CMS管理的老年代区域使用了 2050K,也就是 array1指向的2M的存活对象,进入到了老年代;
这样也就验证了,分配担保规则:JVM中发生新生代GC时,如果Survivor区放不下存活对象的时候,会通过分配担保进行老年代;并且不是全部放入老年代,而是部分留在Survivor区,部分进入老年代。
2.2 达到年龄阈值进入老年代
JVM运行参数
-Xmx20m -Xms20m -Xmn10m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=10m
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:log/demo2.log
示例代码:
public class Demo2 {
public static void main(String[] args) {
byte[] array1 = new byte[128 * 1024];
int count = 16;
while (count >= 0) {
byte[] array2 = new byte[2 * 1024 * 1024];
byte[] array3 = new byte[2 * 1024 * 1024];
byte[] array4 = new byte[2 * 1024 * 1024];
count--;
}
}
}
2.2.1 代码执行流程
首先创建了一个array1指向的128M的byte数组对象,这个对象一直被array1引用,不会成为垃圾对象;
然后在一个while循环里面,每次循环都创建 3个2M的对象;由于while循环也是以栈帧的方式处理,所以当出了此次循环之后,这6M的对象都会变成垃圾对象;
JVM参数:-Xmn10m -XX:SurvivorRatio=8
,新生代10M的内存空间,按照比例Eden内存空间8M;
每在下一次进入while循环时,由于新生代已经存在了6M(还有些元数据对象)的对象,不能再继续创建2M的对象了,所以需要进行新生代GC。
2.2.2 GC日志分析
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for linux-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 00:56:38 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 1863076k(220764k free), swap 2097148k(2097148k free)
CommandLine flags: -XX:CMSInitiatingOccupancyFraction=70 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
0.058: [GC (Allocation Failure) 0.058: [ParNew: 6651K->392K(9216K), 0.0036491 secs] 6651K->392K(19456K), 0.0037028 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
0.063: [GC (Allocation Failure) 0.063: [ParNew: 6698K->405K(9216K), 0.0003712 secs] 6698K->405K(19456K), 0.0003906 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.064: [GC (Allocation Failure) 0.064: [ParNew: 6693K->469K(9216K), 0.0003293 secs] 6693K->469K(19456K), 0.0003395 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.064: [GC (Allocation Failure) 0.064: [ParNew: 6762K->407K(9216K), 0.0003119 secs] 6762K->407K(19456K), 0.0003247 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.065: [GC (Allocation Failure) 0.065: [ParNew: 6704K->392K(9216K), 0.0003626 secs] 6704K->392K(19456K), 0.0003756 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.066: [GC (Allocation Failure) 0.066: [ParNew: 6691K->388K(9216K), 0.0002962 secs] 6691K->388K(19456K), 0.0003057 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.066: [GC (Allocation Failure) 0.066: [ParNew: 6689K->387K(9216K), 0.0003149 secs] 6689K->387K(19456K), 0.0003485 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.067: [GC (Allocation Failure) 0.067: [ParNew: 6689K->387K(9216K), 0.0002938 secs] 6689K->387K(19456K), 0.0003027 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.067: [GC (Allocation Failure) 0.067: [ParNew: 6690K->387K(9216K), 0.0002991 secs] 6690K->387K(19456K), 0.0003108 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.068: [GC (Allocation Failure) 0.068: [ParNew: 6690K->387K(9216K), 0.0003484 secs] 6690K->387K(19456K), 0.0003609 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.068: [GC (Allocation Failure) 0.068: [ParNew: 6690K->387K(9216K), 0.0002949 secs] 6690K->387K(19456K), 0.0003041 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.069: [GC (Allocation Failure) 0.069: [ParNew: 6690K->387K(9216K), 0.0003131 secs] 6690K->387K(19456K), 0.0003263 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
0.070: [GC (Allocation Failure) 0.070: [ParNew: 6690K->387K(9216K), 0.0002947 secs] 6690K->387K(19456K), 0.0003039 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.070: [GC (Allocation Failure) 0.070: [ParNew: 6690K->387K(9216K), 0.0003084 secs] 6690K->387K(19456K), 0.0003199 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.071: [GC (Allocation Failure) 0.071: [ParNew: 6690K->387K(9216K), 0.0002944 secs] 6690K->387K(19456K), 0.0003034 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.071: [GC (Allocation Failure) 0.071: [ParNew: 6691K->0K(9216K), 0.0007872 secs] 6691K->392K(19456K), 0.0007992 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 9216K, used 6467K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 78% used [0x00000000fec00000, 0x00000000ff250fa0, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 392K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2475K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 267K, capacity 386K, committed 512K, reserved 1048576K
-
[ParNew: 6651K->392K(9216K), 0.0036491 secs]:
-
在每下次进入while循环时,Eden区不够再分配2M对象时,执行的新生代GC;
-
392K:新生代存活对象,也就是array1执行的128K的存活对象,和一些元数据对象;
-
[ParNew: 6698K->405K(9216K), 0.0003712 secs]:
-
同上,一直执行15次;
-
[ParNew: 6691K->0K(9216K), 0.0007872 secs]:
这里是第16次执行新生代GC;
0K:说明在这次GC执行之后,新生代中没有存活对象了,那array1指向的存活对象和一些元数据对象到哪去了呢?
程序运行结束时:
-
concurrent mark-sweep generation total 10240K, used 392K:
-
used 392K:老年代中多出了 392K的对象;
-
这也就是上面在新生代中连续15次存活,在第16次消失不见了的对象;
这样也就验证了,达到年龄阈值:对象在新生代熬过了15次(-XX:MaxTenuringThreshold)GC,达到了年龄阈值,会晋升到老年代。
2.3 动态年龄判断进入老年代
JVM运行参数
-Xmx20m -Xms20m -Xmn10m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=10m
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:log/demo3.log
示例代码:
public class Demo3 {
public static void main(String[] args) {
// 第一阶段
byte[] array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];
array1 = new byte[2 * 1024 * 1024];
array1 = null;
byte[] array2 = new byte[2 * 128 * 1024];
// GC
byte[] array3 = new byte[2 * 1024 * 1024];
// 第二阶段
array3 = new byte[2 * 1024 * 1024];
array3 = new byte[2 * 1024 * 1024];
array3 = null;
byte[] array4 = new byte[3 * 128 * 1024];
// GC
byte[] array5 = new byte[2 * 1024 * 1024];
// 第三阶段
array5 = new byte[2 * 1024 * 1024];
array5 = new byte[2 * 1024 * 1024];
array5 = null;
// GC
byte[] array6 = new byte[2 * 1024 * 1024];
}
}
2.3.1 代码执行流程
JVM参数:-Xmn10m -XX:SurvivorRatio=8
,新生代10M,按比例Eden区8M,单个Survivor区1M;
-
第一阶段:
-
连续创建了3个2M的byte数组对象,并把array1最终指向null,即这6M对象都是垃圾对象了;
-
再创建了一个array2指向的256K的对象,且一直被array2引用;
-
在继续创建array3指向的2M的对象的时候,Eden区不够了,需要进行一次新生代GC;
-
第二阶段:
-
在第一阶段的GC过后,新生代还存活了:array2指向的256K的对象、 array3指向的2M的对象(这个会随着代码继续运行,不会在第一次GC日志中展示);
-
继续创建2个2M的对象,并把array3指向null,即这6M对象都是垃圾对象了;
-
再创建了一个array4指向的384K的对象,且一直被array4引用;
-
在继续创建array5指向的2M的对象的时候,Eden区不够了,需要再进行一次新生代GC;
-
第三阶段:
-
在第二阶段的GC过后,新生代还存活了:array2指向的256K的对象、array4指向的384K的对象、array5指向的2M的对象(这个会随着代码继续运行,不会在第一次GC日志中展示);
-
再继续创建2个2M的对象,并把array5指向null,即这6M对象也都成为了垃圾对象了;
-
在继续创建array6指向的2M的对象的时候,同样Eden区不够了,需要进行一个新生代GC;
-
这个时候,新生代本该还存活:array2指向的256K的对象、array4指向的384K的对象、array6指向的2M的对象
2.3.2 GC日志分析
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for linux-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 00:56:38 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 1863076k(241792k free), swap 2097148k(2097148k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
0.042: [GC (Allocation Failure) 0.042: [ParNew: 6651K->392K(9216K), 0.0104425 secs] 6651K->392K(19456K), 0.0104896 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
0.053: [GC (Allocation Failure) 0.053: [ParNew: 7082K->789K(9216K), 0.0004104 secs] 7082K->789K(19456K), 0.0004275 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.055: [GC (Allocation Failure) 0.055: [ParNew: 7078K->384K(9216K), 0.0008941 secs] 7078K->776K(19456K), 0.0009134 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 9216K, used 2596K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 27% used [0x00000000fec00000, 0x00000000fee290e0, 0x00000000ff400000)
from space 1024K, 37% used [0x00000000ff500000, 0x00000000ff560010, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 392K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2475K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 267K, capacity 386K, committed 512K, reserved 1048576K
-
[ParNew: 6651K->392K(9216K), 0.0104425 secs] 6651K->392K(19456K), 0.0104896 secs]:
-
第一阶段发生的GC;
-
392K:本次GC过后,新生代存活的 array2指向的256K对象 + 100多K的元数据对象;
-
此时他们年龄1岁;
-
[ParNew: 7082K->789K(9216K), 0.0004104 secs] 7082K->789K(19456K), 0.0004275 secs]:
-
第二阶段发生的GC;
-
789K:本次GC过后,新生代存活的 array2指向的256K对象 + array4指向的384K对象 + 100多K的元数据对象;
-
此时上面的392K对象年龄2岁,789K - 392K = 397K 对象的年龄1岁;
-
[ParNew: 7078K->384K(9216K), 0.0008941 secs] 7078K->776K(19456K), 0.0009134 secs]
-
第三阶段发生的GC;
-
384K:本次GC过后,新生代存活的对象大小;但是这里为什么比上一次的 789K还少了呢?
-
此时注意到,第二次GC过后,新生代存活了 789K的对象,存活对象大小已经超过了Survivor的50%;
-
根据动态年龄判断:1岁(397K)+ 2岁(392K) 超过了 50%,所以需要把2岁的对象晋升到老年代,即把 392K的晋升到老年代;
程序运行结束时:
-
concurrent mark-sweep generation total 10240K, used 392K:
-
CMS管理的老年代有 392K的对象,也就是从新生代晋升上来的2岁的对象;
这样也就验证了,动态年龄判断:在新生代Survivor区的对象,如果:年龄1 + 年龄2 + 年龄3 + 年龄N 的对象大小占比大于了Survivor区的50%以上,那年龄N及以上的对象就会晋升到老年代
2.4 大对象直接进入老年代
JVM运行参数:
-Xmx10m -Xms10m -Xmn5m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=1m
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:log/demo4.log
示例代码:
public class Demo4 {
public static void main(String[] args) {
byte[] array1 = new byte[2 * 1024 * 1024];
}
}
这里的代码流程没有什么值得分析的,就是直接 创建一个2M的byte数组对象,但是这里由于手动指定了大对象的JVM参数:-XX:PretenureSizeThreshold=1m
为1M,所以这个2M的对象超过了这个大对象阈值。
2.4.1 GC日志分析
Java HotSpot(TM) 64-Bit Server VM (25.181-b13) for linux-amd64 JRE (1.8.0_181-b13), built on Jul 7 2018 00:56:38 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 1863076k(242176k free), swap 2097148k(2097148k free)
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:MaxTenuringThreshold=15 -XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=1048576 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
Heap
par new generation total 4608K, used 508K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 12% used [0x00000000ff600000, 0x00000000ff67f190, 0x00000000ffa00000)
from space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
to space 512K, 0% used [0x00000000ffa80000, 0x00000000ffa80000, 0x00000000ffb00000)
concurrent mark-sweep generation total 5120K, used 2048K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2474K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 267K, capacity 386K, committed 512K, reserved 1048576K
在这里只创建了一个 2M的对象,不会发生GC,所以日志中也没有GC相关的;
par new generation total 4608K, used 508K:
程序执行完成后,新生代中只有 508K的对象;
但是我们创建了一个 2M的对象,说明没有在新生代中;
程序运行结束时:
concurrent mark-sweep generation total 5120K, used 2048K:
- CMS管理的老年代中有了 2048K的对象,也就是我们创建的那个2M的对象;
- 说明由于设置了大对象阈值:
-XX:PretenureSizeThreshold=1m
,这个超过阈值的大对象,直接进入了老年代;
这样也就验证了,大对象直接进入老年代:当手动设置了阈值时,如果生成一个大于这个大小的对象(比如一个超大的数组或者其他对象),就会直接在老年代中为这个对象分配内存。