前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM 知识点整理

JVM 知识点整理

原创
作者头像
深雾
修改2021-07-27 14:31:42
3660
修改2021-07-27 14:31:42
举报
文章被收录于专栏:工具类工具类

工作,学习的重要的知识点记录下来

# JVM

JVM虚拟机处理器可以执行java的字节码程序。java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码在特定平台运行。

类装载器ClassLoader:用来装载.class文件

执行引擎:执行字节码,或者执行本地方法

运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈

# 内存结构

![image.png](http://liujun11.cn/upload/2020/09/image-cf001a105bec4e2e80d81019302f7ca0.png)

JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成。

## 堆内存

堆是内存中最大的一块,在虚拟机启动时创建,存放对象实例,被所有线程共享,是垃圾收集器管理的主要区域。很多时候也被称做“GC堆”,还可以细分为:新生代和老年代。

## 方法区

线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,有时被称为持久代。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

## 程序计数器(寄存器)

较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都有独立的程序计数器,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空

## JVM栈

Java虚拟机栈也是线程私有的,生命周期与线程相同。方法执行的内存模型:方法被执行时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。方法被调用直至执行完成,对应栈帧在虚拟机栈中从入栈到出栈的过程。

### 本地方法栈

与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈则是为虚拟机使用到的Native方法服务。

# 生命周期

1、JVM实例对应了一个独立运行的java程序

启动Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点

main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

2、每个Java程序都运行于它自己的Java虚拟机实例中。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出,当程序关闭退出,这个虚拟机实例也就随之消亡。

# 类加载过程

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

## 类加载方式

1、从本地系统中直接加载

2、通过网络下载.class文件

3、从zip,jar等归档文件中加载.class文件

4、从专有数据库中提取.class文件

5、将Java源文件动态编译为.class文件

### 类的生命周期

加载——验证——准备——解析——初始化五个阶段

顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)

### 加载

查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。

### 验证

确保被加载的类的正确性

验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

1、文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

2、元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。

3、字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

4、符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

### 准备

为类的静态变量分配内存,并将其初始化为默认值

备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

1、这时候进行内存分配的仅包括类变量(static静态),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为: publicstaticintvalue=3;

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的 publicstatic指令是在程序编译后,存放于类构造器 <clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

**初始化赋值**

对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

数组初始化时没有对数组中的各元素赋值,元素将根据对应的数据类型赋予默认的零值。

### 解析

把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

### 初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

1、声明类变量是指定初始值

2、使用静态代码块为类变量指定初始值

JVM初始化步骤:

1、假如这个类还没有被加载和连接,则程序先加载并连接该类

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

1、创建类的实例,也就是new的方式

2、访问某个类或接口的静态变量,或者对该静态变量赋值

3、调用类的静态方法

4、反射(如 Class.forName(“com.shengsiyuan.Test”))

5、初始化某个类的子类,则其父类也会被初始化

6、Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类

### 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

执行了 System.exit()方法

程序正常执行结束

程序在执行过程中遇到了异常或错误而异常终止

由于操作系统出现错误而导致Java虚拟机进程终止

## 类加载器

BootstrapLoader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式

![image.png](http://liujun11.cn/upload/2020/09/image-0391397b266540f39986931060959510.png)

Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

Java开发人员的角度来看,类加载器可以大致划分为以下三类:

1、启动类加载器: BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

2、扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

3、应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。

2、动态地创建符合用户特定需要的定制化构建类。

3、从特定的场所取得java class,例如数据库中和网络中。

### 类加载机制

1、全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

2、父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

3、缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

## 类的加载

### 类加载方式

1、命令行启动应用时候由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别:

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

### 双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

1、当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。

3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。

ClassLoader源码分析:

```java

public Class<?> loadClass(String name) throws ClassNotFoundException {

return loadClass(name, false);

}

// -----??-----

protected Class<?> loadClass(String name, boolean resolve)

throws ClassNotFoundException

{

// First, check if the class has already been loaded

Class<?> c = findLoadedClass(name);

if (c == null) {

try {

if (parent != null) {

c = parent.loadClass(name, false);

} else {

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// ClassNotFoundException thrown if class not found

// from the non-null parent class loade

}

if (c == null) {

// If still not found, then invoke findClass in orde

// to find the class.

c = findClass(name);

}

}

return c;

}

```

双亲委派模型意义:

1、系统类防止内存中出现多份同样的字节码

2、保证Java程序安全稳定运行

## 自定义类加载器

通常情况下是直接使用系统类加载器。但有时也需要自定义类加载器。比如应用是通过网络来传输Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

```java

public class MyClassLoader extends ClassLoader {

/**

* @param args

* @throws ClassNotFoundException

* @throws SecurityException

* @throws NoSuchMethodException

* @throws InvocationTargetException

* @throws IllegalArgumentException

* @throws IllegalAccessException

*/

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {

// TODO Auto-generated method stub

if (args.length == 0) {

System.out.println("没有类啊");

}

// 取出第一个参数,就是需要运行的类

String procressClass = args[0];

// 剩余参数为运行目标类的参数,将这些参数复制到一个新数组中

String[] procress = new String[args.length - 1];

System.arraycopy(args, 1, procress, 0, procress.length);

MyClassLoader myClassLoader = new MyClassLoader();

Class<?> class1 = myClassLoader.loadClass(procressClass);

Method main = class1.getMethod("main", (new

String[0]).getClass());

Object argsArray[] = { procress };

main.invoke(null, argsArray);

}

/**

* @TODO 读取文件内容

*/

public byte[] getBytes(String fileName) {

File file = new File(fileName);

long len = file.length();

byte[] raw = new byte[(int) len];

try {

FileInputStream fileInputStream =

new FileInputStream(file);

try {

int r = fileInputStream.read(raw);

fileInputStream.close();

if (r != len)

throw new IOException("fail to read

the file...");

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

return raw;

} catch (FileNotFoundException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

return null;

}

/**

* @TODO 编译java文件

*/

public boolean complie(String javaFile) {

System.out.println("正在编译...");

Process process = null;

try {

process = Runtime.getRuntime().exec("javac " + javaFile);

try {

process.waitFor();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

int result = process.exitValue();

return result == 0;

}

/**

* @TODO 关键,重写findClass方法

*/

@Override

protected Class<?> findClass(String arg0) throws ClassNotFoundException {

// TODO Auto-generated method stub

Class<?> class1 = null;

String filePath = arg0.replaceAll(".", "/");

String className = filePath + ".class";

String javaName = filePath + ".java";

File javaFile = new File(javaName);

File classFile = new File(className);

if (javaFile.exists()

&& (!classFile.exists() || javaFile.lastModified() > classFile .lastModified())) {

if (!complie(javaName) || !classFile.exists()) {

throw new ClassNotFoundException(javaName + " Class找不到");

}

}

if (classFile.exists()) {

byte[] raw = getBytes(className);

class1 = defineClass(arg0, raw, 0, raw.length);

}

if (class1 == null) {

throw new ClassNotFoundException(javaName + " 加载失败");

}

return class1;

}

}

```

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即 com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。

2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把 com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader加载,而不会通过我们自定义类加载器来加载。

# GC算法 垃圾收集器

垃圾收集Garbage Collection通常被称为“GC”,jvm中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此内存垃圾回收主要集中于java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

## 对象存活判断

**引用计数**:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

**可达性分析**:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

GC Roots包括:

1、虚拟机栈中引用的对象。

2、方法区中类静态属性实体引用的对象。

3、方法区中常量引用的对象。

4、本地方法栈中JNI引用的对象。

## 垃圾收集算法

### 标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。是最基础的收集算法,后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外个空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后运行过程中需要分配较大对象时无法找到足够的连续内存提前触发另一次垃圾收集动作。

### 复制算法

它将可用内存按容量划分为相等的两块,只使用其中的一块。当这块的内存用完,就将还存活着的对象复制到另一块,再把已使用过的内存空间清理掉。

每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低,一般在新生代使用。

### 标记-整理算法

根据老年代的特点,有人提出了另外一种“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

### 分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

## 垃圾收集器

### Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)

参数控制: -XX:+UseSerialGC 串行收集器

![image.png](http://liujun11.cn/upload/2020/09/image-2918eb2d4cad4c428c6662b081b74cc5.png)

ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

参数控制:

-XX:+UseParNewGC ParNew收集器

-XX:ParallelGCThreads 限制线程数量

### Parallel收集器

类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩

参数控制:

-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

### CMS收集器

以最短回收停顿时间为目标的收集器。大部分Java应用都集中在互联网站或B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

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

1、初始标记(CMS initial mark)

2、并发标记(CMS concurrent mark)

3、重新标记(CMS remark)

4、并发清除(CMS concurrent sweep)

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

整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

优点: 并发收集、低停顿

缺点: 产生大量空间碎片、并发阶段会降低吞吐量

参数控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行碎片整理;整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行碎片整理

-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

![image.png](http://liujun11.cn/upload/2020/09/image-b5abe61f17db4507b4722d2bdd6cf9f1.png)

### G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

1、空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

2、可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征。

上面的垃圾收集器范围都是整个新生代或者老年代,G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

![image.png](http://liujun11.cn/upload/2020/09/image-a334c4f4932b467e8eca6cc14fbc41e1.png)

跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

收集步骤:

1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

![image.png](http://liujun11.cn/upload/2020/09/image-9aa73e0a150949d3a4b43c5824425943.png)

4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

![image.png](http://liujun11.cn/upload/2020/09/image-f30368c5e5dd4e3d8e2271045a4b1d83.png)

6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

![image.png](http://liujun11.cn/upload/2020/09/image-4908191caf474302b3c8b111aada122e.png)

## 常见收集器组合

![image.png](http://liujun11.cn/upload/2020/09/image-49c8b987c370406d8fbda91c2eab175a.png)

![image.png](http://liujun11.cn/upload/2020/09/image-993cec4c3ea2493f821c2ed20ba8dab6.png)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
应用性能监控
应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档