1.如何判断对象是否存活?
1.1引用计数算法
基本思路:
- 在对象中添加一个引用计数器
- 每当有一个地方引用它的时候,计数器就加
+1
- 每当有一个引用失效的时候,计数器就减
-1
- 当计数器的值为
0
的时候,那么该对象就是可被GC回收的垃圾对象
存在的问题:对象循环引用
a
对象引用了 b
对象,b
对象也引用了 a
对象,a
、b
对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是 0
,所以引用计数算法就无法回收它们。
1.2. 可达性分析算法
又称为根可达性算法
基本思路:
过定义了一系列称为“GC Roots”的根对象作为起始节点集,从 GC Roots 开始,根据引用关系往下进行搜索,查找的路径我们把它称为 "引用链" 。当一个对象到 GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象。
可达性分析算法也是JVM 默认使用的寻找垃圾算法。
例如:
Object 6
、Object 7
、Object 8
彼此之前有引用关系,但是没有与"GC Roots"相连,那么就会被当做垃圾所回收。
2.Java 中的四种引用类型
无论上述哪种算法,我们都可以看到引用在其中的重要作用。
2.1.强引用(Strong Reference)
强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,JVM 即使
抛出OOM错误,使程序异常终止,也不会随意回收具有强引用的对象来解决内存不足的问题。
Object strongReference = new Object();
如果强引用对象不使用时,需要弱化从而使GC
能够回收。
弱化方式1
显式地设置strongReference
对象为null
,gc
认为该对象不存在引用,这时就可以回收这个对象。
注意:具体什么时候收集取决于GC
算法, 例如,strongReference
是全局变量时,就需要在不用这个对象时赋值为null
,因为强引用不会被垃圾回收。
strongReference = null;
弱化方式2:
让对象超出作用域范围。在一个方法的内部有一个强引用,这个引用保存在VM Stack
栈中(GC Root
),而真正的引用对象(Object
)保存在堆中。当这个方法运行完成后,就会退出方法栈,则这个对象会被回收。
public void test() {
Object strongReference = new Object();
// 省略其他操作
}
2.2. 软引用(Soft Reference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可用来实现内存敏感的高速缓存。
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);// 访问软引用
softReference.get();
当内存不足时,JVM
首先将软引用中的对象引用置为null
,然后通知垃圾回收器进行回收:
public class Demo01 {public static void main(String[] args) {//强引用String strongReference = new String("abc");String str = new String("just do do");//弱引用,进行保护,内存充足不回收。SoftReference<String> softReference = new SoftReference<>(str);str = null;// Notify GCSystem.gc();try {byte[] buff1 = new byte[900000000]; // 内存充沛
// byte[] buff2 = new byte[900000000];
// byte[] buff3 = new byte[900000000];
// byte[] buff4 = new byte[900000000]; // 内存不足} catch (Error e) {e.printStackTrace();}System.out.println(strongReference);System.out.println(softReference.get()); // just do do 或 null}
}
内存充足:
内存不足:
上述案例可以看出:强引用与软引用在内存不足时的回收情况。
2.3. 弱引用(Weak Reference)
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
创建弱引用,使用WeakReference
:
public static void main(String[] args) {String str = new String("abc");WeakReference<String> weakReference = new WeakReference<>(str);str = null;System.gc();// 一旦发生GC,弱引用一定会被回收System.out.println(weakReference.get());//结果为null}
2.4. 虚引用(Phantom Reference)
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收。
虚引用,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。
3.垃圾收集算法
2.1. 分代收集理论
目前主流JVM虚拟机中的垃圾收集器,都遵循分代收集理论:
- 弱分代:绝大多数对象都是朝生夕灭
- 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡
按照分代收集理论设计的“分代垃圾收集器”,所采用的设计原则:收集器应该将Java
堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。
2.2.分代存储
如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM
虚拟机就可以使用较低的频率,来对这个区域进行回收。
2.3.分代收集
垃圾收集类型划分:
- 部分收集(
Partial GC
):没有完整收集整个Java
堆的垃圾收集,其中又分为:
-
- 新生代收集(
Minor GC
/Young GC
) - 老年代收集(
Major GC
/Old GC
) - 混合收集(
Mixed GC
):收集整个新生代和部分老年代的垃圾收集。
- 新生代收集(
- 整堆收集(
Full GC
):收集整个Java
堆的垃圾收集。
2.4.具体垃圾收集算法
在Java
堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法、标记-清除算法、标记-整理算法等。
2.4.1.标记-清除算法( Mark-Sweep )
实现思路:
分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
存在的问题:
- 执行效率不稳定问题:如果执行垃圾收集的区域,大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低。
- 内存空间碎片化问题:标记清除后会产生大量不连续的碎片,空间碎片太多,会导致分配较大对象时,无法找到足够的连续空间,从而会触发新的垃圾收集动作。
黑色:可回收对象 蓝色:存活对象 白色:未使用空间
2.4.2.标记-复制算法 ( Copying )
“标记-复制”收集算法简称“复制算法”,为了解决“标记-清除”面对大量可回收对象时执行效率低下的问题。
实现思路:
该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把已使用的空间一次清理掉。
存在的问题:
- 对象存活率较高,需要进行较多的内存间复制,效率降低
- 浪费过多的内存,使现有的可用空间变为原先的一半
2.4.3. 标记-整理算法 ( Mark-Compact )
实现思路:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向内存空间一端移动, 然后直接清理边界以外的内存,这样清理的机制,不会像标记-清除那样留下大量的内存碎片。
综上所述:
当前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域选择使用合适的垃圾收集算法。
例如:Heap
堆分为新生代和老年代,这样我们就可以根据各个年代的特点,从存活几率和额外的空间中选择合适的垃圾收集算法,