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

2023-10-16 09:06:10 浏览数 (1)

一、垃圾回收日志说明

[GC[DefNew: 7307K->494K(9216K), 0.0043710 secs] 7307K->6638K(19456K), 0.0044894 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

1、日志开发的“[GC”说明了这次垃圾回收的停顿类型,如果有FULL,说明这次GC是发生了Stop-The-World的。

2、接下来的“[DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。例如Serial收集器中的新生代名“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,显示“[ParNew”。如果是Parallel Scavenge收集器,显示“PSYoungGen”。

3、后面方括号内部的“7307K->494K(9216K)”含义是:GC前该区域已使用容量->GC后该区域已使用容量(该内存区域总容量)。而在方括号之外的“7307K->6638K(19456K)”含义是:GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)。

4、再往后,“0.0044894 secs”表示该内存区域GC所占用的时间,单位秒。

二、对象优先在Eden分配

对象通常在新生代的Eden区分配,当Eden区没有足够空间时,虚拟机会发起一次Minor GC。与Minor GC对应的还有Major GC、Full GC。

Minor GC:指发生在新生代的垃圾收集动作,非常频繁。速度较快。

Major GC:指发生在老年代的GC,出现Major GC,经常会伴随一次Minor GC,同时Minor GC也会引起Major GC,一般在GC日志中统称为GC,不频繁。

Full GC:指发生在老年代和新生代的GC,速度较慢,需要Stop The World。

测试代码:

代码语言:javascript复制
/**
 * 虚拟机参数为“-verbose:gc -XX: PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8”,即10M新生代,10M老年代,10M新生代中8M的Eden区,两个Survivor区各1M
 * -Xms为堆最小容量,-Xmx为堆最大容量,-Xmn为新生代容量,SurvivorRatio=8为Eden和Survivor(from、to)比例为8:1:1
 */
public class TestAllocation {
    
    private static final int _1MB = 1024 * 1024;
    public static void test() {
        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]; //出现一次minor GC
    }
    
    public static void main(String[] args) {
        test();
    }
}

测试结果:

代码语言:javascript复制
[GC[DefNew: 7307K->494K(9216K), 0.0043710 secs] 7307K->6638K(19456K), 0.0044894 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4920K [0x04800000, 0x05200000, 0x05200000)
  eden space 8192K,  54% used [0x04800000, 0x04c52770, 0x05000000)
  from space 1024K,  48% used [0x05100000, 0x0517b970, 0x05200000)
  to   space 1024K,   0% used [0x05000000, 0x05000000, 0x05100000)
 tenured generation   total 10240K, used 6144K [0x05200000, 0x05c00000, 0x05c00000)
   the space 10240K,  60% used [0x05200000, 0x05800030, 0x05800200, 0x05c00000)
 compacting perm gen  total 12288K, used 1672K [0x05c00000, 0x06800000, 0x09c00000)
   the space 12288K,  13% used [0x05c00000, 0x05da22f0, 0x05da2400, 0x06800000)

说明:

GC前新生代占用了7M,然后来了一个4M,Eden和from仅剩下2M不够分配了,触发了一次Minor GC。但是GC后内存仍然不够,因为application1、application2、application3三个引用还存在,另外一块1M的survivor也不够放下这总共6M的三个对象,那么这次Minor GC的效果其实是通过分配担保机制将这6M的内容转入老年代中。然后再来一个4M的,由于此时Minor GC之后新生代只用了494K,够分配了,所以4M顺利进入新生代。

三、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,最典型的大对象是那种很长的字符串以及数组,大对象对虚拟机的分配来说是个坏消息(比遇到大对象更加怀的消息就是遇到一群“朝生夕灭”的短命大对象,写程序时候应避免)。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集器回收,以获取足够的连续空间来安置他们。

虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阀值,超过阀值的对象直接进入老年代。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集)。

测试代码(超过3M直接分配进老年代):

代码语言:javascript复制
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8
 * -XX:PretenureSizeThreadshold=3145728
 */
public class TestPretenureSizeThreadshold {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] application = new byte[4 * _1MB];
        
    }
}

测试结果:

代码语言:javascript复制
Heap
 def new generation   total 9216K, used 1327K [0x04600000, 0x05000000, 0x05000000)
  eden space 8192K,  16% used [0x04600000, 0x0474bf18, 0x04e00000)
  from space 1024K,   0% used [0x04e00000, 0x04e00000, 0x04f00000)
  to   space 1024K,   0% used [0x04f00000, 0x04f00000, 0x05000000)
 tenured generation   total 10240K, used 4096K [0x05000000, 0x05a00000, 0x05a00000)
   the space 10240K,  40% used [0x05000000, 0x05400010, 0x05400200, 0x05a00000)
 compacting perm gen  total 12288K, used 1670K [0x05a00000, 0x06600000, 0x09a00000)
   the space 12288K,  13% used [0x05a00000, 0x05ba1a50, 0x05ba1c00, 0x06600000)

四、长期存活对象进入老年代

虚拟机给每个对象定义了一个年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳,对象年龄就加1。对象在Survivor区每熬过一次Minor GC,年龄就加1。当年龄超过一定阀值(默认为15),则进入老年代中。阀值可通过-XX:MaxTenuringThreshold来设置。

测试代码:

代码语言:javascript复制
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8 
 * -XX:MaxTenuringThreshold=1 -XX: PrintTenuringDistribution
 */
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];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
}

测试结果:

代码语言:javascript复制
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     768272 bytes,     768272 total
: 5515K->750K(9216K), 0.0050436 secs] 5515K->4846K(19456K), 0.0051715 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:        256 bytes,        256 total
: 5094K->0K(9216K), 0.0015457 secs] 9190K->4845K(19456K), 0.0016256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x04400000, 0x04e00000, 0x04e00000)
  eden space 8192K,  51% used [0x04400000, 0x04814828, 0x04c00000)
  from space 1024K,   0% used [0x04c00000, 0x04c00100, 0x04d00000)
  to   space 1024K,   0% used [0x04d00000, 0x04d00000, 0x04e00000)
 tenured generation   total 10240K, used 4845K [0x04e00000, 0x05800000, 0x05800000)
   the space 10240K,  47% used [0x04e00000, 0x052bb6e8, 0x052bb800, 0x05800000)
 compacting perm gen  total 12288K, used 1672K [0x05800000, 0x06400000, 0x09800000)
   the space 12288K,  13% used [0x05800000, 0x059a2248, 0x059a2400, 0x06400000)

说明:发生了两次Minor GC,第一次是在给allocation3进行分配的时候会出现一次Minor GC,此时survivor区域不能容纳allocation2,但是可以容纳allocation1,所以allocation1将会进入survivor区域并且年龄为1,达到了阈值,将在下一次GC时晋升到老年代,而allocation2则会通过担保机制进入老年代。第二次发生GC是在第二次给allocation3分配空间时,这时,allocation1的年龄加1,晋升到老年代,此次GC也可以清理出原来allocation3占据的4MB空间,将allocation3分配在Eden区。所以,最后的结果是allocation1、allocation2在老年代,allocation3在Eden区。

五、动态对象年龄判定

虚拟机并未要求对象一定要达到年龄阀值后,才可进入老年代。当Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象也可直接进入老年代。

测试代码:

代码语言:javascript复制
/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8 
 * -XX:MaxTenuringThreshold=15 -XX: PrintTenuringDistribution
 */
public class TestTenuringThreshold2 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1 = new byte[_1MB / 4];
        //application1 application2大于Survivor空间一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
}

测试结果:

代码语言:javascript复制
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1030592 bytes,    1030592 total
: 5771K->1006K(9216K), 0.0042280 secs] 5771K->5102K(19456K), 0.0043710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:        256 bytes,        256 total
: 5350K->0K(9216K), 0.0016450 secs] 9446K->5102K(19456K), 0.0017286 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x04400000, 0x04e00000, 0x04e00000)
  eden space 8192K,  51% used [0x04400000, 0x04814828, 0x04c00000)
  from space 1024K,   0% used [0x04c00000, 0x04c00100, 0x04d00000)
  to   space 1024K,   0% used [0x04d00000, 0x04d00000, 0x04e00000)
 tenured generation   total 10240K, used 5101K [0x04e00000, 0x05800000, 0x05800000)
   the space 10240K,  49% used [0x04e00000, 0x052fb798, 0x052fb800, 0x05800000)
 compacting perm gen  total 12288K, used 1672K [0x05800000, 0x06400000, 0x09800000)
   the space 12288K,  13% used [0x05800000, 0x059a2260, 0x059a2400, 0x06400000)

结果说明:发生了两次Minor GC,第一次发生在给allocation4分配内存时,此时allocation1、allocation2将会进入survivor区,而allocation3通过担保机制将会进入老年代。第二次发生在给allocation4分配内存时,此时,survivor区的allocation1、allocation2达到了survivor区容量的一半,将会进入老年代,此次GC可以清理出allocation4原来的4MB空间,并将allocation4分配在Eden区。最终,allocation1、allocation2、allocation3在老年代,allocation4在Eden区。

六、空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大连续空间是否大于新生代所有对象大小总和。若成立,则说明Minor GC是安全的。否则,虚拟机需要查看HandlePromotionFailure的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC,以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。

具体的流程图如下:

0 人点赞