编程实现运行时触发指定次数的GC

152 阅读2分钟

注:本专栏文章均为本人原创,未经本人授权请勿私自转载,谢谢。

题目:请写一段程序,使其运行时表现为:5次 Young GC -> 3次Full GC -> 3次 Young GC -> 1次 Full GC。

GC Log 中各参数的说明:

[(年轻代)GC 收集器: GC 前 -> 后(总)容量, 该区 GC 耗时] [...] 堆区: GC 前->后(总)容量, 该区 GC 耗时
[GC (Allocation Failure) [ParNew: 6624K->980K(9216K), 0.0027855 secs] 6624K->4052K(50176K), 0.0028195 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

解法 1( Serial GC/ ParNew GC ):

使用最简单的 Serial GC / ParNew GC 来实现,以下是代码实现:

private static final int MB = 1024 * 1024;
​
@SuppressWarnings("MismatchedReadAndWriteOfArray")
public static void main(String[] args) {
    // JVM 参数:-Xmx50m -Xms50m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+PrintGCDetails
    // 由参数可以算出:Eden 区大小为 8M,老年代大小为 40M
    System.out.println("初始内存占用:" + ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() / 1024 + "K");
    byte[][] bytes = new byte[10][];
​
    // 循环 1:初始时堆内存为 3.7M(Debug)2.1M(Run),所以对于 Run 和 Debug 都是从第二个开始,每两次触发一次 Minor GC。
    // 由于未达内存上限,此时做的所有 GC 都是 Minor GC。注意,以下所涉及的所有讨论均是针对于 Run 的情况,为了计算简便,假定
    // 初始堆为 2M,并对期间 GC 所得的剩余空间值做了一定的简化处理。以下为在该循环中所经历的内存变动:
    // ~~Time  开始前  0         1         2           3            4           5           6     ...         9
    // ~~Young  2M   5M  5M->700k->3.7M  6.7M  6.7M->100k->3.1M  6.1M  6.1M->100K->3.1M  6.1M  ...  6.1M->100K->3.1M
    // ~~Old    0M   0M        3M         3M         9M            9M         12M         12M   ...        24M
    // 【注意】:在此期间也产生了一些系统资源垃圾,大部分随着不断地 Minor GC 被回收了(例如 1 中 Minor GC 后剩余的 700K 大部
    // 分也被 2 中的 Minor GC 所回收掉了)。
    for (int i = 0; i < 10; i++) {
        bytes[i] = new byte[3 * MB];
    }
​
    // 循环 2:每次放入足够大的对象,新生代 Eden 区不够大则直接放入老年代,且每次都超过老年代剩余空间,则此时会执行 Full GC。
    // 【注意】:这里记得每次回收对象,否则会造成内存溢出
    bytes = null;
    for (int i = 0; i < 3; i++) {
        byte[] b = new byte[20 * MB];
        b = null;
    }
​
    // 循环 3:至此,新生代接近 0K,但由于上个循环最后一个对象不会自发 GC ,导致老年代还有 20M 的空间被占用。而此时,由于新生代
    // 接近 0 K,要放第 3 个对象的时候才会触发第一次 Minor GC,之后则是每两个触发一次 Minor GC。
    bytes = new byte[7][];
    for (int i = 0; i < 7; i++) {
        bytes[i] = new byte[3 * MB];
    }
​
    // 最后放入一个大对象,Full GC 结束。
    bytes = null;
    byte[] b = new byte[30 * MB];
    b = null;
}

解法 2( Parallel GC ):

使用 Parallel GC 来实现,Parallel 在分配 Eden 区空间时,会有一个悲观策略,以下是代码实现:

private static final int MB = 1024 * 1024;
​
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
public static void main(String[] args) {
    // JVM 参数:-Xmx42m -Xms42m -Xmn10m -XX:SurvivorRatio=10 -XX:+UseParallelGC -XX:+PrintGCDetails
    // 由参数可以算出:Eden 区大小为 9M,老年代大小为 32M
    System.out.println("初始内存占用:" + ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() / 1024 + "K");
    List<byte[]> caches = new ArrayList<>();
​
    // 循环 1:初始时堆内存为 3.8M(Debug)2.2M(Run),所以说,对于 Run 情况,从第三个开始,每隔两次触发一次 Minor GC
    // (Debug 情况则是从第二个开始,每隔两次触发一次 Minor GC)。注意,以下所涉及的所有讨论均是针对于 Run 的情况,为了计
    // 算简便,假定初始堆为 2M,并对期间 GC 所得的剩余空间值做了一定的简化处理。以下为在该循环中所经历的内存变动:
    // ~~Time  开始前  0   1         2          3           4           5           6        ...         10
    // ~~Young  2M   5M  8M  8M->500k->3.5M  6.5M  6.5M->500K->3.5M  6.5M  6.5M->500K->3.5M ... 6.5M->500K->3.5M
    // ~~Old    0M   0M  0M        6M         6M          12M         12M         18M       ...        30M
    // 另外,由于 Parallel GC 的悲观策略,导致其运行到第 11 次,即 i = 10 时,出现 Full GC,这是因为当 i = 10 时,会触发
    // 一次 Minor GC,此时老年代所占内存为 30M,然后,在将该个对象复制到年轻代前,此时会对当前空间值做如下判断:
    // ~~ min(目前新生代已使用的大小, 平均晋升到老年代的大小) > 旧生代剩余空间大小 ? 执行 Full GC : Minor GC
    // 则在这里有:min(3.5M,6M) > 32-30=2M。成立。即再执行一次 Full GC,然后再将该对象复制到年轻代内存区。
    for (int i = 0; i < 11; i++) {
        caches.add(new byte[3 * MB]);
    }
​
    // 此时年轻代 3.5M,老年代 30M(注意,由于 caches 还在引用,上步的 Full GC 只是将一些系统资源移位,忽略该影响)
    caches.add(new byte[3 * MB]);
​
    // 在 add 了一个 byte 数组后,此时的年轻代 6.5M,老年代 30M,两者都无法再容纳下一个 3M 的对象了。这里的做法是:
    // 删除掉数组第 1 个值,注意这里第 1 个值一定是在老年代的,然后插入一个 3M 的对象,然后 GC 发现该处无法复制到老
    // 年代,只能再次进行 Full GC,由于都在引用,所以只将 1 个数组移位到老年代,新来的数组则插入到该数组的位置处。
    caches.remove(0);
    caches.add(new byte[3 * MB]);
​
    // 删除掉 0 到 8 的元素,它们也都是在老年代的,注意,此时的 0 元素是原先的 1 元素,他们也都是在老年代。
    caches.subList(0, 8).clear();
​
    // 循环 2:此时的情况是,年轻代 6.5M,老年代 6M,老年代 24M 的未引用对象(待回收)。则第一次 add 时,由于年轻代
    // 内存空间不够,将会触发一次 Full GC,将老年代 24M 空间回收;而在 i=6 时,跟循环 1 中 i=11 时的情况是一样的。
    // 以下是该循环中产生的 GC 情况:
    // ~~Time  开始前          0          1           2           3   ...          6
    // ~~Young 6.5M  6.5M->0.5M->3.5M  6.5M  6.5M->500K->3.5M  6.5M  ...  6.5M->500K->3.5M
    // ~~Old    30M    30M->6M->12M     12M         18M         18M  ...         30M
    for (int i = 0; i < 7; i++) {
        caches.add(new byte[3 * MB]);
    }
}