猿创征文|JVM之自动内存管理详解

前言

Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。本文将通过代码进行实践。

本文基于JDK8

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。

以下通过代码试验:

参数设置

-Xms20M-Xmx20M-Xmn10M
//这三个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。
-XX:Survivor-Ratio=8
// 决定了新生代中Eden区与一个Survivor区的空间比例是8∶1。

代码测试

测试代码如下:

private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     */
    public static void testAllocation() {
    
    
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[3 * _1MB];
        allocation3 = new byte[4 * _1MB];
        // 出现一次Minor GC
    }

    public static void main(String[] args) {
    
    
        testAllocation();
    }

运行结果

[GC (Allocation Failure) [PSYoungGen: 5363K->1016K(9216K)] 5363K->3271K(19456K), 0.0023059 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 8419K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 90% used [0x00000000ff600000,0x00000000ffd3af80,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe010,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 2255K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 22% used [0x00000000fec00000,0x00000000fee33d18,0x00000000ff600000)
 Metaspace       used 3344K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 366K, capacity 388K, committed 512K, reserved 1048576K

结果分析

参数解释:

  • GC:表明进行了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC ,注意它不表示只GC新生代,并且现有的不管是新生代还是老年代都会STW。

  • Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

  • PSYoungGen:(PS是指Parallel Scavenge)为Eden+FromSpace,而整个YoungGeneration为Eden+FromSpace+ToSpace。

  • ParOldGen:ParOldGen表示gc回收前后老年代的内存变化

5363K:YoungGC前新生代内存占用

1016K:YoungGC后新生代内存占用

9216K:新生代总共大小

5363K:YoungGC前JVM堆内存占用

3271K:YoungGC后JVM堆内存使用

19456K:JVM堆总共大小

0.0023059 secs:YoungGC耗时

user=0.00:YoungGC用户耗时

sys=0.00:YoungGC系统耗时

real=0.00:YoungGC实际耗时

这次收集结束后,4MB的allocation3对象顺利分配在Eden中。老年代被占用2MB(被allocation1占用)。通过GC日志可以证实这一点。

main为空运行时,eden仍有占用。如下代码所示:

    public static void main(String[] args) {
    
    
//        testAllocation();
    }
Heap
 PSYoungGen      total 9216K, used 3480K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 42% used [0x00000000ff600000,0x00000000ff966058,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3329K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 364K, capacity 388K, committed 512K, reserved 1048576K

大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

参数设置

XX:PretenureSizeThreshold=<字节大小>

参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 -XX:+UseParNewGC

代码测试

public static void testAllocation() {
    
    
     byte[] allocation1, allocation2, allocation3;
     allocation1 = new byte[2 * _1MB];
     allocation2 = new byte[2 * _1MB];
     allocation3 = new byte[4 * _1MB];
 }

 public static void main(String[] args) {
    
    
     testAllocation();
 }

运行结果

Heap
 PSYoungGen      total 9216K, used 7576K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 92% used [0x00000000ff600000,0x00000000ffd66078,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
 Metaspace       used 3313K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K

结果分析

测试以Jdk1.8为例子,未发生GC,allocation3 直接进入老年代。

长期存活的对象将进入老年代

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。

参数设置

对象晋升老年代的年龄阈值,可以如下参数设置。

-XX:MaxTenuringThreshold

代码测试

 private static final int _1MB = 1024 * 1024;

 /**
  * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
  */
 @SuppressWarnings("unused")
 public static void testTenuringThreshold() {
    
    
     byte[] allocation1, allocation2, allocation3;
     allocation1 = new byte[_1MB]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
     allocation2 = new byte[3 * _1MB];
     allocation3 = new byte[3 * _1MB];
     allocation3 = null;
     allocation3 = new byte[3 * _1MB];
 }

 public static void main(String[] args) {
    
    
     testTenuringThreshold();
 }

运行结果

[GC (Allocation Failure) 
Desired survivor size 1048576 bytes, new threshold 1 (max 1)
[PSYoungGen: 7411K->1016K(9216K)] 7411K->5311K(19456K), 0.0315319 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 
Heap
 PSYoungGen      total 9216K, used 7398K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3bad8,0x00000000ffe00000)
  from space 1024K, 99% used [0x00000000ffe00000,0x00000000ffefe010,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4295K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 41% used [0x00000000fec00000,0x00000000ff031d28,0x00000000ff600000)
 Metaspace       used 3344K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 366K, capacity 388K, committed 512K, reserved 1048576K

动态对象年龄判定

HotSpot虚拟机并不是永远要求对象的年龄必须达到XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

参数设置

Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=1
-XX:+PrintTenuringDistribution -XX:+UseParNewGC

代码测试

   private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
     * -XX:+PrintTenuringDistribution -XX:+UseParNewGC
     */
    @SuppressWarnings("unused")
    public static void testTenuringThreshold2() {
    
    
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于Survivor空间一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];

    }

    public static void main(String[] args) {
    
    
        testTenuringThreshold2();
    }

运行结果

[GC (Allocation Failure) [ParNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1035400 bytes,    1035400 total
: 7923K->1022K(9216K), 0.0026930 secs] 7923K->5754K(19456K), 0.0027389 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 5118K->0K(9216K), 0.0006691 secs] 9850K->5765K(19456K), 0.0006902 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5765K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  56% used [0x00000000ff600000, 0x00000000ffba1608, 0x00000000ffba1800, 0x0000000100000000)
 Metaspace       used 3237K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间,如果这个条件成立,那这一次MinorGC可以确保是安全的。如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle
Promotion Failure)
;如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

空间分配担保避免FullGC过于频繁。

在这里插入图片描述

在JDK 6 Update 24之后

JDK 6 Update 24之后不再使用-XX:HandlePromotionFailure参数。

Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Unrecognized VM option 'HandlePromotionFailure'
Did you mean '(+/-)PromotionFailureALot'?

在这里插入图片描述

 private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
     */
    @SuppressWarnings("unused")
    public static void testHandlePromotion() {
    
    
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation1 = null;
        allocation4 = new byte[2 * _1MB];
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2 * _1MB];
    }

    public static void main(String[] args) {
    
    
        testHandlePromotion();
    }

小结

进入老年代4种情况:
请添加图片描述

1.存活对象达到年龄阈值(默认为15)
2.大对象直接进入老年代
3.幸存者区中如果有相同年龄的对象所占空间大于幸存者区的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。(动态对象年龄判定)
4.MGC后,S区空间不能容纳全部存活对象,直接进入老年代。

在这里插入图片描述
点赞 收藏 关注

猜你喜欢

转载自blog.csdn.net/qq_35764295/article/details/126598943