垃圾回收
在 JVM 中需要对没有被引用的对象,也就是垃圾对象进行垃圾回收
对象存活判断算法
判断对象存活有两种方式:引用计数法、可达性分析算法
引用计数法
引用计数法通过记录每个对象被引用的次数,例如对象 A 被引用 1 次,就将 A 的引用计数器加 1,当其他对象对 A 的引用失效了,就将 A 的引用计数器减 1
- 优点:
- 实现简单,判定效率高
- 缺点:
- 需要单独的字段存储计数器,增加存储空间开销
- 每次赋值都要更新计数器,增加时间开销
- 无法处理循环引用的情况,致命问题!即 A 引用 B,B 引用 A,那么他们两个的引用计数器永远都为 1
可达性分析算法
可达性分析算法可以有效解决循环引用的问题,Java 选择了这种算法
可达性分析算法以根对象集合(GC Roots)
为起使点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
,通过可达性分析算法分析后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索过程所走过的路径称为引用链
,如果目标对象没有任何引用链
相连,则是不可达的,就可以标记为垃圾对象
GC Roots 主要包含以下几类元素:
-
虚拟机栈中引用的对象
如:各个线程被调用的方法中所使用的参数、局部变量等
-
本地方法栈内的本地方法引用的对象
-
方法区中引用类型的静态变量
-
方法区中常量引用的对象
如:字符串常量池里的引用
-
所有被
synchronized
持有的对象 -
Java 虚拟机内部的引用
如:基本数据类型对应的 Class 对象、异常对象(如 NullPointerException、OutOfMemoryError)、系统类加载器
垃圾回收过程
在 Java 中对垃圾对象进行回收需要至少经历两次标记过程:
- 第一次标记:如果经过可达性分析后,发现没有任何引用链相连,则会第一次被标记
- 第二次标记:判断第一次标记的对象是否有必要执行
finalize()
方法,如果在finalize()
方法中没有重新与引用链建立关联,则会被第二次标记
第二次被标记成功的对象会进行回收;否则,将继续存活
对象的 finalization 机制:
Java 提供了 finalization
机制来允许开发人员 自定义对象被销毁之前的处理逻辑
,即在垃圾回收一个对象之前,会先调用这个对象的 finalize()
方法,该方法允许在子类中被重写,用于在对象被回收时进行资源释放的工作
对象引用
在 JDK1.2 之后,Java 对引用的概念进行了扩张,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱
-
强引用-不回收:强引用是最普遍的对象引用,也是默认的引用类型,强引用的对象是可触及的,垃圾回收器永远不会回收被引用的对象,因此
强引用是造成Java内存泄漏的主要原因之一
。- 当使用new操作创建一个新对象时,并且将其赋值给一个变量时,这个变量就成为该对象的一个
强引用
- 当使用new操作创建一个新对象时,并且将其赋值给一个变量时,这个变量就成为该对象的一个
-
软引用-内存不足回收:在即将发生内存溢出时,会将这些对象列入回收范围进行第二次回收,如果回收之后仍然没有足够的内存,则会抛出
内存溢出异常
-
软引用通常用来实现内存敏感的缓存,例如
高速缓存
使用了软引用,如果内存足够就暂时保留缓存;如果内存不足,就清理缓存// 创建弱引用 SoftReference<User> softReference = new SoftReference<>(user); // 从软引用中获取强引用对象 System.out.println(softReference.get());
-
-
弱引用-发现即回收:被弱引用关联的对象只能存活在下一次垃圾回收之前,在垃圾回收时,无论空间是否足够,都会会受掉被弱引用关联的对象
-
弱引用常用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的
isEnQueued
方法判断对象是否被垃圾回收器标记Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; // System.gc(); // 有时候会返回null Object o = wf.get(); // 返回是否被垃圾回收器标记为即将回收的垃圾 boolean enqueued = wf.isEnqueued(); System.out.println("o = " + o); System.out.println("enqueued = " + enqueued);
-
-
虚引用:垃圾回收时,直接回收,无法通过虚引用获取对象实例
-
为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾回收时收到一个系统通知
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj, new ReferenceQueue<>()); obj=null; // 永远返回null Object o = pf.get(); // 返回是否从内存中已经删除 boolean enqueued = pf.isEnqueued(); System.out.println("o = " + o); System.out.println("enqueued = " + enqueued);
-
垃圾清除算法
GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
-
标记-清除算法
:在标记阶段,从 GC Roots 开始遍历,标记所有被引用的对象,标记为可达对象,再对堆内存从头到尾遍历,回收没有标记为可达对象的对象(标记清除算法可以标记存活对象也可以标记待回收对象)- 这里并不是真正清除,而是将清除对象的地址放在空闲的地址列表中
- 缺点
- 效率不高
- GC 时需要停止整个应用进程,用户体验不好
- 会产生内存碎片
-
复制算法
:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活
着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉现在商用的 Java 虚拟机大多都优先采用这种收集算法去回收新生代
,如果将内存区域划分为容量相同的两部分太占用空间,因此将复制算法进行了优化
,优化后将新生代分为了 Eden 区、Survivor From 区、Survivor To 区,Eden 和 Survivor 的大小比例为8:1:1
,每次分配内存时只使用 Eden 和其中的一块 Survivor 区,在进行垃圾回收时,将 Eden 和已经使用过的 Survivor 区的存活对象转移到另一块 Survivor 区中,再清理 Eden 和已经使用过的 Survivor 区域,当 Survivor 区域的空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖老年代进行分配担保(通过分配担保机制,将存活的对象放入老年代即可)- 优点
- 实现简单,运行高效
- 复制之后,保证空间的连续性,不会出现“内存碎片”
- 缺点
- 存在空间浪费
- 应用场景
- 在新生代,常规的垃圾回收,一次可以回收大部分内存空间,
剩余存活对象不多
,因此现在的商业虚拟机都是用这种收集算法回收新生代
- 在新生代,常规的垃圾回收,一次可以回收大部分内存空间,
- 优点
-
标记-压缩算法
:标记过程仍然与“标记-清除”算法一样,之后将所有的存活对象压到内存的一端,按顺序排放,之后,清理边界外的内存- 优点
- 解决了标记-清除算法出现内存碎片的问题
- 解决了复制算法中空间浪费的问题
- 缺点
- 效率上低于复制算法
- 移动对象时,如果对象被其他对象引用,则还需要调整引用的地址
- 移动过程中,需要暂停用户应用程序。即 STW
- 优点
-
分代收集算法
:把 Java 堆分为新生代和老年代,这样就可以对不同生命周期的对象采取不同的收集方式,以提高回收效率当前商业虚拟机都采用这种算法
- 新生代中的对象生命周期短,存活率低,因此适合使用
复制算法
(存活对象越少,复制算法效率越高) - 老年代中对象生命周期长,存活率高,回收没有新生代频繁,一般使用
标记-清除
或者是标记-压缩
- 新生代中的对象生命周期短,存活率低,因此适合使用