jvm基础篇04-垃圾回收
目录
一、Serial + Serial Old(单线程回收组合,使用较少,在cpu比较匮乏时使用)
三、Parallel Scavenge + Parallel Old(吞吐量优先组合)
1. Parallel Scavenge(年轻代回收器,JDK8默认的年轻代垃圾回收器)
垃圾回收
··内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
··Java中引入了自动的垃圾回收(GC)机制,通过垃圾回收器,主要来对堆上不再使用的对象进行回收。
自动垃圾回收:自动根据对象是否使用由虚拟机来回收对象
• 优点:降低程序员实现难度、降低对象回收bug的可能性 • 缺点:程序员无法控制内存回收的及时性
手动垃圾回收:由程序员编程实现对象的删除
• 优点:回收及时性高,由程序员把控回收的时机 • 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
1.方法区的回收
··方法区回收的就是不在使用的类。
··判断一个类可以被卸载要满足一下三个条件:
1.此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。 2.加载该类的类加载器已经被回收。 3.该类对应的 java.lang.Class 对象没有在任何地方被引用。
package chapter04.gc; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; /** * 类的卸载演示类,通过循环创建自定义类加载器加载指定类,尝试触发类的卸载操作。 */ public class ClassUnload { /** * 程序入口方法,通过循环创建自定义类加载器加载指定类,并尝试触发垃圾回收以观察类的卸载情况。 * * @param args 命令行参数,在本程序中未使用。 * @throws InterruptedException 如果线程在休眠过程中被中断。 */ public static void main(String[] args) throws InterruptedException { try { // 用于存储加载的类的 Class 对象 ArrayList<Class<?>> classes = new ArrayList<>(); // 用于存储自定义的类加载器 ArrayList<URLClassLoader> loaders = new ArrayList<>(); // 用于存储通过反射创建的类的实例 ArrayList<Object> objs = new ArrayList<>(); // 无限循环,持续加载类并尝试触发垃圾回收 while (true) { // 创建一个自定义的 URLClassLoader,指定要加载类的路径 URLClassLoader loader = new URLClassLoader( new URL[]{new URL("file:D:\\lib\\")}); // 使用自定义类加载器加载指定路径下的类 Class<?> clazz = loader.loadClass("com.itheima.my.A"); // 通过反射创建加载类的实例 Object o = clazz.newInstance(); // 将创建的实例添加到 objs 列表中,当前代码被注释,不执行该操作 // objs.add(o); // 将类加载器置为 null,当前代码被注释,不执行该操作 // loader = null; // 将加载的类的 Class 对象添加到 classes 列表中 // classes.add(clazz); // 将自定义类加载器添加到 loaders 列表中,当前代码被注释,不执行该操作 // loaders.add(loader); // 手动触发垃圾回收,尝试卸载不再使用的类,调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要 执行垃圾回收Java虚拟机会自行判断。 System.gc(); } } catch (Exception e) { // 打印异常堆栈信息 e.printStackTrace(); } } }
··当上面方法被执行时,每一次循环,A类都会被加载之后又删除了。
而把注释的几行代码随便打开一个,破坏三个成立的条件,就发现A类无法被删除,由此可知,只有满足上面的三个条件,类才可以从方法区被删除。
开发中此类场景一般很少出现(自己编写的类是应用类加载器,所以只要被加载就不会被回收),主要在如 OSGi、JSP 的热部署等应用场景中。 每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类 加载器。重新创建类加载器,重新加载jsp文件。
2.堆回收
1.如何判断对象是否可以被回收
1.引用计数法
-
原理:给对象维护一个引用计数器,有新引用指向该对象时,计数器 +1;引用失效(如变量置
null
、超出作用域 )时,计数器 -1 。当计数器为 0,理论上对象可回收。优点是实现简单 -
缺陷:每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响;无法解决循环引用问题(如两个对象相互引用,计数器始终不为 0,但实际无其他引用使用它们 ),导致内存泄漏。因此主流 JVM 未采用该算法。
··为每个对象维护一个引用计数器,当对象对引用时加一,取消引用时减一,如下图:
如果在main方法中最后执行 a1 = null ,b1 = null,是否能回收A和B对象呢? 可以回收,方法中已经没有办法使用引用去访问A和B对象了。
··每次引用和取消引用都需要维护计数器,对性能有损耗,而且会出现循环引用问题,例如上图中的两个对象都被置为null之后,两个对象的计数器依然没有为0,(但其实两个对象都无法通过引用去访问了)也就无法回收。
2.可达性算分析算法(Java目前使用的)
-
核心逻辑:以 GC Roots 为起点,遍历所有可达对象,不可达对象则判定为可回收。
-
GC Roots 包含(JVM 认为 “绝对存活” 的对象 ):
⚫ 系统类加载器加载的java.lang.Class对象。 ⚫ 本地方法调用时使用的全局对象。 ⚫ 线程Thread对象。 ⚫ 监视器对象。
-
流程:
-
标记阶段:从 GC Roots 出发,标记所有能通过引用链访问到的对象(“可达对象” );
-
-
清除阶段:未被标记的对象(“不可达对象” )判定为可回收。
··可达性分析将Java对象分成两类:垃圾回收的跟对象(GCRoot)和普通对象,对象与对象之间存在引用关系,若对象通过引用链路可以到达一个GCRoot对象,就说明不可以被回收,反之就会。
··那么哪些是GCRoot对象呢?-----------------四种GCRoot对象:其他都是普通对象
1.线程Thread对象,引用栈帧的局部变量,方法参数等。如下图示例中,a和b对象有到达Thread对象的链路,所以不可以被回收。
2.系统类加载器(也就是应用程序类加载器,前面说过,它是不可回收的。这个类加载器(包含类的class对象,class对象中有静态变量)不回收,所以这个类也不不回收,类中静态变量也就不回收。)加载的java.lang.Class对象,引用类中的静态字段(静态字段从类被初始化开始就一直存在在方法区,直到类被销毁,所以它的生命周期应该和类是绑定的)。
3.监视器对象(每个Java对象都有一个与之关联的监视器对象,当线程访问同步方法或同步代码块时,必须先获取到对象的监视器),用来保存同步锁synchronized关键字持有的对象。
4.本地方法调用时使用的全局对象(开发人员不用关心)。
2.五种引用
··几种常见的对象引用
可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在, 普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式: ⚫ 软引用 ⚫ 弱引用 ⚫ 虚引用 ⚫ 终结器引用。不同引用对于对象的引用强弱(越强,对象越不容易被删除)关系是不同的
1.强引用
··可达性分析算法中的GCRoot对象对普通对象的的引用链属于强引用,只要普通对象通过强引用链可以找到GCRoot对象,普通对象就不可以被回收。
2.软引用
··若一个对象只被软引用所引用,当程序内存不足时,就会将软引用的数据回收,jdk1.2之后提供SoftReference类来实现软引用,软引用通常用于缓存,示例如下:
package chapter04.soft; import java.io.IOException; import java.lang.ref.SoftReference; /** * 软引用案例2 - 基本使用 * 该类演示了软引用的基本使用方式,软引用在内存充足时不会被回收, * 当内存不足时,垃圾回收器会回收软引用所引用的对象。 */ public class SoftReferenceDemo2 { /** * 程序入口方法,演示软引用的基本特性。 * * @param args 命令行参数,在本程序中未使用。 * @throws IOException 如果在输入操作时发生 I/O 异常。 */ public static void main(String[] args) throws IOException { // 创建一个大小为 100MB 的字节数组,模拟占用大量内存的对象 byte[] bytes = new byte[1024 * 1024 * 100]; // 创建一个软引用对象,让其引用刚刚创建的字节数组 SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes); // 将字节数组的强引用置为 null,此时字节数组仅被软引用引用 bytes = null; // 尝试通过软引用获取其所引用的对象并输出,此时内存充足,对象通常不会被回收 System.out.println(softReference.get()); // 再次创建一个大小为 100MB 的字节数组,进一步占用内存 byte[] bytes2 = new byte[1024 * 1024 * 100]; // 再次尝试通过软引用获取其所引用的对象并输出,此时可能因内存紧张开始回收软引用对象,就会打印null System.out.println(softReference.get()); // 又创建一个大小为 100MB 的字节数组,继续占用内存 byte[] bytes3 = new byte[1024 * 1024 * 100]; // 将软引用置为 null,此时原字节数组没有任何引用,会被垃圾回收,但什么时机置为null呢?软引用可能回收可能不回收,所以要把被回收的软引用保存起来,就可以处理了 // softReference = null; // 手动触发垃圾回收,当前代码被注释,未执行 // System.gc(); // 等待用户输入,当前代码被注释,未执行 // System.in.read(); } }
main 方法 :
-
创建初始对象和软引用 :创建一个 100MB 的字节数组 bytes ,并使用 SoftReference 对其进行软引用,随后将 bytes 置为 null ,此时该字节数组仅被软引用引用。
-
首次输出软引用对象 :调用 softReference.get() 尝试获取软引用所引用的对象并输出,由于此时内存通常充足,对象一般不会被回收。
-
继续占用内存并再次输出 :创建新的 100MB 字节数组 bytes2 ,进一步占用内存,再次调用 softReference.get() 输出对象,此时可能因内存紧张开始回收软引用对象。
-
清除软引用 :创建第三个 100MB 字节数组 bytes3 ,并将 softReference 置为 null ,此时原字节数组没有任何引用,可能会被垃圾回收。
-
注释代码 : System.gc() 用于手动触发垃圾回收, System.in.read() 用于等待用户输入,这两行代码当前被注释,未执行。
··我们将jvm的堆内存设置会200MB,将bytes对象放入SoftReference中实现软引用,然后将bytes置为null,释放掉强引用,此时bytes只有软引用,所以在内存不足时会释放掉byte数组,也就是第二次创建bytes2时释放掉,所以打印出来的情况是第一次有地址,第二次没有地址,如果将jvm的堆区内存提高到300MB,那么打印出来的结果就是两次都有地址,因为内存足够存放两次创建的数组。
··其实上面的代码存在一个缺陷,就是当SoftReference中的数据被回收时,SoftReference的对象也应该被删除,但是由于SoftReference的对象被强引用关联了(这里是Thread对象的强引用),导致该对象无法被回收。为了解决这一问题,SoftReference提供了一套队列机制:在实例化SoftReference时,可以传入一个队列,当SoftReference中的数据被回收后,SoftReference的对象会被放入该队列中(此时软引用对象并未被回收,需要手动回收),此时我们只需遍历该队列,并删除SoftReference即可(ReferenceQueue 的 poll() 方法会尝试从队列中取出并移除一个引用对象。如果队列中有元素,它会返回队列头部的引用对象,并且将该对象从队列中移除;如果队列为空,则返回 null 。),代码示例如下:
package chapter04.soft; import java.io.IOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.ArrayList; /** * 软引用案例3 - 引用队列使用 * 该类演示了如何使用软引用和引用队列。软引用在内存不足时会被垃圾回收器回收, * 引用队列可以用来跟踪被回收的软引用对象。 */ public class SoftReferenceDemo3 { /** * 程序入口方法,创建多个软引用对象并将其添加到列表中,同时关联引用队列, * 最后统计被垃圾回收的软引用对象数量。 * * @param args 命令行参数,在本程序中未使用。 * @throws IOException 如果发生输入输出异常。 */ public static void main(String[] args) throws IOException { // 用于存储软引用对象的列表 ArrayList<SoftReference> softReferences = new ArrayList<>(); // 引用队列,用于存储被垃圾回收的软引用对象 ReferenceQueue<byte[]> queues = new ReferenceQueue<byte[]>(); // 循环创建 10 个字节数组,并为每个数组创建软引用,将软引用添加到列表中 for (int i = 0; i < 10; i++) { // 创建一个大小为 100MB 的字节数组 byte[] bytes = new byte[1024 * 1024 * 100]; // 创建一个软引用对象,将字节数组和引用队列关联起来 SoftReference studentRef = new SoftReference<byte[]>(bytes, queues); // 将软引用对象添加到列表中 softReferences.add(studentRef); } // 用于存储从引用队列中取出的软引用对象 SoftReference<byte[]> ref = null; // 计数器,用于统计被垃圾回收的软引用对象数量 int count = 0; // 从引用队列中取出被垃圾回收的软引用对象,直到队列为空 while ((ref = (SoftReference<byte[]>) queues.poll()) != null) { // 每取出一个软引用对象,计数器加 1 count++; } // 输出被垃圾回收的软引用对象数量 System.out.println(count); } }
-
创建存储结构 :创建一个 ArrayList 用于存储软引用对象,创建一个 ReferenceQueue 用于存储被垃圾回收的软引用对象。
-
循环创建软引用 :通过 for 循环创建 10 个大小为 100MB 的字节数组,并为每个数组创建一个软引用对象,将软引用对象添加到 ArrayList 中。
-
统计被回收的软引用数量 :使用 while 循环从引用队列中取出被垃圾回收的软引用对象,每取出一个对象,计数器 count 加 1。
-
输出结果 :最后输出被垃圾回收的软引用对象数量。
看到这,你可能会问:软引用对象不是被ArrayList强引用了吗?为什么会被回收
虽然 ArrayList 强引用了 SoftReference 对象,但真正可能被回收的是 SoftReference 所引用的对象,而非 SoftReference 本身,软引用的特性是:在内存充足时,软引用所引用的对象不会被回收;当内存不足,即将发生 OutOfMemoryError 时,JVM 会先回收软引用所引用的对象,以此来释放内存。下面详细解释:
在代码里,存在如下引用关系:
-
ArrayList 强引用 SoftReference 对象。
-
SoftReference 软引用 byte[] 数组对象。
软引用对象是否被回收
当 bytes 数组被垃圾回收器回收时, studentRef 这个软引用对象会被添加到 queues 队列里,但软引用对象本身并不会立即被回收。软引用对象( SoftReference 实例)属于普通的 Java 对象,遵循 Java 对象的垃圾回收规则。
只有当没有任何强引用指向这个软引用对象,并且在后续的垃圾回收过程中,垃圾回收器判定该对象可以被回收时,它才会被回收。在当前代码里, softReferences 列表持有对这些软引用对象的强引用,所以在代码执行期间,这些软引用对象不会被回收。直到 softReferences 列表不再引用这些软引用对象,并且经过垃圾回收,它们才会被销毁。
注意区分:软引用对象本身(相当于一个盒子)的回收时机
软引用对象( SoftReference 实例)属于普通的 Java 对象,遵循 Java 垃圾回收的一般规则。只有当没有任何强引用指向这个软引用对象,并且在后续的垃圾回收过程中,垃圾回收器判定该对象可以被回收时,它才会被回收。
// 创建一个大小为 100MB 的字节数组,模拟占用大量内存的对象 byte[] bytes = new byte[1024 * 1024 * 100]; // 创建一个软引用对象,让其引用刚刚创建的字节数组 SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes); // 将此软引用的局部变量置为 null,此时软引用对象本身(相当于一个盒子)没有任何引用,会被垃圾回收,但什么时机置为null呢?软引用可能回收可能不回收,所以要把被回收的软引用保存起来,就可以处理了 //softReference 是一个局部变量,它对 SoftReference 对象持有强引用。只要 softReference 变量还在作用域内,它就会保持对 SoftReference 对象的强引用,这个 SoftReference 对象就不会被回收。 softReference = null; // 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列 //通过代码遍历引用队列,将SoftReference的强引用删除 -------------------------------------------------------------------- //如下所示,cleanCache中,不仅将局部变量ref=null,还将hashmap对其的强引用也删除了,那么软引用对象就没有任何强引用了,可以删除了。 // 创建学生对象的软引用,并关联到引用队列 StudentRef ref = new StudentRef(em, q); /** * 清除那些所软引用的 Student 对象已经被回收的 StudentRef 对象。 * 从引用队列中取出被回收的软引用,并从映射中移除对应的键值对。 */ private void cleanCache() { StudentRef ref = null; // 循环从引用队列中取出被回收的软引用 while ((ref = (StudentRef) q.poll()) != null) { // 从映射中移除对应的键值对,此后软引用对象就没有任何强引用了,可以删除了 StudentRefs.remove(ref._key); } }
遍历队列解决问题的原理
遍历队列并删除 SoftReference 能解决问题,是因为这一操作切断了程序中对这些 SoftReference 对象的强引用。下面详细解释:
-
强引用的存在形式 通常, SoftReference 对象会被存储在集合(如 Map 、 List )中,这些集合对 SoftReference 对象持有强引用;只要这些强引用存在, SoftReference 对象就不会被回收。
-
遍历队列删除 SoftReference 遍历引用队列时,会把队列里的 SoftReference 对象找出来,然后从存储它们的集合里移除,或者断开 Thread 这类对象对它们的引用。这样一来, SoftReference 对象就不再有强引用指向它,满足了垃圾回收的条件。
打印结果分析
若堆空间仅有 200MB,运行代码时,打印结果大概率是 9。下面详细解释其原因:
在代码里,每次循环都会创建一个大小为 100MB 的 byte 数组,并且循环 10 次。由于堆空间只有 200MB,当创建第 2个 100MB 的 byte 数组时,堆空间就会不足。此时,垃圾回收器会启动,尝试回收之前软引用所引用的对象。
因为软引用在内存不足时会被优先回收,所以这前 9个 100MB 的 byte 数组都会被回收,对应的软引用对象会被添加到 queues 引用队列中。后续代码从 queues 中取出软引用对象并计数,最终输出的计数结果就是 9。
这是 Java 中 ** 软引用(SoftReference)** 在缓存场景的应用示意图,实体信息解析如下:
核心实体与逻辑
-
角色与引用关系:
-
StudentCache
(缓存类):通过强引用持有HashMap
(缓存容器 )。 -
HashMap
:作为缓存,key
是学生 ID(唯一标识 ),value
是软引用对象(指向Student
实例 )。 -
软引用对象
:用软引用关联Student
,当 JVM 内存不足时,Student
可被回收,避免内存溢出。 -
Student
:实际缓存的学生数据对象。
-
-
软引用的意义:
-
软引用关联的对象,在内存充足时会被保留,内存不足时会被 JVM 回收,适合实现内存敏感的缓存(如学生数据缓存,优先保存在内存,内存紧张时释放 )。
-
-
清理逻辑:
-
若软引用关联的
Student
被回收,需清理HashMap
中对应的key
,避免缓存失效(“空引用” 占坑 )。
-
package chapter04.soft; /** * 软引用案例4 - 学生信息的缓存 * 该类使用软引用实现了一个学生信息的缓存系统,当内存充足时,缓存中的学生信息会被保留; * 当内存不足时,垃圾回收器会回收这些软引用所引用的学生信息对象,从而释放内存。 */ public class StudentCache { // 单例模式,创建一个 StudentCache 实例 private static StudentCache cache = new StudentCache(); /** * 程序入口方法,用于测试学生信息缓存功能。 * 不断向缓存中添加学生信息对象。 * * @param args 命令行参数,在本程序中未使用。 */ public static void main(String[] args) { // 无限循环,不断创建学生对象并缓存 for (int i = 0; ; i++) { StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i))); } } // 用于存储缓存内容的映射,键为学生 ID,值为学生对象的软引用 private Map<Integer, StudentRef> StudentRefs; // 垃圾引用的队列,用于存储被垃圾回收的学生对象对应的软引用 private ReferenceQueue<Student> q; /** * 继承 SoftReference,使得每一个实例都具有可识别的标识。 * 并且该标识与其在 HashMap 内的 key 相同。 */ private class StudentRef extends SoftReference<Student> { // 学生对象的 ID,作为该软引用的标识 private Integer _key = null; /** * 构造方法,创建一个学生对象的软引用,并关联到引用队列。 * * @param em 学生对象 * @param q 引用队列 */ public StudentRef(Student em, ReferenceQueue<Student> q) { // 调用父类构造方法,创建软引用并关联引用队列 super(em, q); // 保存学生对象的 ID _key = em.getId(); } } /** * 私有构造方法,构建一个缓存器实例。 * 初始化存储软引用的映射和引用队列。 */ private StudentCache() { // 初始化存储软引用的 HashMap StudentRefs = new HashMap<Integer, StudentRef>(); // 初始化引用队列 q = new ReferenceQueue<Student>(); } /** * 取得缓存器实例,使用单例模式确保全局只有一个缓存器实例。 * * @return 缓存器实例 */ public static StudentCache getInstance() { return cache; } /** * 以软引用的方式对一个 Student 对象的实例进行引用并保存该引用。 * 在保存之前会先清除垃圾引用。 * * @param em 学生对象 */ private void cacheStudent(Student em) { // 清除垃圾引用 cleanCache(); // 创建学生对象的软引用,并关联到引用队列 StudentRef ref = new StudentRef(em, q); // 将学生对象的软引用添加到映射中 StudentRefs.put(em.getId(), ref); // 输出当前缓存中软引用的数量 System.out.println(StudentRefs.size()); } /** * 依据所指定的 ID 号,重新获取相应 Student 对象的实例。 * 如果缓存中有该学生对象的软引用,则从软引用中获取; * 否则,重新构建一个实例,并保存对这个新建实例的软引用。 * * @param id 学生 ID * @return 学生对象实例 */ public Student getStudent(Integer id) { Student em = null; // 检查缓存中是否有该学生对象的软引用 if (StudentRefs.containsKey(id)) { // 获取该学生对象的软引用 StudentRef ref = StudentRefs.get(id); // 从软引用中获取学生对象 em = ref.get(); } // 如果没有软引用,或者从软引用中得到的实例是 null if (em == null) { // 重新构建一个学生对象实例 em = new Student(id, String.valueOf(id)); // 输出提示信息,表示从信息中心获取学生对象 System.out.println("Retrieve From StudentInfoCenter. ID=" + id); // 将新构建的学生对象添加到缓存中 this.cacheStudent(em); } return em; } /** * 清除那些所软引用的 Student 对象已经被回收的 StudentRef 对象。 * 从引用队列中取出被回收的软引用,并从映射中移除对应的键值对。 */ private void cleanCache() { StudentRef ref = null; // 循环从引用队列中取出被回收的软引用 while ((ref = (StudentRef) q.poll()) != null) { // 从映射中移除对应的键值对,此后软引用对象就没有任何强引用了,可以删除了 StudentRefs.remove(ref._key); } } // /** // * 清除 Cache 内的全部内容。 // * 先清除垃圾引用,再清空映射。 // */ // public void clearCache() { // // 清除垃圾引用 // cleanCache(); // // 清空映射 // StudentRefs.clear(); // // 手动触发垃圾回收,当前代码被注释,未执行 // //System.gc(); // // 强制调用对象的 finalize 方法,当前代码被注释,未执行 // //System.runFinalization(); // } }
-
main 方法 :
-
无限循环创建学生对象,并调用 cacheStudent 方法将其添加到缓存中。
-
-
StudentRef 内部类 :
-
继承自 SoftReference<Student> ,保存学生对象的软引用,并关联一个引用队列。
-
保存学生对象的 ID 作为标识,方便在映射中查找和移除。
-
-
cacheStudent 方法 :
-
先调用 cleanCache 方法清除垃圾引用。
-
创建学生对象的软引用,并添加到 StudentRefs 映射中。
-
-
getStudent 方法 :
-
检查缓存中是否有该学生对象的软引用,如果有则从软引用中获取。
-
如果没有或软引用已被回收,则重新创建学生对象,并添加到缓存中。
-
-
cleanCache 方法 :
-
从引用队列中取出被回收的软引用,并从 StudentRefs 映射中移除对应的键值对。
-
-
Student 类 :
-
表示学生对象,包含学生的 ID 和姓名,以及相应的 getter 和 setter 方法。
-
通过这种方式,使用软引用实现了一个学生信息的缓存系统,在内存不足时可以自动回收缓存中的学生对象,释放内存。
3.弱引用
··弱引用的机制基本和软引用一致,区别在于弱引用所引用的对象在垃圾回收时,不管内存够不够都会被回收,jdk1.2版本之后提供了WeakReference来实现弱引用(我们平时开发不会使用,ThreadLocal中就是使用的弱引用),弱引用本身也可以使用队列进行回收,示例如下图:
··上图中我设置的jvm的堆区大小为200MB,当我把bytes置为null,之后,bytes对应的对象(数组也是存放在堆区的对象)就会只有弱引用和他关联,此时内存是足够的,由打印结果可知该数组被释放了,所以当我调用System.gc方法,让jvm帮我检测是否需要垃圾回收时,jvm回收了该数组对象,可以得出结论,由此验证了只有弱引用关联的对象在内存足够时也会被回收。
4.虚引用
··不能通过虚引用获取到对象,唯一的作用是当对象被垃圾回收时可以接收到对应的通知,ava中使用PhantomReference实现了虚引用,直接内存中为了及时知道 直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
创建一个DirectBuffer对象并向操作系统申请直接内存
在DirectBuffer的构造方法中有一个Cleaner类,该类就继承了PhantomReference,在向操作系统申请内存时,会把DirectBuffer的对象(this对象)传入其中,所以当把DirectBuffer置为null时,然后被回收的时候,Cleaner会接收到通知,把直接内存回收。
5.终结器引用
··当对象需要被回收时,终结器引用会关联对象并放置在Finalizer类的引用队列中,随后由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,第二次回收时才会被真正回收(因为此知识点基本没有现实可操作的意义,所以就不详细分析,只需知道在第一次回收时会执行finalize方法即可)。
3.垃圾回收算法
1.垃圾回收算法的评价标准
··STW:Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程,这个停止的时间就是STW
··1.吞吐量 吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量=执行用户代码时间/(执行用户代码时间 +GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
2.最大暂停时间 最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。
3.堆使用效率 不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算 法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。 一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。 不同的垃圾回收算法,适用于不同的场景。
2.标记-清除算法
··标记清除算法会分为两个阶段:
··1.标记阶段:使用可达性分析算法,从GCRoot对象开始通过引用链标记出所有存活对象。
··2.清除阶段:从内存中删除没有被标记的对象。
··优点:实现简单,第一次给存活对象做标记,第二次回收没有被标记的即可。
缺点:1.碎片化问题 由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一 个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。2.分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。
3.复制算法
复制算法的核心思想是: 1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。 2.在垃圾回收GC阶段,将From中存活对象复制到To空间。 3.将两块空间的From和To名字互换。
完整的复制算法的例子: 1.将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象。 2.GC阶段开始,将GC Root搬运到To空间 3.将GC Root关联的对象,搬运到To空间 4.清理From空间,并把名称互换
··优点:吞吐量高,只需要遍历一次存活对象放入to空间即可,比标记-清除算法少了一次遍历链表的过程,不会发生碎片化,因为复制时会将对象按顺序放入to空间中。
··缺点:每次只能使用一半的空间来创建对象,且复制对象的过程比较损耗性能,整体不如标记-清除算法。
4.标记-整理算法
··主要是对标记-清除算法的碎片化问题进行解决,主要分为两个阶段:
··1.标记阶段:使用可达性分析算法,从GCRoot对象开始通过引用链标记出所有存活对象。
··2.整理阶段:将存活对象移动到堆的一端,清理掉存活对象的内存空间。
··优点:堆的内存使用率高,所有内存都可以使用,因为会将对象内存进行移动,所以不会发生碎片化。
··缺点:整理算法有很多,比如lisp2整理算法需要对整个堆区的对象进行三次搜索,整体性能不佳。
5.分带垃圾回收算法(分带GC--现在使用的算法)
···分带GC就是将上述的垃圾回收算法组合进行使用,将堆区内存分为新生代和老年代,如下图:
通过参数可以控制各个区的大小,如下图:
一、分代回收流程(基于 HotSpot 典型实现 )
-
对象分配与 Young GC
-
新对象优先在 Eden 区 分配(大对象 / 大数组直接进老年代,看
XX:PretenureSizeThreshold
)。 -
Eden 区满时触发Young GC(Minor GC):
-
回收 Eden + 一个 Survivor(From 区 )的垃圾对象;
-
存活对象(年龄 +1 )移到另一个 Survivor(To 区 ),From ↔ To 角色互换(“交换 survivor 区” )。
-
-
-
晋升老年代条件
-
年龄阈值:默认 15(
XX:MaxTenuringThreshold
),年龄到则晋升老年代; -
空间担保:To 区放不下 Eden + From 区存活对象时,直接进入老年代(避免对象 “无处可去” ,所以老年代空间不足时,先尝试触发 Young GC)。
关键补充
老年代空间不足时触发 Young GC,本质是想通过清理年轻代,让 “原本可能晋升老年代的对象先留在年轻代”,给老年代腾空间。但如果 Young GC 后,存活对象仍需晋升(比如年龄到阈值、To 区放不下 ),才会真正进入老年代。
-
-
老年代与 Full GC
-
老年代空间不足时,先尝试触发 Young GC(清理年轻代,腾挪空间 );
-
若 Young GC 后老年代仍不足,触发 Full GC(Major GC,回收整个堆 + 方法区 ),STW(Stop The World )时间长;
-
Full GC 后仍无法分配,抛
OutOfMemoryError: Java heap space
。
-
二、分代设计的核心价值
-
内存与性能适配
-
按对象 “存活周期” 分区(年轻代存短命对象,老年代存长命对象 ),通过
XX:NewRatio
(新生代老年代比例 )、-Xmn
(新生代大小 )等参数,适配不同应用(如 Web 应用短命对象多,调大新生代 )。
-
-
算法针对性优化
-
新生代:用 复制算法(Survivor 区的交换 ),牺牲少量内存(From/To 区 )换低碎片、高效回收;
-
老年代:用 标记 - 整理 / 标记 - 清除(依 GC 算法,如 CMS 用标记清除、G1 用标记整理 ),应对长命对象,减少复制开销。
-
-
降低 Full GC 频率
-
让大部分垃圾(短命对象 )在 Young GC 阶段被回收,仅长命对象进入老年代,减少 Full GC 触发次数,从而降低 STW 对业务的影响。
-
··分代GC为什么采用分代设计思想:方便通过调整年轻代和老年代的比例来适应不同应用程序,提高内存利用率和性能(例如当用户访问量大时,可以适当提高新生代的比例),方便新生代和老年代选择不同的算法(例如新生代选择复制算法减少碎片化,老年代选择标记清除算法提高内存利用率),而且分代设计可以只允许回收新生代,减少了FullGC的执行次数,减少了STW。
三、常见误区修正
-
“Young GC 前必触发老年代回收”:错!Young GC 是年轻代满触发,老年代满才会触发 Full GC,正常流程 Young GC 不涉及老年代回收(除非空间担保失败 )。
-
“分代就是为了用不同算法”:不全面。分代的本质是 按对象存活周期隔离,算法优化是附加收益;若对象存活周期无差异(如内存数据库场景 ),分代优势会削弱(G1/ZGC 等新 GC 弱化分代也是趋势 )。
4.垃圾回收器
一、Serial + Serial Old(单线程回收组合,使用较少,在cpu比较匮乏时使用)
1. Serial(年轻代回收器)
-
算法:复制算法(单线程串行)
-
特点:
-
优点:单 CPU 环境下吞吐量最高,简单高效。
-
缺点:多 CPU 场景下并行能力差,堆内存大时 STW 时间长。
-
适用场景:客户端程序、硬件配置较低的环境(如嵌入式设备)。
-
-
参数:
-XX:+UseSerialGC
(开启后年轻代和老年代均使用 Serial)。
2. Serial Old(老年代回收器)
-
算法:标记 - 整理算法(单线程)
-
特点:
-
主要作为 CMS 回收器的后备方案(当 CMS 失败时使用)。
-
二、ParNew + CMS(低停顿组合)
1. ParNew(年轻代回收器)
-
算法:复制算法(多线程并行)
-
特点:
-
优点:多 CPU 下停顿时间比 Serial 更短。
-
缺点:JDK9 后不再推荐,吞吐量不如 G1。
-
适用场景:JDK8 及之前与 CMS 搭配,适用于高并发请求场景。
-
-
参数:
-XX:+UseParNewGC
(年轻代用 ParNew,老年代默认 Serial Old--但jdk9之后这种组合废弃,还是得用CMS)。
2. CMS(老年代回收器)
-
算法:标记 - 清除算法(并发回收)
-
执行流程:
-
初始标记:STW,标记 GC Roots 直接关联的对象。
-
并发标记:与用户线程并行,标记所有可达对象。
-
重新标记:STW,修正并发阶段的标记变动(如对象引用新增、删除或修改)。
-
并发清除:与用户线程并行,清理死亡对象。
-
-
特点:
-
优点:停顿时间短,适合对响应速度敏感的系统(如 Web 服务)。大型的互联网系统中用户请求数 据量大、频率高的场景 比如订单接口、商品接口等
-
缺点:
-
标记 - 清除会产生内存碎片,需定期整理(
-XX:CMSFullGCsBeforeCompaction
参数控制调整N次Full GC之 后再整理。)。 -
无法处理在并发清理过程中产生的 “浮动垃圾”,可能触发 Full GC。
-
老年代内存不足时会退化为 Serial Old 单线程回收。(CMS 的核心设计目标是降低停顿时间,但其并发清理阶段无法完全保证老年代的内存连续性和可用空间。当内存分配失败时,CMS 无法通过并发方式快速整理内存,只能退化为更 “保守” 的回收器。)
-
-
-
参数:
-XX:+UseConcMarkSweepGC
(开启 CMS)。
三、Parallel Scavenge + Parallel Old(吞吐量优先组合)
1. Parallel Scavenge(年轻代回收器,JDK8默认的年轻代垃圾回收器)
-
算法:复制算法(多线程并行)
-
特点:
-
优点:专注吞吐量优化,可自动调整堆大小(自适应调节)。
-
缺点:无法控制单次停顿时间。
-
适用场景:后台任务、大数据导出,处理(无需频繁与用户交互)。
-
-
参数:
-
-XX:MaxGCPauseMillis
:设置最大停顿时间。 -
-XX:GCTimeRatio
:设置吞吐量比例。 -
-XX:+UseAdaptiveSizePolicy
:开启自适应调节。
-
2. Parallel Old(老年代回收器)
-
算法:标记 - 整理算法(多线程并行)
-
特点:
-
与 Parallel Scavenge 搭配,在多核 CPU 下效率高。
-
-
参数:
-XX:+UseParallelGC
或-XX:+UseParallelOldGC
(开启组合)。
四、回收器对比表格
回收器组合 | 年轻代算法 | 老年代算法 | 线程数 | 核心目标 | 适用场景 |
---|---|---|---|---|---|
Serial + Serial Old | 复制算法 | 标记 - 整理 | 单线程 | 简单高效 | 客户端、低配置环境 |
ParNew + CMS | 复制算法 | 标记 - 清除 | 多线程 | 低停顿 | Web 服务、交互式系统 |
Parallel Scavenge + Parallel Old | 复制算法 | 标记 - 整理 | 多线程 | 高吞吐量 | 后台任务、批量数据处理 |
五、关键注意事项
-
CMS 的碎片问题: 长期运行会产生内存碎片,可通过
-XX:CMSFullGCsBeforeCompaction=5
(5 次 Full GC 后整理一次)缓解。 -
Parallel 的自适应调节: 开启
-XX:+UseAdaptiveSizePolicy
后,JVM 会根据MaxGCPauseMillis
和GCTimeRatio
自动调整堆大小和分代比例。 -
JDK 版本差异:
-
JDK8 默认使用 Parallel Scavenge + Parallel Old。
-
JDK9 + 推荐使用 G1 回收器,逐步淘汰 ParNew 等老年代回收器。
-
六.G1垃圾回收器
1、核心架构:分区(Region)设计
-
内存结构革新
-
堆被划分为多个大小相等的 Region(默认 2048 个),每个 Region 可动态扮演 Eden、Survivor、Old 区角色。
-
Humongous Region:存储大小超过单个 Region 一半的大对象(横跨多个 Region 时称为 Humongous 对象),直接放入老年代。
-
-
Region 大小配置
-
参数:
-XX:G1HeapRegionSize
,默认值 = 堆大小 / 2048(如堆 8GB 时,每个 Region 约 4MB)。
-
2、执行流程:分代回收与混合回收
-
Young GC(年轻代回收)
-
触发条件:年轻代占用超过阈值(默认 60%)。
-
执行逻辑:
-
标记 Eden 和 Survivor 区的存活对象,按
-XX:MaxGCPauseMillis
(默认 200ms)选择回收的 Region 数量。(G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的 参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。 比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。) -
存活对象移至新 Survivor 区,年龄 + 1,达阈值(默认 15)则晋升老年代。
-
-
-
Mixed GC(混合回收)
-
触发条件:老年代占用超过
-XX:InitiatingHeapOccupancyPercent
(默认 45%)。 -
执行逻辑:
-
回收所有年轻代 Region + 部分老年代 Region(优先选存活率低的 Region,基于统计信息)。
-
目标:在控制停顿时间的前提下,逐步清理老年代。
-
-
-
Full GC(全局回收)
-
触发条件:Mixed GC 无法完成回收(如 Region 满、大对象分配失败)。
-
执行逻辑:
-
单线程标记 - 整理算法,STW 时间长(需极力避免)。
-
-
优化方向:通过调优参数(如增大堆空间、调整停顿目标)减少 Full GC 触发。
-
3、核心特性:为什么 G1 更适合大堆?
-
可预测的停顿时间
-
通过
-XX:MaxGCPauseMillis
设置目标停顿时间,G1 动态调整回收的 Region 数量,确保停顿可控。
-
-
空间整合:无碎片
-
采用标记 - 整理算法(Young GC 用复制算法),回收后存活对象压缩到连续 Region,避免碎片化。
-
-
SATB 并发标记算法
-
对比 CMS 的增量更新:
-
SATB(Snapshot At The Beginning):GC 开始时记录对象图快照,后续变化通过日志补偿,减少重新标记耗时。
-
增量更新:实时追踪引用变化,重新标记阶段需扫描更多对象。
-
-
-
分代与并行 / 并发结合
-
年轻代并行回收,老年代并发标记,充分利用多核 CPU,降低 STW 时间。
-
4、关键参数调优
参数 | 作用 | 建议值 |
---|---|---|
-XX:UseG1GC | 启用 G1 回收器 | JDK9 + 默认开启 |
-XX:MaxGCPauseMillis | 目标最大停顿时间(ms) | 100-300(根据业务响应需求调整) |
-XX:InitiatingHeapOccupancyPercent | 老年代占用率触发 Mixed GC 的阈值(%) | 40-50(大堆可适当降低,提前触发回收) |
-XX:G1HeapRegionSize | Region 大小 | 不建议手动设置,由 JVM 自动计算(堆 < 4GB 时,默认 1MB;堆增大时自动调整) |
-XX:G1MixedGCCountTarget | 一次 Mixed GC 的最大次数 | 8(控制每次混合回收的 Region 数量) |
5、优缺点对比
优点 | 缺点 |
---|---|
1. 大堆场景下停顿时间可控(如 8GB 堆停顿 < 500ms) | 1. JDK8 及之前版本不成熟,内存占用略高 |
2. 自动整理空间,无碎片问题 | 2. 并发标记阶段 CPU 占用较高(约 10% 额外开销) |
3. SATB 算法比 CMS 的增量更新更高效,重新标记时间更短 | 3. 调优参数复杂,需结合业务场景精细配置 |
4. 支持 Mixed GC,逐步清理老年代,避免 Full GC 频繁触发 |
6、G1 与 CMS 的核心区别
维度 | G1 | CMS |
---|---|---|
内存结构 | 分区(Region),动态分代 | 连续分代(Eden+Survivor+Old) |
老年代回收 | Mixed GC:回收部分老年代 + 年轻代,优先清理垃圾多的 Region | 并发清理整个老年代,易产生碎片 |
停顿控制 | 可通过参数精确控制停顿时间 | 停顿时间不可预测,大堆场景下可能较长 |
大对象处理 | Humongous Region 直接存入老年代,避免跨代分配问题 | 大对象直接进入老年代,可能触发 Full GC |
碎片化问题 | 标记 - 整理算法,无碎片 | 标记 - 清除算法,需定期整理碎片 |
7、生产环境建议
-
适用场景:
-
堆内存大于 4GB、对停顿时间敏感的服务(如微服务、电商订单系统)。
-
-
避免 Full GC:
-
监控
G1OldGenPercent
指标,当接近InitiatingHeapOccupancyPercent
时提前扩容堆空间。 -
调大
-XX:G1ReservePercent
(默认 10%,预留空间避免大对象分配失败)。
-
-
性能监控工具:
-
使用
jstat -g1gc
查看 G1 回收状态,或通过 JDK Flight Recorder 分析停顿原因。
-
垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选 择如下:
JDK8及之前: ParNew + CMS(关注暂停时间)、Parallel Scavenge + Parallel Old (关注 吞吐量)、 G1(JDK8之前不建议,较大堆并且关注暂停时间)
JDK9之后: G1(默认)
从JDK9之后,由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以 强烈建议在生产环境上使用G1。 G1的实现原理将在《原理篇》中介绍,更多前沿技术ZGC、GraalVM将在 《高级篇》中介绍。