在 JVM 中需要对没有被引用的对象,也就是垃圾对象进行垃圾回收
判断对象存活有两种方式:引用计数法、可达性分析算法
引用计数法
引用计数法通过记录每个对象被引用的次数,例如对象 A 被引用 1 次,就将 A 的引用计数器加 1,当其他对象对 A 的引用失效了,就将 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内存泄漏的主要原因之一
。
强引用
软引用-内存不足回收:在即将发生内存溢出时,会将这些对象列入回收范围进行第二次回收,如果回收之后仍然没有足够的内存,则会抛出内存溢出异常
软引用通常用来实现内存敏感的缓存,例如高速缓存
使用了软引用,如果内存足够就暂时保留缓存;如果内存不足,就清理缓存
// 创建弱引用
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 开始遍历,标记所有被引用的对象,标记为可达对象,再对堆内存从头到尾遍历,回收没有标记为可达对象的对象(标记清除算法可以标记存活对象也可以标记待回收对象)
复制算法
:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活
着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
现在商用的 Java 虚拟机大多都优先采用这种收集算法去回收新生代
,如果将内存区域划分为容量相同的两部分太占用空间,因此将复制算法进行了优化
,优化后将新生代分为了 Eden 区、Survivor From 区、Survivor To 区,Eden 和 Survivor 的大小比例为 8:1:1
,每次分配内存时只使用 Eden 和其中的一块 Survivor 区,在进行垃圾回收时,将 Eden 和已经使用过的 Survivor 区的存活对象转移到另一块 Survivor 区中,再清理 Eden 和已经使用过的 Survivor 区域,当 Survivor 区域的空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖老年代进行分配担保(通过分配担保机制,将存活的对象放入老年代即可)
剩余存活对象不多
,因此现在的商业虚拟机都是用这种收集算法回收新生代标记-压缩算法
:标记过程仍然与“标记-清除”算法一样,之后将所有的存活对象压到内存的一端,按顺序排放,之后,清理边界外的内存
分代收集算法
:把 Java 堆分为新生代和老年代,这样就可以对不同生命周期的对象采取不同的收集方式,以提高回收效率
当前商业虚拟机都采用这种算法
复制算法
(存活对象越少,复制算法效率越高)标记-清除
或者是标记-压缩