深入理解java虚拟机课程的截图-6-宋红康老师

来自B站视频:https://www.bilibili.com/video/BV1BJ41177cp?p=169

【怎么去进行垃圾回收呢?就是使用垃圾回收器进行垃圾回收的!】

【GC:Garbage Collection,垃圾回收。GC:Garbage Collector,垃圾回收器。】

JDK逐渐演进时,不同JDK版本的变化点在哪里查看呢?传送门http://openjdk.java.net/jeps/0

尚硅谷宋红康Java12&13新特性教程:https://www.bilibili.com/video/BV1jJ411M7kQ?p=1

这里的线程数是指:用于垃圾回收的线程数。上图中,橙色代表着垃圾回收线程。上图左边:垃圾回收线程只有一个,代表的是串行垃圾回收器。上图右边:垃圾回收线程有多个,代表的是并行垃圾回收器。具体的串行、并行、并发的概念详看第四章:https://blog.csdn.net/cmm0401/article/details/108908310

这里的工作模式是指:垃圾回收线程是否与用户线程并发执行的概念。如果是与用户线程并发执行,则是并发式垃圾回收器。如果不是与用户线程并发执行,则是独占式垃圾回收器对于并发式垃圾回收器,用户线程是与垃圾回收线程并发执行的,它们之间交替工作,可以尽可能的减少应用程序的停顿时间。对于独占式垃圾回收器,一旦垃圾回收线程运行了,就会暂时停止应用程序当中的所有用户线程,直到垃圾回收过程完全结束为止。】

这里的碎片处理方式是指:垃圾回收结束之后,有没有对内存做压缩整理。如果做了内存的压缩整理,则是压缩式垃圾回收器。如果没有做内存的压缩整理,则是非压缩式垃圾回收器对于压缩式垃圾回收器,再次给新的java对象分配内存空间的时候,使用的是指针碰撞的方式进行内存分配。对于非压缩式垃圾回收器,再次给新的java对象分配内存空间的时候,使用的是空闲列表的方式进行内存分配。】【根据垃圾回收器回收的内存空间来分:年轻代垃圾回收器、老年代垃圾回收器。】

【评价GC的性能指标,主要有三个:(1)吞吐量:用户线程运行时间占总运行时间的比例。其中,总运行时间=用户线程运行时间+垃圾回收线程运行时间。所以,我们追求的是用户线程运行时间越长越好,也即吞吐量越大越好。(2)用户线程被暂停的时间:所以,我们追求的是用户线程被暂停的时间越短越好。(3)内存占用:java堆区所占的内存大小。(4)垃圾回收频率:垃圾回收线程执行的频率,并不是说垃圾回收频率越低,每次进行垃圾回收时用户线程暂停的时间就短,反而是长的。

【吞吐量:是指CPU运行用户线程的时间占CPU总消耗时间的比例,所以,吞吐量越大越好】

上图1:从一段时间上来看,图1表示的是垃圾回收的频率比较低,但是,每一次垃圾回收所花费的时间比较多。图2:从一段时间上来看,图2表示的是垃圾回收的频率比较高,但是,每一次垃圾回收所花费的时间都比较少。所以,从整体上来看,图1的吞吐量是比图2的吞吐量高的。而我们更喜欢追求的:在最大吞吐量优先的前提下,降低延迟时间;或者说,在给定延迟时间的前提下,尽可能的提高系统的吞吐量。

目前,我们追求的目标是:在可控的暂停时间范围之内,尽可能大的提高吞吐量。其中,这个可控的暂停时间范围是指,用户可以接受的系统停顿时间。在这个停顿时间范围之内,我们尽可能的提高系统吞吐量。】

【实际上,不同的垃圾回收器是和JVM紧密相连的,不同的厂商,它们所生产出来的JVM的版本也是不一样的(比如Oracle/IEM/RedHat),不同版本的JVM所使用的垃圾回收器也是有区别的。下面,吧啦吧啦。。。】

垃圾收集器官方文档:https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

【7款经典的垃圾收集器与垃圾分代之间的关系。垃圾收集器的组合关系,已经更新到JDK14。】

JDK版本演进带来的变化,在这里查询http://openjdk.java.net/jeps/0】【没有一个放之四海皆准的垃圾回收器,不同的垃圾回收器,它们的应用场景不同,所以出现了多款垃圾回收器,以及它们之间的组合。熟记它们,以及它们之间的组合方式。

package com.atguigu.java;
import java.util.ArrayList;
/**
 * @author shkstart  [email protected]
 * @create 2020  0:10
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

【上面截图说明:在JDK8中,默认情况下,年轻代使用ParallelGC,同时 老年代会自动使用ParallelOldGC,它们俩搭配使用

【上面截图说明:在JDK9中,默认情况下,年轻代和老年代同时使用G1GC,G1既可以回收新生代,也可以回收老年代。

package com.atguigu.java;
import java.util.ArrayList;
/**
运行此程序时的JVM参数:
-XX:+PrintCommandLineFlags -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代自动使用Serial Old GC
 * @author shkstart  [email protected]
 * @create 2020  0:10
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

【注意:上面这张截图只是针对ParNewGC的一种搭配使用方式罢了。JDK14之后,ParNewGC的位置比较尴尬,因为它的搭配GC都被废弃或者移除了。】

package com.atguigu.java;
import java.util.ArrayList;
/**
运行此程序时的JVM参数:
-XX:+PrintCommandLineFlags -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代自动使用Serial Old GC
-XX:+PrintCommandLineFlags -XX:+UseParNewGC:标明新生代使用ParNew GC
-XX:+PrintCommandLineFlags -XX:+UseParallelGC:表明新生代使用Parallel GC,同时老年代使用 Parallel Old GC
-XX:+PrintCommandLineFlags -XX:+UseParallelOldGC:表明老年代使用 Parallel Old GC,同时新生代使用Parallel GC
【说明:二者可以相互激活】
-XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC,同时年轻代会触发对ParNew的使用
 * @author shkstart  [email protected]
 * @create 2020  0:10
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

【高吞吐量:是指它可以比较高效的利用CPU资源,尽可能快的完成应用程序的计算任务,主要适合在后台运算而不需要太多交互的任务。因此,高吞吐量常在后台服务器环境中被提及使用,比如,执行批量处理、订单处理、科学计算的应用程序。】

在JDK9,JVM默认使用G1作为新生代和老年代的垃圾回收器,参数是:-XX:UseG1GC。在JDK8,JVM默认使用ParallelGC回收新生代,ParallelOldGC回收老年代,参数是:-XX:UseParallelGC,会自动触发老年代使用ParallelOldGC垃圾回收器。】

/**
运行此程序时的JVM参数:
【情况1】
-XX:+PrintCommandLineFlags -XX:+UseSerialGC:表明新生代使用SerialGC,同时老年代会自动使用SerialOldGC

【情况2】
-XX:+PrintCommandLineFlags -XX:+UseParallelGC:表明新生代使用ParallelGC,同时老年代会自动使用ParallelOldGC
-XX:+PrintCommandLineFlags -XX:+UseParallelOldGC:表明老年代使用ParallelOldGC,同时新生代会自动使用ParallelGC
【说明:UseParallelGC和UseParallelOldGC,二者可以相互激活。】
 */

【一种JVM内存空间的分配参数列表,可以记住常用的:-Xmx5120m -Xms5120m -XX:NewSize=2048m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=9 -XX:MaxPermSize=384m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m             -XX:+UseConcMarkSweepGC -XX:+UseAdaptiveSizePolicy。

【参数解释:堆空间最大值是5G,堆空间最小值也是5G,新生代是2G,由此可以计算出老年代是3G,在新生代中,Eden区:S1区:S0区=8:1:1,也即Eden区是1.6G,S1区是0.2G,S0区也是0.2G,年轻代中的java对象晋升到老年代的年龄值阈值是9次(在JVM中用4个bit存储, 放在对象头中, 所以其最大值也是15。ParallelGC中默认值为15, CMS中默认值为6, G1中默认值为15)

默认情况下,JVM内存空间分配的比例是:新生代:老年代=1:2,使用参数-XX:NewRatio=2设置。新生代中,Eden区:S1区:S0区=8:1:1,使用参数-XX:SurvivorRatio=8设置。

 【JVM参数列表:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

【 Concurrent Mode Failure 失败的场景:要理解与谨记!在上图中的最后一段有清晰的描述。注意:这个阈值是可以设置的,见下面的JVM参数:-XX:CMSInitiatingOccupanyFraction。】

为什么呢?答:CMS第四阶段“并发清除”的时候,用户线程还在继续执行,它还在正常执行的前提是它所需要的资源不受影响,试想一下,如果你突然改变了用户线程所使用的一些对象的地址,那么它还怎么继续执行呢,所以为了保证用户线程能够继续正常执行,垃圾回收算法采用的是标记-清除算法,而不是标记-整理算法。】

【下图是CMS垃圾回收器的优点和缺点,需要理解谨记!】

(1)什么是浮动垃圾:在CMS第二阶段“并发标记”的时候,用户线程和垃圾回收线程是并发执行的,那么,在并发标记期间,如果用户线程产生了新的垃圾对象,CMS将无法对这些新的垃圾对象进行标记,最终会导致本次GC过程不会回收这些新产生的垃圾对象,从而只能在下一次执行GC的时候去释放这些新垃圾对象所占用的内存空间。这里的新垃圾对象,就是浮动垃圾。

(2)为什么在JKD14的时候移除CMS垃圾回收器了?CMS垃圾回收器的缺点很致命,比如 CMS在老年代使用的垃圾回收算法是并发-清除算法,而并发-清除算法执行完成之后,会在老年代产生大量的内存碎片,在无法分配大对象的时候不得不提前触发一次fullGC。再比如 CMS垃圾回收器对CPU资源非常敏感,在并发标记阶段与并发清除阶段,CMS虽然不会导致用户线程STW,但是会因为占用了一部分线程而导致应用程序整体变慢,整体吞吐量降低。再比如 CMS垃圾回收器无法处理浮动垃圾,在CMS第二阶段“并发标记”的时候,用户线程和垃圾回收线程是并发执行的,那么,在并发标记期间,如果用户线程产生了新的垃圾对象,CMS将无法对这些新的垃圾对象进行标记,最终会导致本次GC过程不会回收这些新产生的垃圾对象,从而只能在下一次执行GC的时候去释放这些新垃圾对象所占用的内存空间,其中,这里新产生的新垃圾对象就是浮动垃圾。如果在CMS第四阶段“并发清除”的时候,JVM无法为仍在继续执行的用户线程分配足够大小的内存空间的话,那么JVM就会出现“Concurrent Mode Failure”失败,而这个失败会导致进行另外一次的fullGC,在另外一次fullGC的时候老年代会使用SerialOldGC,这是一个“串行的标记-整理”垃圾回收器,性能相对来说没那么强,在业务高峰的时候,STW的时间可能会比较长,用户不一定能接受。基于上述原因,所以在JDK14的时候CMS被移除了,并且,又引入了一款新的垃圾回收器G1。】

【-XX:+UseConcMarkSweepGC,这个参数打开之后,会自动触发新生代垃圾回收器 -XX:+UseParNewGC,同时,老年代的垃圾回收会自动使用SerialOldGC作为“Concurrent Mode Failure”的后备方案。】

/**
运行此程序时的JVM参数:
【情况1】
-XX:+PrintCommandLineFlags -XX:+UseSerialGC:表明新生代使用SerialGC,同时老年代会自动使用SerialOldGC

【情况2】
-XX:+PrintCommandLineFlags -XX:+UseParallelGC:表明新生代使用ParallelGC,同时老年代会自动使用ParallelOldGC
-XX:+PrintCommandLineFlags -XX:+UseParallelOldGC:表明老年代使用ParallelOldGC,同时新生代会自动使用ParallelGC
【说明:UseParallelGC和UseParallelOldGC,二者可以相互激活。】

【情况3】
-XX:+PrintCommandLineFlags -XX:+UseParNewGC:标明新生代使用ParNew GC
-XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC,同时年轻代会触发对ParNew的使用
 */

【G1:区域化分代式的垃圾回收器。分代式:新生代、老年代啊等。区域式:分配对象的新的思路,Region。 】

【GC:我们关注的比较重要的点是“高吞吐量”与“低延迟”。吞吐量:ParallelGC-ParallelOldGC,它俩也是JDK8的默认垃圾回收器。低延迟:ParNew-CMS。】

【官方给G1垃圾回收器设定的目标是:在暂停时间(延迟时间)可控的情况下,尽可能高的提高系统吞吐量,所以才担当起全功能收集器的重任与期望。下面这个截图的描述要理解与记住!】

【CMS垃圾回收器,在JDK9的时候被标记为废弃,在JDK14的时候被彻底移除!JDK9及之后,G1成为默认的垃圾回收器。

回收某一个Region区域的价值 是指什么:(1)回收这个Region可以获得的空间大小(2)回收这个Region所需要的时间的经验值。另外:G1垃圾回收器,会在后台维护一个Region区域的价值优先列表。实时:是指100%的概率都可以在N毫秒内完成垃圾回收。软实时,是指比如在90%的概率下都可以在N毫秒内完成垃圾回收,不是100%的概率。】

【CMS垃圾回收器,在小内存应用上比较优秀。G1垃圾回收器,在大内存应用程序上比较优秀。平衡点一般是在6-8GB。】

注意:每个Region区域的大小是可以设置的,大小是2的整数次幂,2的0次幂~2的5次幂,范围是1MB-32MB,目标是根据最小的java堆大小划分出2048个Region区域,默认情况下,Region区域大小是堆内存的1/2000。

JVM参数列表:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

【G1垃圾回收器使用时,参数设置:-XX:+UseG1GC -XX:Xmx5120m -XX:Xms5120 -XX:G1HeapRegionSize=16m -XX:MaxGCPauseMillis=200】

【(1)因为G1使用的是复制算法和标记-整理算法,所以内存区域都是规整的,所以JVM给新对象分配内存的时候会使用指针碰撞的方式,而不是空闲列表的方式。(2)在每一个Region区域内,可以划分出几个TLAB,防止因为多个线程同时抢占相同地址的区域而加锁 使得分配内存的效率变低。】

【并行:有多个垃圾回收线程。独占式:用户线程短暂的停止,仅垃圾回收线程工作,即 : Stop The World。】

跨代引用问题:一个java对象存储在新生代区,它可以被其他新生代中的对象引用,也可以被老年代中的对象引用,但是,我们现在是在YGC,只进行新生代的垃圾回收,那么是不是也必须把老年代也扫描一遍呢?如果是的话,那YGC的效率就太低了。基于这个问题,提出一个概念,记忆集,Rset。【Remembered Set:Rset,记忆集、写屏障、卡表】

【下图中的G1回收过程二:并发标记过程,主要针对是老年代!】

【总体而言,G1垃圾回收器的回收过程还是有点混沌,需要再次学习下!】

(阶段一)上图中,凡是连线的,不管是虚线还是实线,在JDK7中都可以去使用。(阶段二) 在JDK8中,SerialGC+CMSGC-SerialOldGC 这一条红色线被废弃了,但仍然可以使用,直到JDK9时才被移除;同时,ParNewGC+SerialOldGC 这一条红色线也被废弃了,但仍然可以使用,直到JDK9时才被移除。(阶段三)CMSGC在JDK9时被废弃了,但仍然可以使用,直到JDK14时才被移除。ParallelGC-SerialOldGC 这一条绿色线在JDK14时被废弃了,但没有被移除,仍然可以使用,相信在未来的版本中该条绿色线也会被移除掉。(阶段四)把上图中的虚线全部擦除干净后,发现还剩下三种GC组合:Serial+SerialOldGC;ParallelGC+ParallelOldGC。G1GC。

【参数分为两部分:一部分是 JVM内存分配的参数,一部分是 GC的参数。】

package com.atguigu.java;
import java.util.ArrayList;
/**
 * -Xms60m -Xmx60m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -Xloggc:./logs/gc.log
 */
public class GCLogTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 500; i++) {
            byte[] arr = new byte[1024 * 100];//100KB
            list.add(arr);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

【下面这些GC参数需要理解并且掌握!!!】

package com.atguigu.java;
/** 在jdk7和jdk8中分别执行:
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
此时:Eden=8M,S0=1M,S1=1M,Old=20M.
注意:-Xmn10M代表新生代是10MB.
 */
public class GCLogTest1 {
    private static final int _1MB = 1024 * 1024;
    public static void testAllocation() {
        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];
    }
    public static void main(String[] agrs) {
        testAllocation();
    }
}

JDK7场景下,def new generation:默认是垃圾回收器是SerialGC+SerialOldGC。测试用例整个过程是:分配allocation1、allocation2、allocation3对象分别进入新生代的Eden区,每个对象占用2MB,总共占用6MB,此时Eden区还剩下2MB。再分配4MB的allocation4对象时,发现Eden区不够,此时进行YGC,发现S0与S1区域都不够存放,于是会把Eden区域中的allocation1、allocation2、allocation3都移动到Old区域,然后再把allocation4分配进已经空闲出来的Eden区域。】

JDK8场景下,def new generation:默认是垃圾回收器是ParallelGC+ParallelOldGC。测试用例整个过程是:分配allocation1、allocation2、allocation3对象分别进入Eden区域,每个占用2MB,总共占用6MB,此时Eden区域还剩下2MB。接下来再分配大对象4MB的allocation4时,发现Eden区域存放不了,于是4MB的大对象allocation4直接进入Old区域了。】 

GCeasy在线分析工具:http://gceasy.io/

package com.atguigu.java;
import java.util.ArrayList;
/**
  GCeasy在线分析工具使用测试用例:http://gceasy.io/
 * -Xms60m -Xmx60m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -Xloggc:./logs/gc.log
 */
public class GCLogTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 500; i++) {
            byte[] arr = new byte[1024 * 100];//100KB
            list.add(arr);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

JEP官网:http://openjdk.java.net/jeps/0

各种GC(包含ZGC)的各种结束,来自官网:https://docs.oracle.com/en/java/javase/index.htmlJDK12中的GC介绍:http://docs.oracle.com/en/java/javase/12/gctuningJDK15中的GC介绍:https://docs.oracle.com/en/java/javase/15/gctuning/index.htmlJDK15介绍:https://docs.oracle.com/en/java/javase/15/index.html

【对应着博客中的1-6】【深入理解java虚拟机课程的截图-1-宋红康老师,深入理解java虚拟机课程的截图-6-宋红康老师】

猜你喜欢

转载自blog.csdn.net/cmm0401/article/details/108911114