JVM概述

1、JVM概述

1.1为什么要学习JVM?

我们很多人心里都很清楚,学习JVM就是为了面试需要,但是在面试的时候肯定不能这样回答。
学习JVM当然是可以使得我们对Java语言的执行更加了解。

1.2JVM作用

首先来讲一下,把.java文件通过javac编译成为.class文件的过程不归JVM管,这个是开发阶段的事情。JVM负责将字节码文件加载到虚拟机中,再将字节码解释/编译成机器码。管理运算时数据存储,垃圾回收。
另外,现在的JVM不仅可以执行Java的字节码文件,还可以执行其他语言编译后的字节码文件,是一个跨语言平台。
JVM的特点:一次编译到处执行,自动内存管理,自动垃圾回收功能

1.3JVM的组成部分

先放高清图:


JVM整体组成部分:
1.类加载器(ClassLoader):将字节码文件加载到Java虚拟机
2.运行时数据区(Runtime Data Area):按照不同的数据进行存储
3.执行引擎(Execution Engine):将字节码编译/解释成机器码
4.本地库接口(Native Interface):负责调用操作系统方法

2、类加载

2.1类加载子系统概述

类加载子系统负责从硬盘上加载字节码文件。类加载子系统只负责字节码文件的加载,是否可以运行由执行引擎决定。加载的类信息放于一块称为方法区的内存空间。

2.2类加载过程

2.2.1加载

首先通过类名(类地址)获取此类的二进制字节流,然后将该类的静态存储结构转换为方法区的运行时结构,最后在内存中生成一个代表该类的Class对象,作为访问该类数据的访问入口。

2.2.2链接

验证: 检验被加载的类内部结构是否正确,并和其他类协调一致
1.验证文件格式是否一致:class文件在文件开头有特定的文件标识(字节码文件都以CA FE BA BE表示开头)
2.元数据验证:对字节码文件描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
准备: 准备阶段负责为类的静态属性分配内存,并设置默认初始值。(注意:这里的静态属性不包括静态常量,静态常量在编译阶段进行初始化)
例如:public static int key = 10;
key在准备阶段之后的初始值为0,而不是10
解析: 将类的符合引用替换为直接引用(符号引用时class文件的罗基符号,直接引用指向方法区中某一地址)

2.2.3初始化

初始化是为类的静态变量赋予正确的初始值
类什么时候初始化?
1.通过new关键字创建对象
2.使用类的静态变量,静态方法
3.对类进行反射操作
4.初始化子类会导致父类的初始化
5.执行该类的main方法
类不会被初始化的场景
1.引用类的静态常量,这里的常量是指已经指定字面量的常量,对于需要计算才能得出结果的常量就会导致类加载。
2.构造某个类的数组时不会导致该类的初始化
类的初始化顺序
如果同时包含多个静态变量和静态代码块,会按照自上而下的顺序依次执行。
如果初始化一个类时,父类还未初始化,则优先初始化其父类

2.3类加载器分类

站在JVM角度看, 类加载器可以分为两种:
1.引导类加载器:用C/C++编写的,JVM底层的开发语言,负责加载Java核心类库
2.其他类加载器:由Java语言实现,全部继承自java.lang.ClassLoader
站在Java开发人员角度,自JDK1.2以来一直保持着三层类加载器
1.引导类加载器(启动类加载器 BootStrap ClassLoader):
用C/C++实现,JVM底层的开发语言,用来加载Java的核心类库。
并不继承自java.lang.ClassLoader没有父加载器
引导类加载器只加载存放于<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数指定的路径中存储的类。
2.扩展类加载器(Extension ClassLoader)
Java语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现,派生自ClassLoader类,
加载jre/lib/ext目录下的类
3.应用程序类加载器(系统类加载器 Application ClassLoader)
Java 语言编写的,由 sun.misc.Launcher $AppClassLoader 实现,派生于 ClassLoader 类,
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类,
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
4.自定义类加载器

2.4双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说需要该类时才会将它的class文件加载到内存中生成class对象。并且在加载某个类时的class文件时,Java虚拟机采用的是双亲委派机制。

双亲委派机制工作原理:
一个类加载器收到了类加载请求,它不会先去加载,而是将这个请求委托给父类加载器去执行。如果父类加载器还存在父类加载器,则进一步向上委托,最终请求到达引导类加载器。如果父类加载器可以完成类的加载任务,就成功返回,如果父类加载器无法完成加载任务,子类加载器才会尝试自己去加载,这就是双亲委派机制。如果都加载失败,就会抛出ClassNotFoundException异常。
双亲委派机制优点?
1.安全,可以避免用户编写的类替换Java的核心类
2.避免类重复加载,如果父类加载器已经加载了该类,就没必要子类加载器再加载一次

2.5如何打破双亲委派机制

Java虚拟机的类加载器本身可以满足加载的要求,但是也允许开发者自定义类加载器。再ClassLoader类中涉及类加载的方法有两个,loadClass(String name),findClass(String name),这两个方法并没有被final修饰,也就表示其子类可以重写。我们可以通过自定义类加载器,并重写方法打破双亲委派机制。
loadClass方法是实现双亲委派逻辑的地方,修改它会破坏双亲委派机制,而重写findClass则不会

3、JVM运行时数据区

3.1运行时数据区组成概述

JVM运行时数据区,不同的虚拟机实现可能有所不同,但都遵守Java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存会包括以下运行时数据区:

3.2程序计数器

程序计数器存储下一条指令的地址,也就是即将要执行指令代码,由执行引擎读取下一条指令。
特点:
1.内存空间小,也是运行速度最快的区域
2.再JVM规范中,每个线程都有自己的程序计数器。属于线程私有的,生命周期与线程生命周期一致
3.存储当前线程执行指令的地址
4.是Java虚拟机规范中没有内存溢出的区域,也没有垃圾回收

3.3Java虚拟机栈

Java虚拟机栈,早期也叫Java栈,每个线程再创建时都会创建一个虚拟机栈。每个虚拟机栈内部保存着一个个栈帧,每个栈帧对应着一个方法的调用。Java虚拟机栈是线程私有的,它保存方法的局部变量(基本数据类型或者对象类型),部分返回结果,并参与方法的调用和返回。
特点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM对于Java栈的操作只有两个:入栈和出栈。
Java栈不存在垃圾回收问题,但是当线程调用的方法数量过多时,会出现栈溢出
栈的运行原理:
在Java栈中会有许多栈帧,每个栈帧对应一个方法,栈顶的栈帧即为当前执行的方法,栈底的栈帧对应main方法。栈顶栈帧被称为当前栈帧,与当前栈帧对应的方法是当前方法,定义该方法的类是当前类。如果一个方法调用另一个方法,对应新的栈帧就会被创建出来,放在栈的顶端,称为新的当前栈帧。如果栈顶栈帧对应的方法执行完毕,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机就会丢弃当前栈帧,使得前一个栈帧成为当前栈帧。
Java方法有两种返回方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不论何种方式,都会导致栈帧弹出。
栈帧的内部结构:
1.局部变量表:局部变量表是一组变量的存储空间,用于存放方法参数和方法内部定义的局部变量。
基本数据类型存储值,引用数据类型则存储指向对象的引用。
2.操作数栈或表达式栈:栈最典型的一个应用就是用来对表达式求值。线程在执行方法时,实际上就是不断执行语句的过程,而归根结底就是进行计算的过程。因此可以这样说,程序中所有的计算过程都是借助操作数栈来完成。
3.动态链接或指向运行时常量池的方法引用:在方法的执行过程中可能需要用到类中的变量,所以必须要有一个引用指向运行时常量
4.方法返回地址:当一个方法执行完成之后,要返回调用它的地方,因此栈帧中必须保存方法返回地址

3.4本地方法栈

Java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用。Java语言中有一些方法直接操作内存,这种方法一般都是本地方法,用C语言编写。本地方法栈是线程私有的,和Java虚拟机栈一样,如果线程调用的方法数量过多会栈溢出
常见的本地方法:
可以在Object类和Thread类中进行查询,应该还有其他类中大量含有本地方法

public final native Class<?> getClass();
public native int hashCode();
protected native Object clone() throws CloneNotSupportedException;
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public static native Thread currentThread();
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
private native void start0();

3.5Java堆内存

3.5.1堆内存概述

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,存放运行时的对象
Java堆区在JVM启动的时候被创建,其空间就确定了,也是JVM管理的最大一块内存区域
堆内存的大小可以调节
所有线程共享Java堆
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆是GC执行垃圾回收的重点区域

3.5.2堆内存区域的划分

Java8之后堆内存分为:新生代+老年代
新生代分为伊甸园和幸存者区域

3.5.3为什么要分区(代)?

将对象根据存活概率进行划分,存活时间长的,放在固定区,减少垃圾扫描时间和GC频率。根据分类执行不同的垃圾回收算法,对算法扬长避短。

3.5.4对象创建内存分配的过程

一段废话:为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如
何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考
虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片
正片:
1.新创建的对象先放在伊甸园区
2.当伊甸园的空间填满时,程序又需要创建对象,垃圾回收器堆伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的垃圾对象销毁,再加载新的对象到伊甸园区。
3.然后将伊甸园区中幸存的对象移动至幸存者1区
4.如果再次发生垃圾回收,则将伊甸园区中幸存的对象和上次存放到幸存者1区中,现在仍然存活的对象移动到幸存者2区,将伊甸园区和幸存者1区中的垃圾进行消除,以保证每次都有一个幸存者区域空着
5.如果再次发生垃圾回收,此时便会将存活对象放置到幸存者1区,接着再去幸存者2区
6.当有对象经历过15次垃圾回收,便会移动至老年代
这个次数可以设置,我这里不谈,因为现在用不到
在对象头中,由四位数据来控制这个次数,最大值为1111,当对象经历过15次垃圾回收,便会移动至老年代
7.老年区在内存不足时,会触发垃圾回收
8.如果养老区触发垃圾回收(Major GC)之后仍然无法保存对象,便会报内存溢出异常

3.5.5堆空间的参数设置

官网地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-Xms:初始堆空间内存
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细的 GC 处理日志

3.5.6分代收集思想

JVM在进行垃圾回收时,并不是每次都是新生区和老年区一起回收的,大部分回收指的是新生区。针对HotSpot VM的实现,它里面的GC按照回收区域分为两种类型:部分收集和整堆收集
部分收集:新生区收集和老年区收集
整堆收集:收集整个Java堆和方法区垃圾回收
整堆收集出现的情况:
System.gc();
老年区空间不足,方法区空间不足
开发器间要避免整堆收集

3.5.7字符串常量池

字符串常量池为什么要调整位置?
JDK7及以后的版本将字符串常量池放到了堆空间中。因为方法区的回收效率较低,在FULL GC时才会触发方法区的垃圾回收,而FULL GC是老年代的空间不足、方法区不足时才会触发。这就会导致字符串常量池回收效率较低,而开发中会有大量的字符串创建,回收效率低,就会导致内存不足。放在堆中,能及时回收内存。
说白了就是,字符串常量池放置在堆中会及时回收内存。

3.6方法区

3.6.1方法区概念

方法区是一个被线程共享的区域。其中主要存储加载的字节码、class/method/field等元数据、static final常量、static变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开,所以方法区看作是一块独立于Java堆的内存空间。
方法区在JVM启动时创建,并且它的实际物理内存空间和Java堆区一样都可以是不连续的
方法区的大小和堆空间一样,可以选择固定大小或者扩展
方法区的大小决定了系统可以保存多少个类,如果系统定义了过多的类,导致方法区溢出
关闭JVM就会释放方法区内存

3.6.2 方法区大小设置

Java 方法区的大小不必是固定的,JVM 可以根据应用的需要动态调整.
元数据区大小可以使用参数-XX:MetaspaceSize 和-XX:MaxMataspaceSize 指定,替代上述原有的两个参数
默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB,
-XX:MaxMetaspaceSize的值是-1,级没有限制
这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线一旦触及就会触发 Full GC.
因此为了减少FullGC那么这个-XX:MetaspaceSize 可以设置一个较高的值

3.6.3方法区的垃圾回收

一般来说方法区的垃圾回收难以令人满意,对于类卸载的条件苛刻。但是这部分区域的回收又确实是必要的。
判断一个类属于“不再被使用的类”要满足下面三个条件:
1.该类及该类的子类都已经被回收了
2.加载该类的类加载器已经被回收
3.该类的Class对象没有被引用

4、本地方法接口

什么是本地方法?
本地方法是指被native关键字修饰的方法,本地方法不是有Java语言实现,有操作系统实现。
Java中为什么需要本地方法?
因为上层的高级语言没有对底层硬件直接操作的权限,而是通过操作系统提供的接口进行访问

5、执行引擎

5.1概述

执行引擎是Java虚拟机最核心的组成部分之一。执行引擎任务是将字节码指令解释/编译为对应平台上的机器码指令。
注意区分:
前端编译:将.java文件编译成.class字节码文件这个过程是前端编译
后端编译:将.class字节码文件编译/解释称为机器码指令

5.2解释器与编译器

解释器: Java虚拟机启动的时候会对字节码文件采用逐行执行的方法执行
JIT编译器: 虚拟机将源代码一次性编译成机器码指令

5.3为什么Java是半编译半解释型语言

Java在执行代码时,会将编译执行和解释执行结合起来。
当程序启动时,解释器可以马上发挥作用,省去了编译的时间
程序在运行过程中会将热点代码编译成机器码提高执行效率
这样编译执行和解释执行结合之后便会提升代码的执行效率

6、垃圾回收

6.1垃圾回收概述

6.1.1概述

Java和C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动收集
垃圾收集不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾回收技术的Lisp语言诞生

6.1.2什么是垃圾?

垃圾是指在程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾
如果不及时对垃圾进行回收,这些垃圾会一直占用内存,最终可能会导致内存溢出

6.1.3为什么需要GC?

如果不进行垃圾回收,内存迟早被消耗完,因为不断分配内存空间却不进行回收,就好像不断产生生活垃圾而不打扫卫生一样。除了释放没有的对象,垃圾回收还可以清除内存中的记录碎片。碎片整理将占用的堆内存移动至堆的一段,已便后续的空间分配

6.1.4早期的垃圾回收

在早期的C/C++时期,垃圾回收要通过程序员手动回收,这样会给开发人员带来繁重的工作。如果有一处内存空间没有回收,就会产生内存泄漏,长此以往,可能直至出现内存溢出并造成程序崩溃

6.1.5垃圾回收的关心区域

垃圾回收对堆进行回收,甚至是全栈和方法区的回收,其中堆是垃圾回收的重点区域
从次数上讲:频繁回收老年代,较少手机年轻代,基本不收集方法区

6.2垃圾回收相关算法

6.2.1垃圾标记阶段算法

6.2.1.1标记阶段的目的

在堆中存放着几乎所有的Java对象实例,在执行垃圾回收前,需要先区分出哪些是有用对象,哪些是垃圾对象。只有被标记为垃圾对象,才会在垃圾回收时,释放占用的内存空间,这个过程称之为垃圾标记阶段
判断一个对象是否为垃圾对象一般有两种方式:引用计数算法和可达性分析算法

6.2.1.2引用计数算法

首先要清楚,引用计数算法并未在JVM中使用,因为无法解决循环引用问题
引用计数算法: 每个对象保存一个整形的引用计数器属性,记录对象被引用的次数。如果有引用指向了某个对象,该对象的引用计数器属性就加一。反之,当引用失效的时候,引用计数器属性减一。只要对象的引用计数器属性值为0,表示该对象为垃圾对象
优点: 实现简单,不过这都是屁话,因为该种算法有致命性问题
缺点:
1.需要单独的字段记录对象被引用的次数
2.每次指向该对象的引用变化时,都伴随着加法和减法
3.无法解决循环引用问题,导致Java回收器没有使用这种算法
循环引用问题:
有三个对象:A,B,C,A引用了B,B引用了C,C引用了A。然后有一个引用指向了对象A,所以我们可以通过该引用访问A,B,C对象,当我们令该引用指向null时,此时三个对象的引用计数器属性都为1,但是此时三个对象无法被访问。这样便无法被标记为垃圾对象,产生了内存泄露

6.2.1.3可达性分析算法

可达性分析算法:也可以成为根搜索算法
相较于引用计数算法,可达性分析算法不仅具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决引用计数算法无法解决的循环引用问题
可达性分析算法: 可达性分析算法以根(GC Roots)为起点,按照从上至下的方式搜索被跟对象所关联的所有对象,如果对象直接或间接的与跟对象相连,表示即为可用对象。反之,即为垃圾对象

哪些对象可以被作为GC Roots?
1.虚拟机栈引用的对象,比如各个线程调用方法中使用的参数,局部变量等
2.方法区中类静态属性引用的对象
3.被synchronized同步锁持有的对象
4.Java虚拟机内部的引用:一些常驻的异常对象,系统类加载器

6.2.1.4对象的finalization机制

当一个对象被标记为垃圾对象,垃圾回收器回收此对象前,总会先调用对象的finalize()方法
finalize()方法允许在子类中被重写,用于对象被回收时进行资源释放。通常在这个对象中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库链接等
但是永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
原因:
1.在finalize方法中可能会导致对象复活
2.finalize()方法的执行时间没有保障,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法没有执行机会
3.一个糟糕的finalize会影响GC性能。比如finalize是个死循环

6.2.1.5生存还是死亡

由于finalize方法的存在,虚拟机中的对象一般处于三种可能的状态。
可触及的:从根节点可以到达这个对象
可复活的:对象的所有引用都被释放,但是对象可能会在finalize方法中复活
不可触及的:对象的finalize方法被调用,并且没有复活,那么就会进入不可触及状态为
判断一个对象是否可以回收,要经历两次标记过程:
1.如果对象从根节点开始无法到达,则进行第一次标记
2.进行筛选,判断对象是否有必要执行finalize方法
  如果对象没有重写finalize方法或者finalize方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象被判定为不可触及的
  如果对象重写了finalize方法,且还未执行过,那么对象会被插入到队列中,由一个虚拟机创建的、低优先级的Finalizer线程执行finalize方法
  finalize方法是对象逃离死亡的机会,稍后GC会对队列中的对象进行第二次标记。如果对象在finalize方法中与引用链上的任何一个对象产生联系,那么在第二次标记时,对象会被移除即将回收的集合。之后,对象如果再次出现没有引用存在情况,这个时候,finalize方法不会再执行,对象变为不可触及状态

6.2.2垃圾回收阶段算法

当成功区分出内存中的存活对象和死亡对象后,垃圾回收器接下来的任务就是执行垃圾回收。
目前JVM中比较常见的三种垃圾收集算法是:复制算法,清除算法,压缩算法

6.2.2.1复制算法

它可将内存按容量分为两块,每次只使用其中一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清楚正在被使用内存块中的所有对象。
复制算法适合存活对象少,垃圾对象多,适合收集新生代

6.2.2.2清除算法

把需要清除对象的地址保存在空闲的地址列表中。下次有新对象时,使用新对象覆盖这些列表中的地址
清除算法适合存活对象多,垃圾对象少,适合老年代。会产生内存碎片

6.2.2.3压缩算法

清除算法可以应用于老年代中,但是回收内存后会产生内存碎片,所以需要改进
压缩算法正是解决了内存碎片这一问题,压缩算法会将存活对象按顺序存储到内存的一端。清理其余的空间
优点:消除了压缩算法中内存碎片的缺点,我们需要为新对象分配存储空间的时候,只需要持有一个内存的起始地址即可。
缺点:移动对象的时候,如果对象被其他对象引用,则需要调整引用的地址,需要全面暂停用户线程

6.2.2.4分代收集

由于不同对象的生命周期不一样。因此我们可以对不同生命周期的对象采用不同的收集方式,以提高回收效率。另外,不同的回收算法各有千秋,我们使用算法时扬长避短,尽量发挥算法的优势。

6.3垃圾回收相关概念

6.3.1内存泄漏与内存溢出

内存溢出:应用程序占用内存速度非常快,造成垃圾回收已经跟不上内存消耗的速度,这样就会导致内存溢出
内存泄露:对象不会被程序再用到,但是又不能回收它们的情况。尽管内存泄漏并不会立即导致程序崩溃,但是内存泄露会一步步蚕食内存,长此以往就会导致内存溢出
一些提供close的资源未关闭就会导致内存泄漏,数据库连接dataSourse.getConnection(),网络连接 socket 和 io 连接必须手动close,否则是不能被回收的。

6.3.2STW

STW(Stop the World),指的是在GC事件发生过程中,会产生应用程序的停顿,停顿会导致整个应用程序线程都会被暂停,这个暂停称为STW
可达性分析算法就会导致所有Java线程停顿,为什么需要暂停所有的Java执行线程呢?
如果分析过程中对象的引用关系在不断变化,则分析结果的准确性无法保证,所以需要暂停所有Java执行线程
由于STW会暂停所有用户线程,导致出现停顿,所有要尽量减少STW的出现

6.4垃圾回收器

6.4.1垃圾回收器概述

如果说垃圾收集方法是方法论,那么垃圾收集器就是实践者。
垃圾收集器在JVM规范中没有过多的规定,可以由不同的厂商、不同版本的JVM来实现
由于JDK版本的高速迭代,Java发展至今已经衍生出众多的垃圾回收器。从不同的角度分析垃圾收集器,可以将GC分为不同的类型。
实际使用时,可以根据不同的使用场景选择不同的垃圾回收器,这也是JVM调优的重要组成部分

6.4.2垃圾回收器分类

按线程数量可以分为单线程(串行)垃圾回收器和多线程(并行)垃圾回收器
单线程垃圾回收器(Serial): 只有一个线程进行垃圾回收,适用于小型使用场景,会导致其他用户线程暂停
多线程垃圾回收器(Parallel): 提供多个线程进行垃圾回收器,在多CPU场景下效率高,但同样会暂停其他用户线程
按照工作模式,可以分为独占式和并发式垃圾回收器
独占式垃圾回收器: 在垃圾回收时,用户线程会暂停
并发式垃圾回收器: 用户线程可以和垃圾回收线程同时执行
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器

6.4.3GC性能指标

吞吐量:运行用户代码的时间占总运行时间的比例
垃圾收集开销:垃圾收集的时间占总运行时间的比例
暂停时间:执行垃圾回收时,程序工作线程被暂停的时间
内存占用:Java堆区所占用的内存大小

6.4.4HotSpot垃圾收集器

下图展示了7种不同的收集器,如果两个收集器之间存在连线,说明可以搭配使用。

6.4.5CMS回收器

CMS(Concurrent Mark Sweep,并发标记清除)收集器是以达到最短回收停顿时间为目标的收集器,它在垃圾收集时使得用户线程和垃圾回收线程并发执行,因此垃圾回收过程中用户不会感到明显停顿

垃圾回收过程:
初始标记:STW,仅用一条初始标记线程对所有与GC Roots直接或间接关联的对象进行标记
并发标记:垃圾回收线程与用户线程并发执行,此过程进行可达性分析,标记处所有的垃圾对象
重新标记:STW,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来
并发清除:只使用一条GC线程,与其他用户线程并发执行,清除刚才标记的垃圾对象,这个过程非常耗时
并发标记与并发清除过程耗时最长,且可以与用户线程一起执行,因此,总体上来说,CMS回收器垃圾回收线程和用户线程可以一起执行
CMS优点: 可以做到并发收集,低延迟
CMS缺点:
CMS是基于标记清除算法来实现的,会产生内存碎片
CMS在并发阶段虽然不会导致用户程序停顿,但是回因为占用一部分线程导致应用程序变慢,总吞吐量会降低
CMS在并发清理阶段,用户线程和垃圾回收线程同时进行,用户线程仍在制造垃圾,因此会产生浮动垃圾。浮动垃圾就是本地GC无法清理,只能留到下次GC再清理
三色标记算法
为了提高JVM的垃圾回收性能,从CMS垃圾收集器开始,引入了并发标记的概念。引入并发标记就会带来问题,在程序的执行过程中,会对现有的引用关系链产生改变。技术通常都是双刃剑嘛
三色标记法将对象分为了黑、灰、白三种颜色
黑色:该对象已经被标记过了,且该对象下的属性也全部标记过了,例如GC Roots
灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有被扫描的引用
白色:表示对象没有被垃圾收集器访问过,表示不可达
三色标记的过程:
1.刚开始,确定GC Roots的对象为黑色
2.将与GC Roots直接关联的对象设置为灰色
3.遍历灰色对象的所有引用,灰色对象设置为黑色,与其关联的引用设置为灰色
4.重复步骤3,直至没有灰色对象为止
5.结束时,黑色对象存活,白色对象回收
这个过程正确执行的前提是没有其他线程改变对象之间的引用关系,然后,并发标记的过程,用户线程仍在运行,因此就会产生漏标和错标的问题
漏标:
假设GC已经在遍历对象B了,而此时线程执行了A.B=null的操作,切断了A到B的引用。本来执行A.B=null之后,B,D,E都可以回收了,但是由于B已经称为灰色,继续遍历下去,B,D,E仍然存活,称为浮动垃圾。

错标:
假设GC线程已经遍历到B了,此时用户线程执行了以下操作:B.D=null,A.XX=D,B到D的引用切断,A到D的引用建立。此时GC线程继续工作,由于A已经被标记为黑色,不会再遍历A了,所以D将一直都是白色,最后被当作垃圾回收。

显而易见,错标比漏标严重,漏标导致产生的浮动垃圾下次GC可以清理,但是错表会把不该回收的对象回收,造成错误
错标产生的原因: 灰色指向白色的引用断开,黑色指向白色的引用建立
只要我们打破其中一条就可以解决错标的问题
原始快照打破的是第一个条件:当灰色对象指向白色对象的引用断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一下
增量更新打破的是第二个条件:当黑色对象指向白色对象的引用建立时,就将这条引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象建立了指向白色对象的引用,就会变成灰色对象
总结
CMS为了让GC线程和用户线程一起工作,回收算法的过程比以前旧的回收器复杂很多。究其原因,在于并发标记的过程中,用户线程不断改变引用关系的原因。
虽然CMS从来没有被JDK当作默认的垃圾收集器,存在许多缺点,但是它开创了垃圾回收线程与用户线程并发工作的先河,为后面的收集器提供了思路

6.4.6G1回收器

由于应用程序面对的业务越来越复杂,用户越来越多,所以尝试对垃圾回收器进行优化。同时适应不断扩大的内存和不断增加的处理器数量,进一步降低时间,同时兼顾良好的吞吐量。官方给G1设定的目标是尽可能获得高的吞吐量。
为什么名字叫Garbage First(G1)呢?

G1是一个并行回收器,将堆内存的分割,使得内存粒度化,(物理上不连续,逻辑上连续),使用多个区域表示伊甸园,幸存者区,老年代等
G1避免了在堆中进行全区域的垃圾收集,G1会统计各个区域垃圾堆积的价值大小,维护一个列表,每次根据允许的收集时间,优先回收垃圾多的区域。由于垃圾多的区域优先回收,所以这款回收器成为垃圾优先回收器
G1是一款面向服务器端的垃圾回收器,主要针对于配备了多核CPU以及大容量内存的机器,具备高吞吐量
如图所示,G1收集器收集过程有初始标记、并发标记、最终标记、筛选回收,和CMS收集器前几步的收集过程很相似:

垃圾回收过程:
1.初始标记:标记出与GC Roots直接或者间接相连的对象,这个过程很快,需要停止用户线程
2.并发标记:从GC Roots开始进行可达性分析,找出存活对象,这个过程耗时较长,但可以和用户线程并发执行
3.最终标记:标记并发标记过程中产生的垃圾
4.筛选回收:筛选回收阶段会用最少的时间回收垃圾最多的区域,这里为了提高效率,没有和用户线程并发执行

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值