- 掌握垃圾回收算法,了解垃圾回收过程
- 能够进行GC调优,分析GC日志
实验环境:JDK8 GC学习参考网站:https://www.bilibili.com/video/BV1PJ411n7xZ?from=search&seid=12974670655223980724 垃圾回收算法: 1.标记–清除算法 执行步骤: 标记:遍历内存区域,对需要回收的对象打上标记。 清除:再次遍历内存,对已经标记过的内存进行回收。 图解: 缺点: 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。 2.复制算法 将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。 图解: 优点 相对于标记–清理算法解决了内存的碎片化问题。 效率更高(清理内存时,记住首尾地址,一次性抹掉)。 缺点: 内存利用率不高,每次只能使用一半内存。 3. 标记–整理算法 因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。 执行步骤: 标记:对需要回收的进行标记 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。 图解: 优点 相对于标记–清理算法解决了内存的碎片化问题。 效率比复制算法低,内存利用效率高 4. 分代收集算法 当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如: 新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。 老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。、 新生代垃圾回收过程:新生代将内存按8:1:1(默认情况下)分为一块较大的Eden(伊甸园)区和两块较小的survivor(存活)区:s0与s1(也可称之为from区和to区) 1、程序启动后,所有新建的对象都是出生在Eden(伊甸园)区,两块较小的survivor(存活)区都是空的 2、当Eden(伊甸园)区内存占满后,发生一次 minor gc(小型的垃圾回收),将存活的对象移动到其中一块survivor区(s0)并记录对象的年龄,清空Eden区 3、当再次Eden区占满后,发生第二次minor gc,同时将Eden区与s0区中存活的对象移动至s1区,并且记录对象的年龄,每有一次gc年龄就加1,清空Eden区,将s0与s1名称互换,便于下一次gc 4、对象的年龄存在一个阈值,当年龄超过这个阈值(默认是15)后,就将存活的对象移动至老年代,另一种情况是当某个年龄的对象占据幸存者区的一半,也会讲对象移动至老年代中。 老年代垃圾回收过程:当老年代对象占满后就发生一次full gc,标记整理老年代的存活对象 参考地址:https://www.cnblogs.com/pypua/p/9966050.html Hotspot里System.gc的实现: 参考网站:https://www.cnblogs.com/hushaojun/p/4967529.html 在JDK中System.gc()的实现是通过调用Runtime.getRuntime().gc()源码如下:
/** * Runs the garbage collector. * <p> * Calling the <code>gc</code> method suggests that the Java Virtual * Machine expend effort toward recycling unused objects in order to * make the memory they currently occupy available for quick reuse. * When control returns from the method call, the Java Virtual * Machine has made a best effort to reclaim space from all discarded * objects. * <p> * The call <code>System.gc()</code> is effectively equivalent to the * call: * <blockquote><pre> * Runtime.getRuntime().gc() * </pre></blockquote> * * @see java.lang.Runtime#gc() */ public static void gc() { Runtime.getRuntime().gc(); } |
Runtime类中gc方法:
/** * Runs the garbage collector. * Calling this method suggests that the Java virtual machine expend * effort toward recycling unused objects in order to make the memory * they currently occupy available for quick reuse. When control * returns from the method call, the virtual machine has made * its best effort to recycle all discarded objects. * <p> * The name <code>gc</code> stands for "garbage * collector". The virtual machine performs this recycling * process automatically as needed, in a separate thread, even if the * <code>gc</code> method is not invoked explicitly. * <p> * The method {@link System#gc()} is the conventional and convenient * means of invoking this method. */ public native void gc(); |
这里看到gc方法是native的,在java层面只能到此结束了。 Hotspot里System.gc的实现 上面提到了Runtime.gc是一个本地方法,那需要先在jvm里找到对应的实现,这里稍微提一下jvm里native方法最常见的也是最简单的查找,jdk里一般含有native方法的类,一般都会有一个对应的c文件,比如上面的java.lang.Runtime这个类,会有一个Runtime.c的文件和它对应,native方法的具体实现都在里面了,如果你有source,可能会猜到和下面的方法对应
1 2 3 4 5 | JNIEXPORT void JNICALL Java_java_lang_Runtime_gc(JNIEnv *env, jobject this) { JVM_GC(); } |
其实没错的,就是这个方法,jvm要查找到这个native方法其实很简单的,看方法名可能也猜到规则了,Java_pkgName_className_methodName,其中pkgName里的”.”替换成”_”,这样就能找到了,当然规则不仅仅只有这么一个,还有其他的,这里不细说了,有机会写篇文章详细介绍下其中细节
上面的方法里是调用JVM_GC(),实现如下
1 2 3 4 5 6 | JVM_ENTRY_NO_ENV(void, JVM_GC(void)) JVMWrapper("JVM_GC"); if (!DisableExplicitGC) { Universe::heap()->collect(GCCause::_java_lang_system_gc); } JVM_END |
看到这里我们已经解释其中一个疑惑了,就是DisableExplicitGC这个参数是在哪里生效的,起的什么作用,如果这个参数设置为true的话,那么将直接跳过下面的逻辑,我们通过-XX:+ DisableExplicitGC就是将这个属性设置为true,而这个属性默认情况下是true还是false呢
1 2 | product(bool, DisableExplicitGC, false, \ "Tells whether calling System.gc() does a full GC") |
ExplicitGCInvokesConcurrent参数
这里主要针对CMSGC下来做分析,所以我们上面看到调用了heap的collect方法,我们找到对应的逻辑
void GenCollectedHeap::collect(GCCause::Cause cause) { if (should_do_concurrent_full_gc(cause)) { #ifndef SERIALGC // mostly concurrent full collection collect_mostly_concurrent(cause); #else // SERIALGC ShouldNotReachHere(); #endif // SERIALGC } else { #ifdef ASSERT if (cause == GCCause::_scavenge_alot) { // minor collection only collect(cause, 0); } else { // Stop-the-world full collection collect(cause, n_gens() - 1); } #else // Stop-the-world full collection collect(cause, n_gens() - 1); #endif } } bool GenCollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) { return UseConcMarkSweepGC && ((cause == GCCause::_gc_locker && GCLockerInvokesConcurrent) || (cause == GCCause::_java_lang_system_gc && ExplicitGCInvokesConcurrent)); } |
collect里一开头就有个判断,如果should_do_concurrent_full_gc返回true,那会执行collect_mostly_concurrent做并行的回收,其中should_do_concurrent_full_gc中的逻辑是如果使用CMS GC,并且是system gc且ExplicitGCInvokesConcurrent==true,那就做并行full gc,当我们设置-XX:+ ExplicitGCInvokesConcurrent的时候,就意味着应该做并行Full GC了,不过要注意千万不要设置-XX:+DisableExplicitGC,不然走不到这个逻辑里来了。 三、实验步骤及结果: JVM参数参见:https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
public class TestGC { private static final int SIZE = 1024*1024; /** * VM参数: * -Xms20M -Xmx20M -Xmn10M -XX:+UseParallelGC -XX:+PrintGCDetails * * JVM初始分配的内存由-Xms指定,默认是物理内存的1/64 * JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4 * * -XX:+UseParallelGC * 代表新生代使用Parallel收集器,老年代使用串行收集器。 * * * Ergonomics就是负责自动的调解gc暂停时间和吞吐量之间的平衡 */ public static void testGC1(){ byte[] allocation1,allocation2,allocation3,allocation4; allocation1 = new byte[4*SIZE]; allocation2 = new byte[2*SIZE]; allocation3 = new byte[2*SIZE]; allocation4 = new byte[2*SIZE]; } /** * VM参数: * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution * -XX:+PrintTenuringDistribution参数作用: * JVM 在每次新生代GC时,打印出幸存区中对象的年龄分布。 * -XX:+UseSerialGC * 使用串行回收器进行回收,这个参数会使新生代和老年代都使用串行回收器,新生代使用复制算法, * 老年代使用标记-整理算法。Serial收集器是最基本、历史最悠久的收集器,它是一个单线程收集器。 * 一旦回收器开始运行时,整个系统都要停止。Client模式下默认开启,其他模式默认关闭 */ public static void testGC2(){ byte[] allocation1,allocation2,allocation3,allocation4; allocation1 = new byte[4 * SIZE]; allocation2 = new byte[4 * SIZE]; allocation3 = new byte[4 * SIZE]; allocation3 = null; allocation4 = new byte[4 * SIZE]; } /** * VM参数: * 6. -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=4 -XX:+PrintTenuringDistribution * -XX:+UseSerialGC * 使用串行回收器进行回收,这个参数会使新生代和老年代都使用串行回收器,新生代使用复制算法, * 老年代使用标记-整理算法。 * * -XX:+PrintTenuringDistribution参数作用: * JVM 在每次新生代GC时,打印出幸存区中对象的年龄分布。 */ public static void testGC3(){ byte[] allocation1,allocation2,allocation3,allocation4; allocation1 = new byte[SIZE / 4]; allocation2 = new byte[4 * SIZE]; allocation3 = new byte[4 * SIZE]; allocation4 = null; allocation4 = new byte[4 * SIZE]; System.gc(); } public static void main(String[] args) throws Exception { testGC1(); } } |
执行testGC1()方法,VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseParallelGC -XX:+PrintGCDetails 结果:
[GC (Allocation Failure) [PSYoungGen: 7995K->840K(9216K)] 7995K->6992K(19456K), 0.0044359 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6743K(10240K)] 6992K->6743K(19456K), [Metaspace: 3152K->3152K(1056768K)], 0.0051839 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 4420K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 53% used [0x00000000ff600000,0x00000000ffa51268,0x00000000ffe00000) from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) ParOldGen total 10240K, used 6743K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) object space 10240K, 65% used [0x00000000fec00000,0x00000000ff295c10,0x00000000ff600000) Metaspace used 3189K, capacity 4496K, committed 4864K, reserved 1056768K class space used 347K, capacity 388K, committed 512K, reserved 1048576K |
分析:PSYoungGen: 7995K->840K(9216K)红色是在进行GC前年轻代内存的使用情况,蓝色是GC后年轻代内存的使用情况,绿色是整个年轻代内存大小,后面的分析方法一样。从输出内容上可明显看出:Eden:From:To = 8:1:1,当发现一个大对象在Eden区+Survior1区中存不下的时候就需要分配担保机制把当前Eden区+Survior1区的所有对象都复制到老年代中去。 修改testGC1方法:如下 对对象分配过程: 执行testGC2()方法,VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution 结果:
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 614800 bytes, 614800 total : 5947K->600K(9216K), 0.0032761 secs] 5947K->4696K(19456K), 0.0033109 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 128 bytes, 128 total : 4780K->0K(9216K), 0.0025522 secs] 8876K->8789K(19456K), 0.0025747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew: 4151K->4151K(9216K), 0.0000128 secs][Tenured: 8788K->8790K(10240K), 0.0014736 secs] 12940K->8790K(19456K), [Metaspace: 3183K->3183K(1056768K)], 0.0015235 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4460K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 54% used [0x00000000fec00000, 0x00000000ff05b010, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 8790K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe95b38, 0x00000000ffe95c00, 0x0000000100000000) Metaspace used 3206K, capacity 4496K, committed 4864K, reserved 1056768K class space used 351K, capacity 388K, committed 512K, reserved 1048576K |
查看TestGC.clss字节码,用jclasslib插件打开: 以testGC3为例,ldc将常量压入栈中,newarray是创建指定类型的数组,astore_0是将栈顶引用类型值保存到局部变量1中,aconst_null是将null入栈。 字节码指令参考:https://www.cnblogs.com/longjee/p/8675771.html 四、实验体会: 通过本次实验,了解了JVM的内部结构,对垃圾回收算法也有了一定的了解,对GC日志有了一定的认识,能够进行一定的分析。 |