注:本专栏文章均为本人原创,未经本人授权请勿私自转载,谢谢。
题目:请写一段程序,使其运行时表现为: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]);
}
}