初识JVM

关于JVM里的知识很复杂,本文主要介绍JVM中常见面试题来讲解

  • JVM内存区域划分
  • JVM类加载机制
  • JVM垃圾回收机制

1. JVM简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统

常见的虚拟机:JVM、VMwave、Virtual Box

注: JVM 是一台被定制过的现实当中不存在的计算机

目前Java官方默认使用的JVM是HotSpot, oracle官方文档Java HotSpot VM Options (oracle.com)

2. JVM的执行流程

Java程序在执行之前会先把java文件(.java文件)转换为字节码文件(.class文件),JVM首先需要把字节码文件通过一定方式(类加载器 ClassLoader)把文件加载到运行时数据区(Runtime Data Area),而字节码是属于JVM的字节码规范,并不能交给底层操作系统去执行,因此需要特定的命令解析器(执行引擎 Execution Engine ),

将字节码翻译成底层操作系统指令再交给CPU去执行,而这个过程需要调用其他语言的接口(本地库接口 Native Interface)来实现整个程序的功能.

image-20221029132001694

总结: JVM主要分为以下4个部分

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

3. JVM运行时内存区

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称 JMM)完全不同 ,其由下面5个部分组成

image-20221029132452430

3.1 堆

堆的作用: 存放程序中创建的所有对象(通过new 关键字创建的)

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。新生代还有 3 个区域:一个 Eden+ 两个 Survivor(S0/S1)

image-20221029133428562

垃圾回收的时候会将 Eden中存活的对象放到一个未使用的 Survivor 中,并把当前的 Eden和正在使用的 Survivor 清除掉

3.2 Java虚拟机栈

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。

Java 虚拟机栈中包含了以下 4 部分:

image-20221029133749714

  1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量
  2. 操作栈:每个方法会生成一个先进后出的操作栈
  3. 动态链接:指向运行时常量池的方法引用
  4. 方法返回地址:PC 寄存器的地址

问题: 什么是线程私有?

由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存 ,例如Java虚拟机栈,本地方法栈,程序计数器都是线程私有的

3.3 本地方法栈

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的(java代码使用的栈),而本地方法栈是给本地方法使用的(给JVM内部C++代码使用的栈)

3.4 程序计数器

程序计数器的作用:用来记录当前线程执行到那个指令,并存储下一个需要执行的指令

程序计数器是占用内存较小的,对于java代码是加载到内存中,而在内存中的每个指令都有对应的地址,程序计数器就是用来记录当前执行到那条指令.

  • 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
  • 如果正在执行的是一个Native方法,这个计数器值为空

3.5 方法区

方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

我们写的.java文件编译成.class文件后被JVN加载到内存中构造出类对象,

类对象: 包含类的名称,继承关系,接口关系,属性信息,方法信息等等

在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)

PS:永久代(PermGen)和元空间(Metaspace)是 HotSpot 中对《Java虚拟机规范》中方法区的实现,它们三者之间的关系就好比,对于一辆汽车来说它定义了一个部分叫做“动能提供装置”,但对于不同的汽车有不同的实现技术,比如对于燃油车来说,它的“动能提供装置”的实现技术就是汽油发动机(简称发动机),而对于电动汽车来说,它的“动能提供装置”的实现就是电动发动机(简称电机),发动机和电机就相当于永久代和元空间一样,它是对于“制动器”也就是方法区定义的实现

运行时常量池:

运行时常量池是方法区的一部分,存放字面量与符号引用

字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

小结:

方法区和栈区是线程共享的,而程序计数器和栈是每个线程都有一份

image-20221029140549819

3.5 内存中的常见异常

堆异常

在IDEA中设置堆的大小:

image-20221029214237877

image-20221029214259516

参数解释: -Xms20m 最小启动内存 -Xmx20m 最大运行内存

设置好堆的相关参数后,我们通过测试代码观察堆异常

public class Demo1 {
    static class OOMObject{

    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

image-20221029214158687

通过异常信息可以发现这是一个关于内存溢出的异常,具体是"Java heap space"是堆的内存溢出

还要一种情况是内存泄漏(泄漏对象无法被GC,即被垃圾回收器回收)

栈异常

在Java1.8后HotSpot虚拟机就把虚拟机栈和本地方法栈合二为一了.

关于虚拟机栈会产生的两种异常 :

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
  • 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常

示例: 观察StackOverFlow异常(单线程环境下)

public class Demo1 {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        try {
            demo1.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack Length" + demo1.stackLength);
            throw e;
        }
    }
}

image-20221029220000828

这是因为方法调用的深度太大.

示例:观察多线程下的内存溢出异常

public class Demo1 {
    public void Do() {
        while (true) {

        }
    }
    public void test() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    Do();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        demo1.test();
    }
}

显示结果: 死循环没有任何报错

如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程 .

4. JVM类加载机制

类加载: Java进程启动(JVM启动)把生成的字节码文件从硬盘中加载到内存,并构造出类对象的过程就是类加载,在官方文档中把类的生命周期分为5个部分,而类加载过程只包括前三个 加载 -> 连接 -> 初始化

image-20221029221244157

1) 加载

首先要注意加载(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段

在加载 Loading 阶段,Java虚拟机需要完成以下三件事情

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

总结就是找到.class文件.打开文件,读文件,创建空的类对象

2) 连接

验证中分为三个步骤

验证

检查.class文件格式是否符合规范.

官方文档中对一个.class文件的规范https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

image-20221029230834213

​ 解释说明:

  • u4,u2分别代表4字节无符号整数,2字节无符号整数
  • cp_info,field_info…代表一些结构体
  • magic:固定数值,标识当前文件是java的,class文件
  • minor_version,major_version…代表当前.class是针对java的那个版本
准备

为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。

例如:

static int value = 123;

它初始化的值是0,而非123

解析

Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程

解释:

编译过程中编译器,能够发现当前代码里有哪些字符串常量.编译过程中,就会使用一些特殊的符号来分别表示这些字符串常量.当真正进行类加载的时候,就可以把字符串常量真正的放到内存中.把对应的内存地址替换刚才的占位符号.

3) 初始化

Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

image-20221029232546051

解释:

  • BootstrapClassLoader(启动类加载器),负责加载JDK 中 lib 目录中 Java 的核心类库 ,例如String,ArrayList等等
  • ExtensionClassLoader(拓展类加载器),负责加载一些拓展类
  • ApplicationClassLoader(应用程序类加载器),负责加载应用程序里自己写的类

双亲委派模型的优点

  1. **避免重复加载类:**比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了
  2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了.

5. 垃圾回收

对于一个Java进程在运行时时由操作系统为它分配的内存中的程序计数器,栈.方法区的生命周期与其对应的线程有关,而堆中存储的对象不会随线程的结束而被回收,所以java官方为我们提供了GC(garbage collection 垃圾回收机制)帮助清除堆中的’死亡对象’.而对于C/C++中是通过malloc/new从堆上申请空间,需要使用free/delete手动释放内存.

那如何判断对象已经’死亡’?垃圾回收机制是如何运行的?,这些是下面需要讲解的

死亡对象的判断算法

注: 首先要明确内存回收是以对象为单位,而不是以字节为单位.

1) 引用计数算法

描述: 给对象增加一个引用计数器,记录有多少个引用指向它,当引用计数器值为0时就会被回收

由于引用计数法的实现简单,效率高,比如Python采用的就是引用计数法,但是在主流的JVM中没有采用引用计数器来判断对象是否死亡,最主要的原因是引用计数法无法解决对象的循环引用问题

缺陷:

  1. 多线程中修改同一个引用计数需要考虑线程安全
  2. 根据对象大小所承担空间开销不同,如果一个引用计数器大小为4字节,对于大对象(1kb)加上引用计数器后其内存占用就变成1028相比之前负担只是增加一点点,但是对于小对象(2字节),从 2字节- >6字节 ,负担就大大增加.
  3. 循环引用(致命缺陷)
static class Test{
    Test ref = null;
}

public static void main(String[] args) {
    Test a = new Test();
    Test b = new Test();
    a.ref = b;
    b.ref = a;

    a = null;
    b = null;

}

解释:

首先在堆上创建两个对象,用a和b分别指向这两个对象,此时a和b的引用计数器都是1,随后a.ref = b; b.ref = a;使a对象中的ref指向b对象,此时ref对象的引用计数器为1,b对象同理.随后把a,b对象指向空对象,它们的引用计数器都变为0,但是其内部的ref对象的引用计数器不为0,导致对象无法被回收,造成内存泄漏,

image-20221030141908082

2) 可达性分析算法

Java中采用的是可达性分析来判断对象是否死亡.

描述: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的 .

image-20221030142748323

在开始GC Roots和object 5是可达的,如果object 5和GC Roots的联系断了,那么就认为object 5 ~ object 7是死亡对象,因为它们都无法可达GC Roots.

在Java语言中,可作为GC Roots的对象包含下面几种

  1. 局部变量表的引用,即栈中的局部变量(对于一个Java进程,内部有很多线程,每个线程都有一个栈,每个栈中有很多栈帧,每个栈帧都有属于自己的局部变量表,对于所有线程中栈帧中所有的局部变量表就会被视为 GC Roots)
  2. 方法区中类静态属性对象引用的对象
  3. 方法区中常量引用的对象

了解: Java中对引用概念的扩充

在 JDK1.2 时,Java 对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减.

  1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
  2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
  4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

垃圾回收算法

当我们把垃圾对象标记出来了,现在就可以用怎样的方法来回收垃圾

1) 标记-清除算法

思想: 算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

image-20221030150254379

缺陷:

  1. 效率问题: 标记和清除这两个过程的效率不高
  2. 空间问题: 标记清楚后会产生大量不连续的内存碎片,如果后续需要创建大对象,因为其分配的空间是连续的,那么对于这些内存碎片就无法被使用,这就导致有空间却不能使用的尴尬场景.

2) 复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可.

image-20221030151008098

缺陷:

解决了内存碎片问题,但是把空间一分为二导致空间利用率降低

如果对象存活率高就会一直复制导致效率较低

3) 标记-整理算法

复制算法的空间利用率低,于是标记-整理算法沿用标记-清除算法的标记过程,但是对于清除过程变为整理(类似于顺序表删除元素),让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存 .

image-20221030152217683

4) 分代算法

在实际开发场景中可以使用不同的算法达到高效率,所有分代算法就是综合了这些算法.该算法给对象引入了一个概念"年龄".年龄指的是对象活过GC的轮次,根据年龄大小把对象分为新时代和老年代.

在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法

该算法把内存中的新生代内存分为一块较大的Eden(伊甸园)空间和两块较小的Survivor Spaces(幸存者空间),以及Old(老年代)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保

image-20221030153158749

HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To =8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。

HotSpot实现的复制算法流程如下 :

  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

面试题:请问了解Minor GC和Full GC么,这两种GC有什么不一样吗 ?

  1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上

垃圾收集器

收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间

以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:

image-20221030155029795

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确三个概念

  • 并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
  • 并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上
  • 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值
    • 吞 吐 量 = 运 行 用 户 代 码 时 间 运 行 用 户 代 码 时 间 / 垃 圾 收 集 时 间

为什么有这么多的垃圾回收器

自从有了 Java 语言就有了垃圾收集器,这么多垃圾收集器其实是历史发展的产物。最早的垃圾收集器为 Serial,也就是串行执行的垃圾收集器,Serial Old 为串行的老年代收集器,而随着时间的发展,为了提升更高的性能,于是有了 Serial 多线程版的垃圾收集器 ParNew。后来人们想要更高吞吐量 的垃圾收集器,吞吐量是指单位时间内成功回收垃圾的数量,于是就有了吞吐量优先的垃圾收集器 ParallelScavenge(吞吐量优先的新生代垃圾收集器)和 Parallel Old(吞吐量优先的老年代垃圾收集器)。随着技术的发展后来又有了 CMS(Concurrent Mark Sweep)垃圾收集器,CMS 可以兼顾吞吐量和以获取最短回收停顿时间为目标的收集器,在 JDK 1.8(包含)之前 BS 系统的主流垃圾收集器,而在 JDK 1.8 之后,出现了第一个既不完全属于新生代也不完全属于老年代的垃圾收集器 G1(Garbage
First),G1 提供了基本不需要停止程序就可以收集垃圾的技术,下面我们来看每种垃圾收集器具体的介绍。

1) Serial收集器(新生代收集器,串行GC)

Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。

  • 特性:

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)

  • 应用场景:

Serial收集器是虚拟机运行在Client模式下的默认新生代收集器

  • 优势

简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚拟机运行在Client模式下的默认新生代收集器

image-20221030160845827

2) ParNew收集器(新生代收集器,并行GC)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码

  • 特性

Serial收集器的多线程版本

  • 应用场景:

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器

作为Server的首选收集器之中有一个与性能无关的很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器ParallelScavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个

  • 对比分析

与Serial收集器对比:
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器

然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的

image-20221030161135495

3) Parallel Scavenge收集器(新生代收集器,并行GC)
  • 特性

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量 :

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了

  • 应用场景:

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

  • 对比分析
  • Parallel Scavenge收集器 VS CMS等收集器:
    Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
  • Parallel Scavenge收集器 VS ParNew收集器:
    Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略
  • GC自适应的调节策略

Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy 。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

4) Serial Old收集器(老年代收集器,串行GC)
  • 特性:

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法

  • 应用场景:
  • Client模式

    Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用

  • Server模式
    如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

image-20221030161747011

5) Parallel Old收集器(老年代收集器,并行GC)
  • 特性

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

  • 应用场景

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高
级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合

image-20221030162100349

6) CMS收集器(老年代收集器,并发GC)(重点)
  • 特性

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤

  • 初始标记(CMS initial mark)
    初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark)
    并发标记阶段就是进行GC Roots Tracing的过程。
  • 重新标记(CMS remark)
    重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)
    并发清除阶段会清除对象

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的

  • 优点

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

  • 缺点
  • CMS收集器对CPU资源非常敏感
    其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大
  • CMS收集器无法处理浮动垃圾
    CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次FullGC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了
  • CMS收集器会产生大量空间碎片
    CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

image-20221030162827877

7) G1收集器(唯一一款全区域的垃圾回收器) (重点)

G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。

image-20221030163001623

一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象

  • 年轻代垃圾收集

在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域

image-20221030163124605

  • 老年代垃收集

对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同

  1. 初始标记(Initial Mark)阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了

  2. 并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用

  3. 最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象

  4. 筛选回收(Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示

    image-20221030163420747

G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。 如果你的应用追求低停顿,G1可以作为选择;如果你的应用追求吞吐量,G1并不带来特别明显的好处

总结: 一个对象的一生

一个对象的一生:我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了 Survivor区的 “From” 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区(S1 区),居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每次GC加一岁)然后被回收了

image-20221030163537212

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zzt.opkk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值