1.类的加载
1.1. 类加载过程
在Java中,类的加载可以分为加载、连接、初始化、使用、卸载五个过程,而连接又分为验证、准备、解析三个过程。这些过程都是在持续运行期间完成的
- 加载
查找并加载类的二进制数据
- 连接
- 验证:确保被加载类的正确性,如:jdk版本,字节流中包含的内容符合虚拟机规范等
- 准备:为类的静态变量分配内存,并将其初始化为默认值。如:int类型的静态变量,在准备阶段会为其分类内存赋予默认值为0
- 解析:把类中的符号引用转换为直接引用
- 初始化
为类的静态变量赋予正确的值。通过执行类构造器方法来为静态变量赋予正确的值,类构造器方法可以理解为编译器收集到的所有为静态变量赋值动作、静态代码块合并成一个方法,这个方法就是类构造器方法,在初始化阶段执行这个方法
- 使用
Java 程序对类的使用分为主动使用和被动使用两种
- 主动使用
1.2. JVM类加载器
1.2.1. JVM自带类加载器
JVM 自带类加载器有根类加载器(BootstrapClassLoader)、扩展类加载器(ExtensionClassLoader)、系统类加载器(SystemClassLoader)三种
- BootstrapClassLoader
跟类加载器用来加载Java的核心类,是原生代码实现的,并不继承自java.lang.ClassLoader(负责加载$JAVA_HOME/jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader的子类)。由于根类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
- ExtensionClassLoader
扩展类加载器负责加载jre的扩展目录,lib/ext目录或由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器null
- SystemClassLoader
系统类加载器(应用类加载器),负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性。或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父类加载器。由Java语言实现,父类为ExtClassLoader
1.2.2. 类加载器加载Class步骤
1.2.3. 自定义类加载器
除了JVM提供自带的类加载器外,还可以自定义类加载器来加载Class二进制文件。实现自定义类加载器的三步:
- 继承ClassLoader
- 重写findClass()方法
调用defineClass()方法
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException(name);
} else {
// defineClass方法将字节码转化为类
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 从自定义路径中加载指定类,返回类的字节码文件
InputStream in = null;
ByteArrayOutputStream out = null;
String path = "/com/javastack/" + name + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
// xx.xx.CustomClassLoader@610455d6
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.3. 类加载机制
1.3.1. 全盘负责机制
当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入
1.3.2. 双亲委派机制
双亲委派机制是指类加载器收到加载请求时,先不自己加载,而是交给父类加载器试图加载Class,只有父类加载器无法加载该类时才尝试从自己的类路径加载该类。通俗的讲,就是某个特定的类加载器接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,请求最终将达到顶层类加载器,如果父类加载器可以完成类的加载任务就由父类加载。只有父类加载器无法完成加载时才由自己去加载
双亲委派机制的优势
- 双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,就没必要子类加载器再加载以此。
避免Java核心API中定义的类不会被随意篡改,假设通过网络传递一个名为java.lang.Integer的类,JVM加载时会传递到启动类加载器(BootstrapClassLoader),而启动类加载器在JavaAPI核心中发现该类已经被加载,直接返回这个类的Integer.class,并不会重新加载网络传递过来的java.lang.Integer。这样可以有效的防止核心API库被篡改
1.3.3. 缓存机制
将已经被加载过的Class放到缓存中,当程序再次加载这个类时,类加载器先从缓存区中查找该Class,如果缓存中不存在该Class对象时,系统才会读取该类的二进制数据并转化成Class对象。如果缓存中存在该Class对象,那么直接返回。这就是为什么修改Class后,必须重启JVM的原因。
1.4. 如何加载类
1.4.1. 什么是类的加载
类的加载是指将类的.class二进制数据读入内存,然后整理成类的元数据放到JVM运行时数据区的方法区内,根据类的元数据结构在堆内存中创建实例对象
1.4.2. 类加载过程做了什么?
通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区类的运行时数据结构
- 在Java方法区中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的数据访问入口
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区中,然后再Java堆区中创建一个java.lang.Class类的对象,这样可以通过该对象访问方法区中的这些数据
1.4.3. 加载.Class文件的方式
- 通过本地加载
- 通过网络下载.Class文件
- 从Zip、Jar等归档文件中加载.Class文件
- 从数据库中提取.Class文件
- 将Java源文件动态编译为.Class文件
1.4.4. 类加载的方式
类加载三种方式
- 命令行启动应用时JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
Class.forName()方法和ClassLoader.loadClass()区别
Class.forName():将类的.class 文件加载到jvm中之后,还会对类进行解释,执行类中的static块
ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static 静态代码块中的内容,只有再newInstance()方法时才会区执行static静态代码块
2. 运行时数据区
JVM运行时数据区分为程序计数器、虚拟机栈、元数据区、堆区、本地方法栈五个区。
其中虚拟机栈、程序计数器为线程私有区域,说明每个线程启动时都有一块独有的内存空间,不受其他线程干扰。堆区、元数据区为线程共享区域,所有线程共享堆区和元数据区内的信息,在多线程情况下会有线程安全问题
2.1. 程序计数器
程序计数器是一个块很小的内容空间,它是线程私有的。程序计数器可以看作是当前线程所执行的字节码的行号指示器。字节码解释器执行时通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、异常处理、线程恢复等功能都是依赖程序计数器来完成
为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,个线程之间计数器互不干扰,独立存储
程序计数器主要两个作用
- 字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制
- 在多线程情况下,程序计数器用于记录当前线程执行位置,解决线程由挂起切换到执行时知道线程上次执行到哪里了。
程序计数器是JVM中唯一不会出现OutOfMemeryError 的内存区域。如果线程执行的是Java方法,那么计数器记录虚拟机字节码指令的地址,如果为Native方法那么计数器为空
2.2. 虚拟机栈
虚拟机栈和程序计数器一样,同样属于线程私有的,它的生命周期和线程相同,栈是描述Java方法执行的内存模型
每个方法被执行的时候都会创建一个栈帧用于存储局部变量、操作栈、动态连接、方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程
局部变量表主要存放了编译期间可知的各种基本数据类型(boolean、byte、char、short、int、flot、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
Java虚拟机栈会出现两种异常类型:
- 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError
- 虚拟机栈空间可以动态扩展,当动态扩展无法申请到足够的空间时,抛出OutOfMemery异常
2.3. 本地方法栈
本地方法栈和虚拟机栈的作用非常相似,区别在于虚拟机栈是执行虚拟机Java代码(字节码),而本地方法栈是为虚拟机执行本地的Native方法。在HotSpot虚拟机中和Java虚拟机栈合二为一
2.4. 方法区
方法区是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区为了和堆区分,又称为非堆
2.5. 堆
Java堆是虚拟机所管理的最大的一块内存区域,是所有线程共享的一个内存区域,在虚拟机启动时创建。堆内存区域的作用是存放对象实例,几乎有所的对象实例以及数组都在这里分配内存
JVM根据对象的存活周期不同,把堆内存划分为新生代和老年代。新生代又分为Eden区、S0区、S1区,在觉大多数情况下,新建的对象首先会分配在新生代中的Eden区,在一次新生代回收之后,进入S0或者S1区,每经过一轮新生代的回收,如果对象还存活,对象的年龄就加1,当对象经过一定次数(默认15次)新生代回收后依然存活,就会被认为是老年代对象,转到老年代中
2.5.1. 对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局分为
- 对象头
- 实例数据
- 对其填充
2.5.1.1 对象头(markword)
在32位系统下,对象头8个字节,64位系统是16个字节
2.5.1.2. 实例数据
存放对象中各种类型的字段类型,不管是从父类中继承下来的还是子类中定义的
分配策略:相同宽度的字段总是放在一起,如:double和long类型
2.5.1.3. 对齐填充
起到占位符的作用满足JVM的要求
由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符填充
3. 垃圾收集器
3.1. 哪些内存需要回收
虚拟机栈、本地方法栈、程序计数器是属于线程私有的,随着线程的创建内分配,线程结束自动清除,所有这部分区域的内存不需要GC进行垃圾回收。需要垃圾回收的区域是线程共享的堆区和方法区
3.2. 什么时候回收
堆区中存放的是Java创建的实例对象,因此在回收之前必须确定哪些对象是“存活”的,哪些对象是“死亡”的,回收时只回收“死亡”对象。
3.3. 确定回收对象
JVM在扫描对象没有引用时(既死亡对象),就认为是需要被回收,通过引用计数法和可达性分析算法来分析对象是否可以回收
3.3.1. 引用计数法
所谓引用计数法是指给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1。当计数器位0时说明对象就不再被引用
这个方法很简单,效率也高,但是目前主流的虚拟机中并没有选择这种算法来管理内存,最主要的原因是它很难理解对象之间相互循环引用的问题。
如下面代码所示,除了objA和objB相互引用这对方之外,这两个对象没有其他任何引用。但是他们之间相互引用,导致他们的引用计数器都不位0,导致GC无法回收这些对象
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
3.3.2. 可达性分析
通过一系列称为“GC Roots”的对象作为起点,从这些节点向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots 没有任何引用链相连的话,就说明此对象不可达,可以判定其为一个可回收对象
可以作为“GC Roots”对象有以下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 被同步锁持有的对象
3.3.3. 引用类型
无论是可达性分析还是引用计数法来分析对象是否可以回收,可达性分析是分析对象的引用是否还被其他对象引用,引用计数法是判断引用对象的计数器值是否为0
JAVA中的引用类型有强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种
- 强引用(StrongReference)
我们使用的大部分引用实际上都是强引用,如果一个对象具有强引用,垃圾回收器是不会回收它的。当内存空间不足时,Java虚拟机会抛出OutOfMemeryError错误使程序终止,也不会去回收强引用对象来解决内存不足的问题
- 软引用(SoftReference)
软引用对象类似于一个可有可无的对象,当JVM内存足够时,垃圾回收器不会回收它,如果内存空间不够时,垃圾回收器就会回收这些软引用对象。只要垃圾回收器没有回收它,那么该对象就可以被程序使用。软引用可以实现内存敏感的高速缓存
- 弱引用(WeakReference)
弱引用和软引用比较类似,区别在于弱引用对象比软引用对象的生命周期更短。在垃圾回收器触发GC时,一旦发现了弱引用对象,不管当前JVM内存是否足够,都会回收它。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾收集器回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
弱引用和队列组合使用
public static void main(String[] args) throws Exception {
ReferenceQueue<Object> queue = new ReferenceQueue(); // 创建引用队列
WeakReference<Object> weak = new WeakReference(new Object(), queue); // 弱引用处理
System.out.println("【GC回收前】弱引用:" + weak.get() + "、引用队列:" + queue.poll());
System.gc(); // 执行GC
TimeUnit.SECONDS.sleep(1); // 引用队列需要进行回收对象的存储
System.out.println("【GC回收后】弱引用:" + weak.get() + "、引用队列:" + queue.poll());
}
运行结果
【GC回收前】弱引用:java.lang.Object@573fd745、引用队列:null
【GC回收后】弱引用:null、引用队列:java.lang.ref.WeakReference@15327b79
- 虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
虚引用和队列组合使用
public static void main(String[] args) throws Exception {
Object obj = new Object(); //创建强引用
ReferenceQueue<Object> queue = new ReferenceQueue(); // 创建引用队列
PhantomReference<Object> weak = new PhantomReference<>(new Object(), queue); // 幽灵引用处理
obj = null; // 强行断开强引用
System.out.println("【GC回收前】幽灵引用:" + weak.get() + "、引用队列:" + queue.poll());
System.gc(); // 执行GC
TimeUnit.SECONDS.sleep(1); // 引用队列需要进行回收对象的存储
System.out.println("【GC回收后】幽灵引用:" + weak.get() + "、引用队列:" + queue.poll());
}
运行结果
【GC回收前】幽灵引用:null、引用队列:null
【GC回收后】幽灵引用:null、引用队列:java.lang.ref.PhantomReference@573fd745
4. 垃圾收集算法
4.1. 标记清除法
故名思及,标记清除法分为“标记”和“清除”两个阶段,首先标记出所有不需要回收的对象,然后把所有没有被标记到的对象进行回收。这是一种最基础的收集算法,其他几种算法都是对这种算法的不足加以改进的。
这种算法有两个明显的问题
- 效率问题
- 内存碎片化问题(对象回收后产生大量的不连续的碎片空间)
4.2. 标记-复制算法
因为标记清除法是先扫描整个内存区域进行标记,导致效率比较底下。为了解决效率问题,在标记清除法的基础之上改进,将内存区域分为大小相同的两块,每次只使用其中的一块内存区域。当这一块内存使用完以后,就将还存活的对象复制到另一块去,然后再把被使用的这块空间一次清理掉。这样就使每次内存回收都是对内存区间的一般进行回收。我们管这种算法叫“标记-赋值”算法。这种算法有一个缺点就是内存复用率不高,因为每次要保留一半的内存空间,适用于新生代中
4.3. 标记-整理算法
为了解决内存利用率不高的问题,提出了一种“标记-整理”算法,标记的过程和“标记-清除”算法一样,区别在后续步骤不是直接对可回收内存对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界意外的内存。“标记-整理”算法相比较“标记-复制”算法内存利用率要高,但是涉及到内存的整理,在效率上没有“标记-复制”算法高,适合老年代的垃圾回收算法
4.4. 分代收集算法
当前虚拟机的垃圾收集器都是采用分代收集算法,这种算法没有什么新的思想。就是把堆内存区域分为新生代、老年代。我们根据老年代和新生代的特点选择合适的垃圾收集器算法
如:在新生代中,每次收集都会有大量的对象需要被回收,所以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。老年代的对象存活概率比较高,一般情况下老年代内存分配的比较小,所以我们选择“标记-整理”算法进行垃圾收集
5. 垃圾收集器
5.1. CMS收集器
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标设计的收集器,非常适合在注重用户体验的应用上使用
CMS(Concurrent Mark Sweep) 收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程基本上同时并行执行
CMS收集器是一种“标记-清除”算法实现的,它的收集过程分为以下几个步骤:
- 初始标记:暂停所有的其他线程(短暂STW),并标记直接和“GC Roots”相连的对象,速度很快
- 并发标记:同时开启GC线程和恢复用户线程,对“初始标记”这一步中标记过的对象进行扫描,得到所有可达对象。
- 并发预清理:查找所有在并发标记阶段进入老年代的对象(因为在并发标记阶段恢复了用户线程,所以在并发标记期间有一些对象可能从新生代晋身到老年代,或者有一些对象被直接分配到老年代),通过重新扫描,减少下一阶段的工作
- 重新标记:此阶段会暂停虚拟机,对在“并发标记”阶段被改变的引用或者新建的对象进行标记。这个阶段停顿的时间一般会比初始标记阶段的时间长一些,但远远比并发标记阶段的时间要短
- 并发清除:恢复应用所有暂停的线程,对所有未标记的垃圾对象进行清理,并且会尽量将已回收对象的空间重新整理为一个连续的整理。在此阶段垃圾收集器线程和用户线程并发执行
- 并发重试:重置CMS收集器的数据结构,等待下一次垃圾回收
CMS垃圾收集器的优缺点:
- 只有在“初始标记”和“重新标记”阶段暂停应用线程,好处就是对应用程序的停顿时间段。
- 并发标记和应用线程会争抢CPU资源,对服务器CPU资源造成大的压力
- CMS是一款采用“标记-清除”算法的垃圾收集器,这种算法会导致大量的内存空间碎片产生
5.2. G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗CPU处理器及大内存的服务器,它在大多数情况下可以实现指定GC的暂停时间,以极高效率满足GC停顿时间要求的同时还具备高吞吐量性能特征
写一段程序来查看当前JVM的垃圾收集器
package com.javastack.test
public class JVMDemo {
public static void main(String[] args) {
String str = "";
for (int x = 0; x < 10996; x ++ ) {
str += "";
str.intern(); // 入池,垃圾
}
}
}
执行命令
java -Xmx8m -Xms8m -Xlog:gc* com.javastack.test.JVMDemo
运行结果
Using G1 (使用G1算法)
5.2.1. 介绍
G1收集器(Garbage Frist)是在JDK 1.7 u4版本之后正式引入到Java中的垃圾收集器,此类垃圾收集器主要应用在多CPU以及大内存服务器的环境下,这样可以极大的减少垃圾收集的停顿时间STW,以提升服务器的操作性能。引入此收集器的主要目的是为了在将来某一个时间内可以替换掉CMS(Concurrent Mark Sweep)收集器。
- G1垃圾收集器属于一个分代垃圾收集器
- G1垃圾收集器将堆内存划分为2048个左右的内存区(每个区叫Region),每个区的大小在1-32M之间,支持最大内存为:32M * 2048=65536M(64G)
- Eden、Survivor、Tenured就变为一系列不连续的内存区域,也就避免了全内存区的GC操作
- 每一个内存区都有唯一的一个分代类型(物理上不连续,逻辑上连续)
- 年轻代:Young
- 老年代:Tenured
- 巨型代:Humonous,一个对象大小超过了某一个阈值(HotSpot 中是分区的1/2)
5.2.2. Region操作
每一个Region都可以分成两部分,已分配和未被分配的。它们之间的界限被称为top。总体上来说,把一个对象分配到Region内,只需要简单的增加top的值,这个做法实际上就是Bump-The-Pointer操作流程
每一次都只有一个Region处于被分配状态(Current Region),所以在多线程访问下,G1采用了和CMS一样TLABs技术,为每一个线程分配一个Buffer,如果当前线程耗尽了自己的Buffer之后就需要申请新的Buffer,为防止可能带来的并发问题,G1回收器采用CAS来保证并发安全
之所以将整个堆区划分为若干个子区域(Region),就是为了进行GC操作的提升,所以所有的回收都是围绕着垃圾多的区域展开(空间越满就越有可能有垃圾出现,回收价值越高)
- Region是G1的最小回收单元,每一次都会回收N个Region,在每一次回收时都会选择可能回收最多垃圾的Region区域进行回收(目的是为了节约时间),同时G1收集器要维护一个Region链表,该链表将保存回收后的Region
- 新生代在达到数据存储上限时需要堆整个新生代进行回收和晋级处理,而不是分区处理,这样做的目的是保证新生代的分区策略和老年代相同,方便调整区分大小。MinorGC 会触发所有年轻代的区域整体回收
- G1是一种带有压缩功能的收集器,在回收老年代分区时,会将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部压缩。压缩就意味着可以获得连续的内存空间,这样方便进行内存的在分配
5.2.3. 回收流程
G1保留了分代的概念,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Regions(不需要连续)的集合,每个Region都可能随G1的运行在不同代之间切换
G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制
初始标记、重新标记、清除、转移回收会造成stw
年轻代收集
- 年轻代收集会,不会进行并发标记,所以它全程都是STW
- 应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集
- 工作过程
- 根扫描 Root Scanning:静态和本地对象等被扫描
- 更新已记忆集合 Update RSet:对dirty卡片的分区进行扫描,来更新RSet
- RSet扫描:在收集当前CSet之前,扫描CSet分区的RSet,检测old->young这种引用情况
- 转移和回收-Object Copy:讲CSet分区存活对象的转移到新survivor或old Region,回收CSet内垃圾对象
- 引用处理:主要针对软引用、弱引用、虚引用、final引用、JNI引用;当占用时间过多时,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理
在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
年老代收集
- 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
- 在年轻代收集之后或巨型对象分配之后,会去检查这个空间占比
- 年老代收集同时会执行年轻代收集,进行年老代的roots探测,既初始标记,stw
- 然后恢复应用线程,进行年老代并发标记
- stw,重新标记
- STAB处理
- 引用处理
- 继续stw,清楚垃圾
- 恢复应用线程
混合收集
- 在进行正常的年轻代垃圾收集,也会回收一部分老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中
- 假设一个Region的存活对象达到95%,而进行复制,效率很低,所以G1允许浪费部分内存,那么这个Region不会被混合收集,-XX:G1HeapWastePercent:默认5%
- stw,然后将Cset中的Regions进行收集,使用复制算法
- 下一次年轻代垃圾收集进行时,在将第二个1/8的年老代Regions加入Cset中进行收集
- 当年老代内单个Region的垃圾小于等于G1HeapWastePercent时,复制大量存活对象,效率很低。此时G1会确定结束混合收集周期。所以混合收集次数可能小于8次。
转移失败的担保机制 Full GC
- 当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。
- G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
- 由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。