内存分配与回收策略

接下来我们就通过一些demo结合着GC日志分析下什么时候会触发GC,以及对象在堆中如何分配流转的。
1、对象首先分配到Eden区
我们通过如下这段程序来验证下对象首先是分配到 Eden 区的:

    static final int _1M = 1024 * 1024;

    @RequestMapping(value="test001")
    @ResponseBody
    public void test001() {
        byte[] b1 = new byte[_1M * 30];
        byte[] b2 = new byte[_1M * 30];
    }

jvm参数设置为如下:堆200M,年轻代 100M,Eden区占 80M,Survivor 各占 10M,老年代100M。使用默认的 Parallel Scavenge + Parallel Old 回收器。

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:D:\data\gc.log

程序运行结束后,查看运行的GC日志:

 1 Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for windows-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 09:29:03 by "java_re" with MS VC++ 10.0 (VS2010)
 2 Memory: 4k page, physical 8287844k(2492096k free), swap 18188876k(7140888k free)
 3 CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=209715200 -XX:+ManagementServer -XX:MaxHeapSize=209715200 -XX:MaxNewSize=104857600 -XX:NewSize=104857600 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
 4 2021-06-05T18:57:25.718+0800: 2.100: [GC (Allocation Failure) [PSYoungGen: 81920K->8661K(92160K)] 81920K->8677K(194560K), 0.0132714 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] 
 5 2021-06-05T18:57:26.667+0800: 3.050: [GC (Metadata GC Threshold) [PSYoungGen: 76804K->10235K(92160K)] 76820K->10615K(194560K), 0.0138921 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
 6 2021-06-05T18:57:26.682+0800: 3.064: [Full GC (Metadata GC Threshold) [PSYoungGen: 10235K->0K(92160K)] [ParOldGen: 379K->10176K(102400K)] 10615K->10176K(194560K), [Metaspace: 20588K->20586K(1067008K)], 0.0473320 secs] [Times: user=0.09 sys=0.02, real=0.05 secs] 
 7 2021-06-05T18:57:28.062+0800: 4.444: [GC (Allocation Failure) [PSYoungGen: 81920K->6781K(92160K)] 92096K->16965K(194560K), 0.0076810 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
 8 2021-06-05T18:57:29.907+0800: 6.290: [GC (Allocation Failure) [PSYoungGen: 88701K->9357K(92160K)] 98885K->19550K(194560K), 0.0170180 secs] [Times: user=0.13 sys=0.00, real=0.02 secs] 
 9 2021-06-05T18:57:31.172+0800: 7.554: [GC (Metadata GC Threshold) [PSYoungGen: 52252K->8936K(92160K)] 62444K->19136K(194560K), 0.0125074 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
10 2021-06-05T18:57:31.184+0800: 7.567: [Full GC (Metadata GC Threshold) [PSYoungGen: 8936K->0K(92160K)] [ParOldGen: 10200K->14368K(102400K)] 19136K->14368K(194560K), [Metaspace: 33607K->33607K(1079296K)], 0.0949214 secs] [Times: user=0.34 sys=0.02, real=0.10 secs] 
11 2021-06-05T18:57:34.623+0800: 11.005: [GC (Allocation Failure) [PSYoungGen: 81920K->8369K(92160K)] 96288K->22745K(194560K), 0.0181685 secs] [Times: user=0.13 sys=0.00, real=0.02 secs] 
12 2021-06-05T18:57:38.619+0800: 15.001: [GC (Allocation Failure) [PSYoungGen: 90289K->10180K(92160K)] 104665K->24564K(194560K), 0.0180199 secs] [Times: user=0.13 sys=0.00, real=0.02 secs] 
13 2021-06-05T18:57:40.052+0800: 16.433: [GC (Allocation Failure) [PSYoungGen: 92100K->10220K(92160K)] 106484K->27907K(194560K), 0.0155151 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
14 2021-06-05T18:57:49.342+0800: 25.725: [GC (Allocation Failure) [PSYoungGen: 92140K->10217K(92160K)] 109827K->35134K(194560K), 0.0138674 secs] [Times: user=0.09 sys=0.01, real=0.01 secs] 
15 Heap
16  PSYoungGen      total 92160K, used 89875K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
17   eden space 81920K, 97% used [0x00000000f9c00000,0x00000000fe9ca7f0,0x00000000fec00000)
18   from space 10240K, 99% used [0x00000000fec00000,0x00000000ff5fa578,0x00000000ff600000)
19   to   space 10240K, 0% used [0x00000000ff600000,0x00000000ff600000,0x0000000100000000)
20  ParOldGen       total 102400K, used 24917K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
21   object space 102400K, 24% used [0x00000000f3800000,0x00000000f5055500,0x00000000f9c00000)
22  Metaspace       used 53289K, capacity 56246K, committed 56496K, reserved 1097728K
23   class space    used 7020K, capacity 7577K, committed 7600K, reserved 1048576K

从第16行可以看出,年轻代总共可用空间为 92160K(90M),已经使用了 89875K(87.7M)。代码中创建了两个30M的byte数组,为何会占用87.7呢?多出来的这部分对象可以认为是对象数组本身额外需要占用的内存空间以及程序运行时所创建的一些额外的对象,就称为未知对象吧。
从第17行之后可以看出,Eden 使用了 97%,From Survivor 使用了 99%、To Survivor 使用了 0%、老年代使用率均为 0%。可以确认对象首先是分配到 Eden 区的。

2、Eden 区满了触发 YoungGC
使用如下jvm参数:

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:D:\data\gc.log 

运行如下代码,第2、3、4行将产生60M的垃圾对象,第6行再分配时,eden 区空间就不够分配了,此时就会触发一次 YoungGC:

 

3、大对象将直接进入老年代
要控制大对象的阀值可以通过 -XX:PretenureSizeThreshold 参数设置,但是它只对 Serial 和 ParNew 回收器生效,对 Parallel Scavenge 不生效,所以这里我们使用 ParNew + CMS 的回收器组合,并设置大对象阀值为4M;

4、长期存活的对象将进入老年代
对象诞生在eden区中,eden区满了之后,就会触发YoungGC,将eden区存活的对象复制到survivor中,此时对象的GC年龄设为1岁。对象每熬过一次GC,GC年龄就增加1岁,当它超过一定阀值的时候就会被晋升到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15。

5、动态对象年龄判断
动态对象年龄判断是指,在复制前,如果 survivior 区域内年龄1+年龄2+年龄3+...+年龄n的对象总和大于survivor区的50%时,年龄n及以上存活的对象就会进入老年代,不一定要达到15岁。

6、无法放入Survivor区直接进入老年代
YoungGC时,如果eden区+ from survivor 区存活的对象无法放到 to survivor 区了,这个时候会直接将部分对象放入到老年代。

7、老年代空间分配担保原则

如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。
通过下图来了解空间分配担保原则:

分配担保规则在JDK7之后有些变化,不再判断 -XX:HandlePromotionFailure 参数。YoungGC发生时,只要老年代的连续空间大于新生代对象总大小,或者大于历次晋升的平均大小,就可以进行 YoungGC,否则就进行 FullGC。

8、CMS触发OldGC
CMS回收器有个参数 -XX:CMSInitiatingOccupancyFraction 来控制当老年代内存占用超过这个比例后,就触发CMS回收。因为CMS要预留一些空间保证在回收期间,可以让对象进入老年代。
设置如下jvm参数:当老年代超过80%时,触发CMS回收,CMS GC线程每个2秒检查一次是否回收。

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=15M -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSWaitDuration=2000 

还需注意的是,并不是超过80%就立即触发CMS回收,CMS自己有个间隔时间,通过 -XX:CMSWaitDuration 参数设置,其默认值为2000毫秒,从这里也可以看出,程序运行2秒后才触发了CMS的回收。

9、总结
1)内存参数设置

 

2)垃圾回收触发时机

参考博客:https://www.cnblogs.com/chiangchou/p/jvm-2.html

posted @ 2021-06-05 19:08  郭慕荣  阅读(226)  评论(0编辑  收藏  举报