JVM-内存分配与回收策略实例

45 阅读21分钟

JVM-内存分配与回收策略实例

下面的这些策略都是基于分代收集模型的;并且 垃圾收集器是基于 Serial + Serial Old!

一、对象优先在Eden分配

下面的测试案例:allocation4对象的语句时会发生一次Minor GC,新生代会内存空间会被回收,但是总内存容占用了却几乎不发生改变;产生这次垃圾收集的原因是为allocation4分配内存时,发现Eden已经被占用了6MB(myAlloc1,myAlloc2,myAlloc3),剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。垃圾收集期间虚拟机又发现已有的三个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。

通过GC日志我们可以分析出来。

/**
 * VM参数:-verbose:gc  输出虚拟机中GC的详细情况
 *        仅仅打印的这一行:[GC (Allocation Failure) [PSYoungGen: 6673K->879K(9216K)] 6673K->4983K(19456K), 0.0053822 secs]
 *                                             [Times: user=0.00 sys=0.00, real=0.00 secs]
 *        -Xms20M  堆空间最少的容量
 *        -Xmx20M  堆空间最大的容量
 *        -Xmn10M  年轻代大小
 *        -XX:+PrintGCDetails  打印出GC对各个区域内存的回收过程
 *        -XX:SurvivorRatio=8  Eden、Survivor占比 8:1:1
 */
public class MyTest1 {
    public static void main(String[] args) {
        int size = 1024 * 1024;  //1M
        byte[] myAlloc1 = new byte[2 * size];
        byte[] myAlloc2 = new byte[2 * size];
        byte[] myAlloc3 = new byte[2 * size];
        byte[] myAlloc4 = new byte[4 * size];
​
        System.out.println("hello world");
    }
}
​
// GC (Allocation Failure) : 空间分配失败,发生GC;
// PS : Parallel Scavenge 收集器,YoungGen是新生代;
// 6673K->911K(9216K) : 这个是对新生代空间的回收;
//                    6673K:GC前该内存区域使用容量,911K:GC后该内存区域使用容量,
//                    9216K:该内存区域总容量(9M空间,即一个Eden:8M,一个Survivor:1M),因为复制算法,永远有一个survivor空间浪费掉了
//                    6673K - 911K = 5762K 释放的空间
// 6673K->5015K(19456K) : 和这个是堆的内存情况
//                      6673K:堆区垃圾回收前的大小,5015K:堆区垃圾回收后的大小,
//                      19456K : 堆区总大小;19M,也就是说,一个Survivor没有算上
//                      6673K - 5015K = 1658K,说明整个堆上释放了 1658K空间
// 0.0026703 secs : 该内存区域GC耗时,单位是秒
// [Times: user=0.04 sys=0.00, real=0.01 secs] : 分别表示用户态耗时,内核态耗时和总耗时
// 
// 分析下可以得出结论:
//    该次GC 新生代释放了 6673K - 911K = 5762K 
//    Heap区总共 释放了  6673K - 5015K = 1658K
//    ==> 5762K - 1658K = 4104K ≈ 4M  说明该次GC共有4M内存从年轻代移到了老年代
[GC (Allocation Failure) [PSYoungGen: 6673K->911K(9216K)] 6673K->5015K(19456K), 0.0026703 secs] 
                       [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
// 新生代:
//    total 9216K, used 7459K    : 新生代总可用空间 9216K = 9M , 7459K 被占用的大小
//    eden space 8192K, 79% used : eden 8M,被占用:8192K × 79% ≈ 6M
//    from space 1024K, 88% used :from Survivor 1M,
//                              被占用:88% 不太懂是什么鬼,按理说,实例中4个对象,每一个最小2M,都到不了Survivor中的,不知道这里为啥会有占用
//     to   space 1024K, 0% used : to Survivor 1M, 被占用:0%
 PSYoungGen      total 9216K, used 7459K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 79% used [0x00000000ff600000,0x00000000ffc64f88,0x00000000ffe00000)
  from space 1024K, 88% used [0x00000000ffe00000,0x00000000ffee3cc0,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
                             
// 老年代 :
//     total 10240K : 老年代容量 10M ; 
//     used 4104K :老年代当前被占用的大小 4104K ≈ 4M,就是前面证明的该次GC共有4M内存从年轻代移到了老年代
//     object space 10240K : 老年代中,对象允许占用的容量;
//     40% used : 被占用的百分比,40%,就是4M.
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
                                 
// 元空间
 Metaspace       used 3090K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 324K, capacity 392K, committed 512K, reserved 1048576K

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

在执行下面示例代码我们看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为 -XX:PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能与-Xmx之类的参数一样直接写3MB),因此超过3MB的对象都会直接在老年代进行分配。

/**
 * java -XX:+PrintCommandLineFlags -version : 查看当前虚拟机使用的默认垃圾收集器
 * 由于当前虚拟机使用的是 Parallel Scavenge + Parallel Old 组合,不支持参数 -XX:PretenureSizeThreshold
 * 所以,这里修改一下垃圾收集器组合: -XX:UseSerialGC (Serial + Serial Old)
 *
 * 3145728 = 3MB
 *
 * VM参数:
 *      -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
 *      -XX:PretenureSizeThreshold=3145728
 *      -XX:+UseSerialGC
 */
public class PretenureSizeThresholdTest {
    private static final int _1MB = 1024 * 1024;
    public static void test() {
        byte[] allocation;
        allocation = new byte[4 * _1MB]; //直接分配在老年代中
    }
​
    public static void main(String[] args) {
        test();
    }
}
​
// 首先是没有发生GC
Heap
 def new generation   total 9216K, used 2740K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  33% used [0x00000000fec00000, 0x00000000feead1a8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 
// 老年代
// total 10240K, used 4096K  : 10240K = 10M , 4096K = 4M
// 说明 allocation 变量占用 4M大小, 4M > (3145728 = 3M)  直接将对象分配在老年代中
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3089K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 324K, capacity 392K, committed 512K, reserved 1048576K

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

对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。

/**
 * VM参数:
 *  -verbose:gc -Xms20M -Xmx20M -Xmn10M  -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 *  -XX:+UseSerialGC
 *  -XX:MaxTenuringThreshold=1      :设置 进入老年代阈值大小
 *  -XX:+PrintTenuringDistribution  : 打印 对象进入老年代的年龄阈值大小
 */
public class TenuringThresholdTest {
    private static final int _1MB = 1024 * 1024;
 
    @SuppressWarnings("unused")
    public static void test() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB]; // 这里GC 第一次
        allocation3 = null;
        allocation3 = new byte[4 * _1MB]; // 这里GC 第二次
    }
​
    public static void main(String[] args) {
        test();
    }
}
​
// 第一次GC  (Minor GC)
//    Desired survivor size 524288 bytes, new threshold 1 (max 1) : 
//      survivor允许的大小容量 0.5M,超过这个就要晋升到老年代中。
//     
//    6868K->980K(9216K) : 
//        6868K : GC前占用年轻代大小
//        980K  : GC后占用年轻代大小
//        6868K - 980K = 5888K 年轻代释放的内存空间
//        9216K : 9M, 年轻代可用的空间 Eden:8M, Surivor:1M
//    6868K->5076K(19456K)
//        6868K : GC前占用堆大小
//        5076K : GC后占用堆大小
//        6868K - 5076K = 1792K 堆上的释放空间
//        19456K : 19M, 堆上可用空间,少一个Survivor
//
//    ==>  5888K - 1792K = 4096K = 4M 即:晋升到老年代对象大小为 4M ,其实就是 allocation2对象进入到了老年代
//          也就是说:allocation1还没有晋升或回收,可以熬过一次GC !!!
[ GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1)
  - age 1: 1004104 bytes, 1004104 total : 6868K->980K(9216K), 0.0038407 secs] 6868K->5076K(19456K), 0.0039769 secs] 
[Times: user=0.00 sys=0.00, real=0.00 secs] 
​
// 第二次GC
//     5240K->0K(9216K)
//        5240K - 0K = 5240K 年轻代释放的内存空间
//     9336K->5058K(19456K)
//        9336K - 5058K = 4278K 堆上的释放空间
//
//     ==>  5240K - 4278K = 962K 即:晋升老年代的对象大小。
//           并且我们可以看到,第二次GC,新生代占用的空间被清0了★★★★★
//            说明:allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存在垃圾收集以后非常干净地变成0KB。
[ GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 5240K->0K(9216K), 0.0016666 secs] 9336K->5058K(19456K), 0.0017385 secs] 
[Times: user=0.00 sys=0.00, real=0.00 secs] 
​
// 可以看到两次GC后,空间分配情况:
//    allocation3 : 4M 分配在了 年轻代的 eden中 (used 4178K)
//    allocation1,allocation2 : 4M+256KB 晋升到了老年代中 (used 5058K)
Heap
 def 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 5058K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffaf0b48, 0x00000000ffaf0c00, 0x0000000100000000)
 Metaspace       used 3090K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 324K, capacity 392K, committed 512K, reserved 1048576K

将JVM参数 -XX:MaxTenuringThreshold=15

// 第一次GC Minor GC
// 期望 survivor的空间 :524288 bytes = 521KB = 0.5MB
// new threshold 1:新生代对象年龄达到1时可以进入老年代; age 1 :年龄为1的对象大小:1048576 bytes = 1MB
// 8192K->1024K(9216K) : 8192K=8MB 新生代总大小,1024K=1MB GC后新生代的使用大小,9216K=9MGC后新生代总大小
// 8192K->2111K(19456K): 表示GC前后堆的使用情况 8192K=8MBGC前堆的使用大小,2111K≈2.06MBGC后堆的使用大小,19456K=19MB堆中总大小
[GC (Allocation Failure)
[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1:  1048576 bytes, 1048576 total  
: 8192K->1024K(9216K), 0.0021959 secs] 8192K->2111K(19456K), 0.0022851 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
​
// 第二次GC Minor GC
// 年龄是15了 , 新生代对象有达到15的可以进入老年代
// 6578K->329K(9216K) : ★★★★★
//   说明本次GC,allocation1对象则还留在新生代Survivor空间,这时候新生代仍然有329KB被占用。
[GC (Allocation Failure) 
[DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1:  337856 bytes, 337856 total  
: 6578K->329K(9216K), 0.0030347 secs] 7666K->6535K(19456K), 0.0031469 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
​
// 第三次GC
// DefNew 新生代回收情况,Tenured 老年代回收情况,Metaspace 元空间回收情况
[GC (Allocation Failure)
[DefNew: 4589K->4589K(9216K), 0.0000334 secs]  // 这里我们可以看到,新生代空间没有可回收对象 ★★★★★
[Tenured: 6205K->6488K(10240K), 0.0021953 secs] 10795K->6488K(19456K), 
[Metaspace: 3228K->3228K(1056768K)], 0.0023373 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
​
Heap  
 def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)  
 eden space                 8192K,  52% used  [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)  
 from space                 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)  
 to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)  
 tenured generation   total 10240K, used 6488K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)  
   the space 10240K,  63% used [0x00000000ff600000, 0x00000000ffc561c0, 0x00000000ffc56200, 0x0000000100000000)  
 Metaspace       used 3244K, capacity 4556K, committed 4864K, reserved 1056768K  
  class space    used 337K, capacity 392K, committed 512K, reserved 1048576K  

四、动态对象年龄判定

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

public class Test {
    private static final int _1MB = 1024 * 1024;
 
    /**
     * VM参数:
     *   -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     *   -XX:MaxTenuringThreshold=15
     *   -XX:+PrintTenuringDistribution
     *   -XX:+UseSerialGC
     */
    @SuppressWarnings("unused")
    public static void test() {
        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) {
        test();
    }
}
// 第一次GC
// 这次GC前后的内存使用情况为: 
//   - 年轻代(DefNew):8192K->1024K(9216K),即年轻代从8192K使用到1024K,总大小为9216K。
//   - 堆(Heap):8192K->2111K(19456K),即堆从8192K使用到2111K,总大小为19456K。 
[GC (Allocation Failure)
[DefNewDesired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 1048576 bytes, 1048576 total
: 8192K->1024K(9216K), 0.0027484 secs] 8192K->2111K(19456K), 0.0028598 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
​
// 第二次GC
// 这次GC前后的内存使用情况为: 
//   - 年轻代(DefNew):6838K->586K(9216K),即年轻代从6838K使用到586K,总大小为9216K。 
//   - 堆(Heap):7925K->6791K(19456K),即堆从7925K使用到6791K,总大小为19456K。 
//   ★★★★★ 这里 年轻代从6838K使用到586K ,可以知道经历此次GC之后,allocation1,allocation2熬过了两次GC;
//   ★★★★★ allocation1+allocation2大于survivor空间一半,下次晋升两个变量直接晋升到老年代!!!
[GC (Allocation Failure) 
[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)- age 1: 600216 bytes, 600216 total
: 6838K->586K(9216K), 0.0034962 secs] 7925K->6791K(19456K), 0.0036304 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
​
// 第三次GC
//    我们可以看到,年轻代中使用为4846K-4846K=0k,老年代使用 6745K-6205K=540K
//    也就是 allocation1,allocation2 直接晋升到老年代!!!
[GC (Allocation Failure)
[DefNew: 4846K->4846K(9216K), 0.0000343 secs]
[Tenured: 6205K->6745K(10240K), 0.0025784 secs] 11051K->6745K(19456K), 
[Metaspace: 3228K->3228K(1056768K)], 0.0027457 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] 
​
Heap
 def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 6745K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  65% used [0x00000000ff600000, 0x00000000ffc96588, 0x00000000ffc96600, 0x0000000100000000)
 Metaspace       used 3243K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 337K, capacity 392K, committed 512K, reserved 1048576K

五、空间分配担保

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

这个参数在JDK 6之前可以用,JDK6之后就取消了该参数,同时,只有在JVM的调试版本中才有 -XX:PromotionFailureALot 来替代测试,别的版本,这个参数已经不再使用了。