jvm,深入理解java虚拟机,内存分配与回收策略

Posted 你是我的天晴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm,深入理解java虚拟机,内存分配与回收策略相关的知识,希望对你有一定的参考价值。

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对

象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量篇幅去
介绍虚拟机中的垃圾收集器体系以及运作原理,现在我们再一起来探讨一下给对象分配内存
的那点事儿。
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标
量类型并间接地栈上分配[1]
),对象主要分配在新生代的Eden区上,如果启动了本地线程分
配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的
规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟
机中与内存相关的参数的设置

对象内存的分配主要在堆的新生代Eden区,少数情况会直接分配到老年代。

对象优先在Eden上分配

多数情况下,对象在新生代Eden上分配。Eden空间不够,虚拟机发起一次Minor GC。

-XX:+PrintGCDetail 打印内存回收日志,并且在进程退出时输出当前内存各区域的分配情况

-XX:+SurvivorRation=8 设置新生代中Eden区与一个Survivor区的空间比例

代码:

public class TestAllocation   
    private static final int _1MB = 1024*1024;  
      
    public static void main(String[] args)  
        testAllocation();  
      
      
    public static void testAllocation()  
        byte[] allocation1,allocation2,allocation3,allocation4;  
        allocation1 = new byte[2*_1MB];  
        allocation2 = new byte[2*_1MB];  
        allocation3 = new byte[2*_1MB];  
        allocation4 = new byte[4*_1MB];  
      
  
  

运行参数: -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M

参数解释: UseSerialGC 虚拟机使用Serial+SerialOld收集器进行回收

结果:

[GC[DefNew: 6980K->464K(9216K), 0.0048934 secs] 6980K->6608K(19456K), 0.0049317 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]   
Heap  
 def new generation   total 9216K, used 4890K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  54% used [0x00000000f9a00000, 0x00000000f9e52798, 0x00000000fa200000)  
  from space 1024K,  45% used [0x00000000fa300000, 0x00000000fa3741f0, 0x00000000fa400000)  
  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)  
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)  
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)  
 compacting perm gen  total 21248K, used 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb074888, 0x00000000fb074a00, 0x00000000fc2c0000)  
No shared spaces configured.  

解释:

发生了一次Minor GC,新生代内存从6980K变为464K,但是总内存大小基本没变6980K-6608K,那是因为allocation1,allocation2,allocation3三个对象都存活,没有回收对象。

发生回收的原因为新生代不够为allocation4分配内存,此时根据复制算法需要将Eden+Survivor from复制到Survivor to区域,但是Survivor to不够放置3个2M的对象。此时出发分配担保机制,将这3个2M的对象转移到老年代。

所以最终的内存分配情况是:allocation4放在了eden区,allocation1,allocation2,allocation3放在了老年代。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝

生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major  GC/Full  GC):指发生在老年代的GC,出现了Major  GC,经常会伴
随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行
Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代

大对象指需要大量连续内存的对象,如字符串、数组。大对象会导致内存还有不少空间(但还不够)就会触发垃圾回收。

-XX:PretenureSizeThreshold 大于该值的对象直接在老年代中分配,避免在eden和两个Survivor区间发生复制

代码:

public class TestPretenureThreshold   
    private static final int _1MB = 1024*1024;  
      
    public static void testPretenureSizeThreshold()  
        byte[] allocation;  
        allocation = new byte[4 * _1MB];  
      
      
    //-XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=2M -Xms20M -Xmx20M -Xmn10M  
    public static void main(String[] args)  
        testPretenureSizeThreshold();  
      
  
  

结果:

Heap  
 def new generation   total 9216K, used 999K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  12% used [0x00000000f9a00000, 0x00000000f9af9f70, 0x00000000fa200000)  
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)  
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)  
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)  
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)  
 compacting perm gen  total 21248K, used 2511K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb073d08, 0x00000000fb073e00, 0x00000000fc2c0000)  
No shared spaces configured.  


解释:allocation被直接分配到了老年代。

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

虚拟机采用分代的思想管理内存,每个对象都存在年龄(Age)计数器,如果对象在eden出生并经过第一次Minor GC后依然存活,并被Survivor容纳的话,其年龄就会被设置为1,然后在Survivor区中每熬过一次Minor GC,年龄就会加1,当年龄增加到一定程度(默认15),就会晋升为老年代。

-XX:MaxTenuringThreshold 设置晋升为老年代的年龄阀值。

-XX:+PrintTenuringDistribution 输出Survivor中对象的年龄分布。

代码:

public class TestTenuringThreshold   
    private static final int _1MB = 1024*1024;  
      
    public static void testTenuringThreshold()  
        byte[] allocation1,allocation2,allocation3;  
        allocation1 = new byte[_1MB/4];  
        allocation2 = new byte[4 * _1MB];  
        allocation3 = new byte[4 * _1MB];  
        allocation3 = null;  
        allocation3 = new byte[4 * _1MB];  
      
      
    //-XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -Xms20M -Xmx20M -Xmn10M  
    public static void main(String[] args)  
        testTenuringThreshold();  
      

结果:

[GC[DefNew  
Desired survivor size 524288 bytes, new threshold 1 (max 1)  
- age   1:     737872 bytes,     737872 total  
: 5188K->720K(9216K), 0.0049431 secs] 5188K->4816K(19456K), 0.0050418 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]   
[GC[DefNew  
Desired survivor size 524288 bytes, new threshold 1 (max 1)  
- age   1:        232 bytes,        232 total  
: 5065K->0K(9216K), 0.0018952 secs] 9161K->4816K(19456K), 0.0019256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
 def new generation   total 9216K, used 4178K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e14820, 0x00000000fa200000)  
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa2000e8, 0x00000000fa300000)  
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)  
 tenured generation   total 10240K, used 4816K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)  
   the space 10240K,  47% used [0x00000000fa400000, 0x00000000fa8b4020, 0x00000000fa8b4200, 0x00000000fae00000)  
 compacting perm gen  total 21248K, used 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb074878, 0x00000000fb074a00, 0x00000000fc2c0000)  
No shared spaces configured.  

解释:

allocation1,allocation2被分配到eden区中,allocation3申请内存不够,触发Minor GC,allocation1 被移入Survivor中,内存还不够allocation2移入老年代

allocation3再次申请内存,allocation1会被移入老年代

最终新生代为allocation3  老年代为allocation1,allocation2

动态对象年龄判定

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

/**
 *  长期存活的对象进入老年代
 * *VM参数:-verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 * XX:+PrintTenuringDistribution得到更详细的GC输出
 *
 * -XX:+UseSerialGC   Serial + SerialG old
 */
public class TestTenuringThreshold 
    private static final int _1MB=1024*1024;

    public static void main(String[] args) 
            byte[] allocation1,allocation2,allocation3;
             allocation1=new byte[_1MB/4];
            //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
             allocation2=new byte[4*_1MB];
             allocation3=new byte[4*_1MB];
             allocation3=null;
             allocation3=new byte[4*_1MB];
    

执行代码,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预
期增加了6%,也就是说,allocation1、allocation2对象都直接进入了老年代,而没有等到15
岁的临界年龄。因为这两个对象加起来已经到达了512KB,并且它们是同年的,满足同年对
象达到Survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个

就不会晋升到老年代中去了

 空间分配担保

在Minor GC(新生代GC)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果成立,那么Minor GC是安全的。不成立,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。

如果允许,继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,大于则尝试一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了
内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor
GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要
老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类
似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多
少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋
升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进
行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活
后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle  Promotion  Failure)。
如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full  GC。虽然担保
失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避
免Full GC过于频繁

以上是关于jvm,深入理解java虚拟机,内存分配与回收策略的主要内容,如果未能解决你的问题,请参考以下文章

jvm,深入理解java虚拟机,内存分配与回收策略

深入理解Java虚拟机 -- 内存分配与回收策略

深入理解Java虚拟机-Java内存区域,垃圾回收机制和内存分配策略

深入理解Java虚拟机-Java内存区域,垃圾回收机制和内存分配策略

《深入理解java虚拟机》笔记内存分配与回收策略

深入理解java虚拟机内存分配与回收策略实战