JVM入门知识总结

在学习虚拟机之前我们要知道为什么要学习虚拟机呢?

首先就是增加自己的知识,其次就是面试的需要,其实不懂 JVM 也可以照样写出优质的代码,但是不懂 JVM 有可能别被面试官虐得体无完肤.

一.虚拟机的概述

虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机

1.名声很大的 VMware 就属于系统虚拟机,它是完全对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。

2.程序虚拟机典型的代表就是 java虚拟机了,也就是JVM,它专门为执行某个单个计算机程序而设计。在 java 虚拟机中执行的指令我们称为 java 字节码指令。

Java 虚拟机是一种执行 java 字节码文件的虚拟计算机,它拥有独立的运行机制。

Java 技术的核心就是 java 虚拟机,因为所有的 java 程序都运行在 java 虚拟机内部。

二.JVM的作用:

1.将字节码加载到内存中(运行时数据区)

2.负责存储数据

3.把字节码翻译为机器码, 执行

4.垃圾回收

现在的 JVM 不仅可以执行 java 字节码文件,还可以执行其他语言编译后的字节码文件,是一

个跨语言平台

三.JVM的组成

1.类加载器(负责加载字节码文件)

2.运行时数据区(存储运行时数据, 堆, 栈(java虚拟机栈, 运行java自己的方法), 方法区, 程序计数器, 本地方法栈)

3.执行引擎(更底层, 把字节码进一步翻译为机器码)

4.本地方法接口

5.垃圾回收

程序在执行之前先要把 java 代码转换成字节码(class 文件),jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中的运行时数据区(Runtime Data Area) ,而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(NativeInterface) 来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能,而垃圾回收主要针对于运行时数据区.

    • 类加载器

作用: 负责从硬盘/网络中加载字节码信息, 加载到内存中(运行时数据区的方法区中)

加载过程: 加载,链接,初始化

(1)加载: 使用IO读取字节码文件,转换并存储, 为每个类创建一个Class类的对象, 存储在方法区中

(2)链接: 1.验证: 对字节码文件格式进行验证, 文件是否被污染, 对基本的语法格式进行验证.

2.准备: 为静态的变量进行内存分配,在准备阶段的初始值是0,而不是自己赋的值, 静态的常量在编译期间就初始化

3.解析: 将符号引用转为直接引用,将字节码中的表现形式转为内存中的表现形式(内存地址)

(3)初始化:类的初始化, 为类中的定义的静态变量进行赋值,value在初始化阶段后的值是自己赋的值.

类在什么时候会加载?

1.在类中运行main方法

2.创建对象

3.使用类中的静态变量

4.反射 Class.forName("类的地址")

5.子类被加载

以下两种情况类不会被初始化:

编译期间赋值的静态常量,当牵扯到计算时就会加载(public final static int RANDOM = new Random().nextInt() ; //会导致类加载)

作为数组类型

类的初始化顺序

对 static 修饰的变量或语句块进行赋值.如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

顺序是:父类 static –> 子类 static

有哪些类加载器(具体的负责加载类的一些代码)?

1.引导类加载器:

是用c/c++语言开发的,jvm底层的开发语言,负责加载java核心类库. 与java语言无关

2.扩展类加载器:

Java 语言编写的,由sun.misc.Launcher$ExtClassLoader 实现. 派生于 ClassLoader 类,从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载

3.应用程序类加载器:

加载程序中自己开发的类

4.自定义类加载器

双亲委派机制

Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式

加载一个类时,现委托给父类加载器加载,如果父类加载器没有找到,向上级委托,知道引导类加载器.父级加载器找到就直接返回, 父级如果没有找到, 就委派给自己加载器,最终没有找到就报ClassNotFoundException

假如我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们new String()时,如果没有双亲委派机制,直接由应用程序类加载器加载,就会覆盖掉系统中的String类

new java.lang.String();//这里自己定义的String类里面的静态代码块没有执行,说明类没有加载,是由引导类加载器加载并且找到的Stirng,避免了自己定义的String覆盖了系统核心类库中的Stirng

双亲委派机制的优点

1.安全,可避免用户自己编写的类替换 Java 的核心类,如 java.lang.String.

2.避免类重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次

双亲委派机制, 是java提供的类加载的规范, 但不是强制不能改变的我们可以通过自定义的类加载器, 改变加载方式

如何打破双亲委派机制

我们可以自定义一个类加载器, 可以通过继承ClassLoader类,重写loadClass/findClass方法,

从而实现自定义类的加载, 但是重写loadclass方法会打破原有的双亲委派机制,因为loadclass方法中调用了findclass方法,findclass方法底层只是抛出了一个异常,说明这个方法需要开发者自己去实现, 所以我们一般建议去重写findclass方法

典型的tomcat中, 加载部署在tomcat中的项目时,就是用的是自己的类加载器

2.运行时数据区

Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:

1.程序计数器

程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读取下一条指令.

是一块很小的内存空间,用来记录每个线程运行的指令位置.

是线程私有的, 每一个线程都拥有一个计数器,生命周期与线程一致.

是运行时数据区中唯一一个不会内存溢出的空间, 运行速度最快

2.本地方法栈

用来运行本地方法的一块区域,也是线程私有的,内存空间大小都是可以调整的,可能会出现栈溢出

Java 虚拟机栈管理 java 方法的调用,而本地方法栈用于管理本地方法的调用.

3.java栈

运行时单位, 解决程序的运行问题,管理方法的调用运行, 是用来运行java方法的区域,也会出现栈溢出是线程私有的,保存一些局部变量,结果,参与方法的调用和返回, 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用,栈的运行速度仅次于程序计数器,它的操作就只有调用方法入栈和执行结束出栈,对于栈来说不存在垃圾回收问题.

工作原理:

遵循先进后出的原理, 最顶部的称为当前栈,执行引擎运行的所有字节码指令只针对当前栈帧进行操作. 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.不同的线程中的栈帧不能引用另一个线程的栈帧, 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.

Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出

栈帧内部结构:

1.局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

2.操作数栈

一个队表达式求值计算的过程,程序中的所有计算过程都是在借助于操作数栈来完成的。

3.动态链接

因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

4.方法返回地址

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址

4.堆

是存储空间,用来存储对象,是内存空间最大的一块区域, 在jvm启动时就会创建, 大小可以调整(jvm调优),本区域是存在垃圾回收的, 是线程共享的区域,所有的对象实例都应当在运行时分配在堆上. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除. 堆还是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.

堆空间的分区:

新生区(年轻代)

伊甸园区(对象刚刚创建存储在此区域)

幸存者0

幸存者1

老年代(老年区)

为什么要分区

可以根据对象的存活的时间把它们放在不同的区域就可以区别的进行对待,频繁的回收年轻代,较少回收老年代

创建对象,在堆内存中分布

1.新创建对象,都存储在伊甸园区

2.当垃圾回收时,将伊甸园区垃圾对象直接销毁,将伊甸园区的幸存对象移动到幸存者1区

3.之后创建的对象还是存储在伊甸园区,在此垃圾回收时,将伊甸园区中存活的对象移动到幸存者2区,同样将幸存者1区存活的对象移动到幸存者2区,每次保证一个幸存者区为空,相互转换

4.每次垃圾回收时都会记录此对象经理的垃圾回收次数,当一个对象经历过15次回收,任然存活,就会被移动到老年区

垃圾回收次数,在对象头中有一个4bit的空间记录 最大值只能是15

5.老年区回收次数较少,当内存空间不够用时,才会触发 Major GC,进行养老区的内存清理.

6. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常.

Java.lang.OutOfMemoryError:Java heap space

新生区与老年区的配置比例

配置新生代与老年代在堆结构的占比(一般不会调)

1. 默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整

个堆的 1/3

2. 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代

占整个堆的 1/5

3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优

在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 :1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8, 新生区的对象默认生命周期超过 15 ,就会去养老区养老

分代收集思想Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集.

部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:

新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.

老年区收集(Major GC / Old GC):只是老年区的垃圾收集.

整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.

整堆收集出现的情况:

System.gc();时

老年区空间不足

方法区空间不足

开发期间尽量避免整堆收集.

堆空间的参数设置

官方文档

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+PrintFlagsInitial 查看所有参数的默认初始值

-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内存的 1/64)-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

字符串常量池

在jdk7之后,将字符串常量池的位置从方法区转移到了堆空间中,因为方法区的回收在整堆收集中,回收的频率较低,堆空间的回收频率较高

方法区的回收在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存

5.方法区

主要就是来存储加载类的信息, class/method/field 等元数据、static final 常量、static 变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。

在jvm启动时创建,大小也是可以调整, 是线程共享,也会出现内存溢出

方法区,堆,栈交互关系

方法区存储类信息(元信息)

堆中存储创建的对象

栈中存储对象引用

方法区大小设置

-XX:MetaspaceSize 设置方法区的大小

windows jdk默认的大小是21MB

也可以设置为-XX:MaxMetaspaceSize 的值是-1,级没有限制. 没有限制 就可以使用计算机内存

可以将初始值设置较大一点,减少了FULL GC发生

方法区的内部结构

类信息

以及即时编译期编译后的信息,

以及运行时常量池(指的就是类中各个元素的编号)

运行常量池就是一张表,虚拟机指令根据这张表,找到要执行的类名、方法名、参数类型、字面量(常量)等信息,存放编译期间生成的各种字面量(常量)和符号引用

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。

收集方法区中的类也称作类卸载, 条件比较苛刻, 只有满足一些条件才可以变成不在使用的类.

类卸载

1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子

类的实例。

2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加

载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。

3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通

过反射访问该类的方法

3.执行引擎

jvm将字节码信息加载到内部的时候,并不能直接在操作系统上进行,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表,以及其他辅助信息.

而执行引擎就是将这些字节码信息解释/翻译为对应平台上的机器码指令

前端编译: .java ---编译-->.class 在开发期间,由jdk提供的编译器(javac)进行源码编译 (前端编译)

由执行引擎完成的后端编译: .class(字节码)----解释/编译---> 机器码 (后端编译,在运行时,由执行引擎完成的)

执行引擎的编译方式: 1.解释执行 2.编译执行

解释器: 对字节码采用逐行解释的方式,执行效率低, 将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行

JIT编译器: 将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的 JIT 代码缓存中(执行效率更高了),但就是要进行一次编译,不会立即执行.

为什么要两者一块使用呢?

当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行。而编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取一个平衡点。

4.本地方法接口

本地方法: 使用native修饰的方法,没有方法体

为什么要使用本地方法呢: java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可.

5.垃圾回收

在运行过程中,没有被任何引用 指向的对象,被称为垃圾对象.

如果不及时清理这些垃圾对象,会导致内存溢出.在回收时,还可以将内存碎片进行整理.(数组必须是连续空间的)

早期的垃圾回收是由程序员手动进行的, 这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。

内存溢出: 经过垃圾回收后,内存中仍然无法存储新创建的对象,内存不够用溢出.

内存泄漏: IO流 close jdbc连接 close 没有关闭,生命周期很长的对象, 一些已经不用的对象,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用的内存,称为内存泄漏,大量的此类对象存在,也是导致内存溢出的原因.

java垃圾回收机制的内存自动管理:

自动内存管理的优点

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险.

自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.

自动内存管理的缺点

对程序员管理内存的能力降低了, 解决问题能力变弱了, 不能调整垃圾回收的机制

对哪些区域进行回收呢?

垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,

Java 堆是垃圾收集器的工作重点

从次数上讲:

频繁收集 Young 区

较少收集 Old 区

基本不收集元空间(方法区)

垃圾标记阶段算法

作用: 判断对象是否是垃圾对象, 是否有引用指向对象.

相关的标记算法: :引用计数算法和可达性分析算法

1.引用计数算法:

有个计数器来记录对象的引用数量

String s1 = new String("aaa");
String s2 = s1;  //有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1

缺点:

需要维护计数器,占用空间,频繁操作需要事件开销

无法解决循环引用问题. 多个对象之间相互引用,没有其他外部引用指向他们,计数器都不为0,不能回收,产生内存泄漏.

2.可达性分析算法/根搜索法

实现思路: 从一些为根对象(GCRoots)的对象出发去查找,与根据对象直接或间接连接的对象就是存活对象,不与根对象引用链连接的对象就是垃圾对象.

GC Roots 可以是哪些元素?

在虚拟机栈中被使用的.

在方法中存储的静态成员指向的对象

作为同步锁使用的 synchronized

在虚拟机内部使用的对象

3.对象的 finalization 机制

protected void finalize() throws Throwable { }

当一个对象被标记为垃圾后,在真正被回收之前,会调用一次Object类中finalize(). 是否还有逻辑需要进行处理.

自己不要在程序中调用finalize(),留给垃圾回收器调用.因为以下三点:

1.在 finalize()时可能会导致对象复活。

2.finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,

若不发生 GC,则 finalize()方法将没有执行机会。

3.一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。

有了finalization机制的存在,在虚拟机中把对象状态分为3种:

1.可触及的 不是垃圾,与根对象连接的

2.可复活的 判定为垃圾了,但是还没有调用finalize(),(在finalize()中对象可能会复活)

3.不可触及的: 判定为垃圾了,finalize()也被执行过了,这种就是必须被回收的对象

垃圾回收阶段算法

当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃

圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对

象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是:

标记-复制算法(Copying)

将内存分为大小相等的两份空间, 把当前使用的空间中存活的对象 复制到另一个空间中, 将正在使用的空间中垃圾对象清除.

优点: 减少内存碎片

缺点: 如果需要复制的对象数量多,效率低.

适用场景: 存活对象少 新生代适合使用标记复制算法

标记-清除算法(Mark-Sweep)

清除不是真正的把垃圾对象清除掉,

将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可.

特点:

实现简单

效率低,回收后有碎片产生

标记-压缩算法(Mark-Compact)

将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

二者的本质差异在于标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销.

优点

消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价。

缺点

从效率上来说,标记-压缩算法要低于复制算法。

移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址, 移动过程中,需要全程暂停用户应用程序。即:STW

垃圾回收算法小结

效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。

而为了尽量兼顾上面提到的三个指标,标记-压缩算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段

Stop the World

Stop-the-World,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。

可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿,为什么需要停顿所有 Java 执行线程呢?

1.分析工作必须在一个能确保一致性的快照中进行

2.一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上

3.如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证,会出现漏标,错标问题

4.被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少 STW 的发生。

5.越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉

垃圾回收器

垃圾收集器是垃圾回收的实际实现者,垃圾回收算法是方法论.

由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多的垃圾回收器。从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。实际使用时,可以根据实际的使用场景选择不同的垃圾回收器,这也是 JVM 调优的重要部分.

垃圾回收器的分类

按照线程数量:

单线程垃圾回收器

Serial

Serial old

多线程垃圾回收器

Parallel

按照工作模式分为:

独占式: 垃圾回收线程执行时,其他线程暂停

并行式: 垃圾回收线程可以和用户线程同时执行

按工作的内存区间:

年轻代垃圾回收器

老年代垃圾回收器

GC 性能指标

吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)

垃圾收集开销:垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

内存占用:Java 堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间

HotSpot 垃圾收集器

图中展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器

CMS 回收器

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

垃圾回收过程

初始标记-短暂, 仅仅只是标记一下 GC Roots 能直接关联到的对象, 速度很快。

并发标记-和用户的应用程序同时进行, 进行 GC Roots 追踪的过程, 标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。 这个时间比较长, 所以采用并发处理(垃圾回收器线程和用户线程同时工作)

重新标记-短暂, 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短。

并发清除, 只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象, 这个过程非常耗时。

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

优点: 可以作到并发收集

弊端:

1.CMS 是基于标记-清除算法来实现的,会产生内存碎片。

2.CMS 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

3.CMS 收集器无法处理浮动垃圾(floating garbage)

浮动垃圾:

由于 CMS 并发清理阶段用户线程还在运行着, 伴随程序运行自然就还会有新的垃圾不断产生, 这一部分垃圾出现在标记过程之后, CMS 无法在当次收集中处理掉它们, 只好留待下一次 GC 时再清理掉。 这一部分垃圾就称为“浮动垃圾” 。

由于浮动垃圾的存在, 因此需要预留出一部分内存, 意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。

三色标记算法

由于cms有并发执行过程,所以在标记垃圾对象时有不确定性.

所以在标记时,将对象分为3种颜色(3种状态)

黑色: 例如GCRoots 确定是存活的对象

灰色: 在黑色对象中关联的对象,其中还有未扫描完的, 之后还需要再次进行扫描

白色: 与黑色,灰色对象无关联的, 垃圾收集算法不可达的对象

标记过程:

1.先确立GCRoots, 把GCRoots标记为黑色

2.与GCRoots关联的对象标记为灰色

3.再次遍历灰色,灰色变为黑色,灰色下面有关联的对象,关联的对象变为灰色

4.最终保留黑色,灰色, 回收白色对象

这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并

发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。

漏标:

假设 GC 已经在遍历对象 B 了,而此时用户线程执行了 A.B=null 的操作,切

断了 A 到 B 的引用

本来执行了 A.B=null 之后,B、D、E 都可以被回收了,但是由于 B 已经变为灰色,它仍会被当做存活对象,继续遍历下去,最终的结果就是本轮 GC 不会回收 B、D、E,留到下次 GC 时回收,也算是浮动垃圾的一部分。

错标

假设 GC 线程已经遍历到 B 了,此时用户线程执行了以下操作:

B.D=null;//B 到 D 的引用被切断

A.xx=D;//A 到 D 的引用被建立

B 到 D 的引用被切断,且 A 到 D 的引用被建立。

此时 GC 线程继续工作,由于 B 不再引用 D 了,尽管 A 又引用了 D,但是因为 A 已经标记为黑色,GC 不会再遍历 A 了,所以 D 会被标记为白色,最后被当做垃圾回收。

可以看到错标的结果比漏表严重的多,浮动垃圾可以下次 GC 清理,而把不该回收的对象回收掉,将会造成程序运行错误。

G1(Garbage-First) 垃圾优先

将堆内存各个区又分成较小的多个区域, 对这些个区域进行监测,对某个区域中垃圾数量大的区域优先回收,也是并发收集的.

1.初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需

要停止用户线程,单线程执行。

2.并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。

3.最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。

4.筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域.这就是 Garbage First 的由来——第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用

猜你喜欢

转载自blog.csdn.net/weixin_71243923/article/details/128881464
今日推荐