引用
JDK1.2之前,reference类型仅仅代表数据中存储的数值代表的是另一块内存的地址。
JDK1.2之后,reference类分为强引用、软引用、弱引用和虚引用(Phantom Reference)。
强引用:传统定义的引用,例如 Object obj = new Object(); 这种引用赋值。
软引用:指一些还有用,但是非必须的对象,JDK1.2后可以用SoftReference类来实现软引用。
常用于小内存设备的 Cache 中,可以先清理掉缓存以保证不发生OOM,在后续合适的时机再将它们重新加载到 Cache 中。也常用于内存敏感的场景中。
import java.lang.ref.SoftReference; public class Test { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("softReference")); // 如果 sr 已经被回收,那么 sr.get() 返回 null,否则返回其引用 System.out.println(sr.get()); }}弱引用:同指还有用,但是非必须的对象,但是比软引用弱,JDK1.2后可以有WeakRefernece来实现弱引用。
软引用一般用于保存对象的引用而不影响其 GC 过程。常用于 Debug 和 内存监视软件之中。
JDK的Proxy将动态生成的Class实例暂存于一个由Weakrefrence构成的Map中作为Cache。
虚引用(幽灵引用、幻影引用):最弱的引用关系,不会影响被引用对象的生存时间,也无法通过虚引用获得一个对象实例,JDK1.2后可以用PhantomReference来实现虚引用。
创建虚引用时需要和一个队列关联,在虚引用所引用的对象执行 finalize() 方法后该对象被放到队列中,可以判断队列中是否有该对象判断其是否执行了回收。
引用计数法 (reference counting)
在对象中添加一个计数器,每次当一个地方引用到此对象时则计数器+1,当引用失效时则计数器-1。
计数器为0时则代表对象已经无效。
优点:原理简单,判断快速。
缺点:需要额外的处理才能保证正确工作。
public class ReferenceCountingGc { public Object instance = null; private static final int _1MB = 1024 * 1024; // Byte数组仅用来占据空间 private Byte[] bigSize = new Byte[2 * _1MB]; public static void main(String[] args) { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB.instance; objB.instance = objA.instance; // 原本的两个引用已经无效,但是原指的对象的引用计数并没有清0 objA = null; objB = null; //显示地告诉虚拟机要进行垃圾回收了,并不一定会执行回收动作。 System.gc(); }}可达性分析算法 (Reachability Analysis)
设置一系列"GC Roots"作为起始节点集,从这些节点根据引用关系向下搜索,搜索路径又称为引用链。
如果某个对象无法与GC Roots相连(直接或者间接),那么这个对象被视为无效。

可以被固定作为GC Roots的对象:
虚拟机栈(帧栈中的本地变量表)中引用的对象
public class Test { public static void main(String[] args) { // 本地变量表中的对象引用 Object obj = new Object(); // obj设为null后,原来new的对象无法到达 obj = null; }}方法区中静态属性引用、常量引用的对象
public class Test { // 静态属性引用 public static Test s; public static void main(String[] args) { Test a = new Test(); a.s = new Test(); a = null; }}本地方法中 JNI (java native interface)引用的对象
JNI简单地说就是java调用其他语言编写的程序。Java会将这些方法装入一个新的帧栈然后放入本地方法栈中,调用只是简单的动态连接而已。JVM虚拟机和本地方法之间交换数据的接口是JNI。
Java虚拟机内部的引用
被同步锁 (synchronized关键字) 持有的对象
反应Java虚拟机内部情况的JMXBeans、JVMTI中注册的回调、本地代码缓存等
根据垃圾回收器和当前回收区域的不同,还有其他对象可以“临时性”地加入GC Roots (如发生跨代引用时老年代中的对象)
可达性分析算法回收对象的时间:
可达性分析算法判断不可达的对象,并不会被立即回收,最多会进行两次标记过程后被回收。
第一次标记:
可达性分析算法在发现一个对象没有与GC Roots的引用链时,该对象被第一次标记。随后进行一次筛选,筛选的标准是此对象是否需要执行 finalize() 方法。
如果对象没有实现 finalize() 方法或者 finalize() 方法已经被虚拟机调用,那么被判断为无须执行 finalize() 方法 —— 直接进行垃圾回收。
- finalzie() 的三种调用情况:
- 显示地调用 finalize() 方法
- 在系统退出时为每个对象执行一次 finalize() 方法
- 发生 GC 时
不过 finalize() 可能会被重写,在 finalize() 中重新将对象加入到引用链中,所以实现 finalize() 方法的情况需要第二次小规模标记。
如果被判断为需要执行 finalize() 方法,那么这个对象被放在 F-Queue队列中。虚拟机会在稍后为这个F-Queue自动建立一个低优先级的线程,在这个线程中执行它们的 finalize() 方法。
第二次标记:
对象自我拯救的演示代码:
/* finalize() 方法已经被抛弃,其存在的目的时帮助c++程序员从析构函数转到Java来。 但是 finalize() 方法的开销很大,不如使用try-finally快,已经被抛弃了。*/public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed"); // 执行 finalize() 方法后拯救自己 FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); // 第一次拯救成功了 SAVE_HOOK = null; System.gc(); // finalize() 方法优先级较低,等待0.5s以执行 finalize() 方法 Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } // 下面的代码和上面的一摸一样 // 但是执行结果却是 no, i am dead :( SAVE_HOOK = null; System.gc(); Thread.sleep(500); if(SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } }}/* 运行结果: finalize method executed yes, i am alive :) no, i am dead :(*/回收方法区
方法区垃圾回收是可选择实现的,如HotSpot虚拟机中的元空间或永久代就没有垃圾回收行为。
因为方法区的回收性价比较低 —— 堆中进行一次垃圾回收可以回收 70% 到 99% 的内存。
方法区的内存回收主要有两部分:
废弃的常量:和堆中的对象回收一样,当该常量不可达时被回收。
不再使用的类型:
当满足下述三个条件时,JVM被允许对无用类进行回收:
该类所有的实例(该类、派生子类)都已经被回收。
加载该类的类加载器已经被回收。(这个条件一般难以达到)
classLoader负责将 .class 文件加载到JVM中,并生成 java.lang.Class 的一个实例。
该类对应的 java.lang.Class 对象也是不可达。
当满足这三个条件是,JVM仅仅是被允许回收,而不一定会回收。
部分收集 Partial GC : 目标不是完整收集整个Java堆的垃圾收集。
- 新生代收集 Minor GC / Young GC : 目标时新生代的垃圾收集。
- 老年代收集 Major GC / Old GC : 目标是老年代的垃圾收集。目前只有 CMS收集器会单独收集老年代。
- 混合收集 Mixed GC : 指目标是收集整个新生代和部分老年代的垃圾收集。目前只有G1收集器有这种行为。
整堆收集 Full GC : 目标是整个Java堆和方法去的垃圾收集。
分代收集理论:
当前商业虚拟机的垃圾收集器,大多都遵循了“分代收集” (Generational Collection) 的理论进行设计。
分代收集实质上是一套符合大多数程序运行实际情况的经验法则,而非理论。其建立在两个法则之上:
设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象根据其年龄 (年龄就是对象熬过垃圾回收的次数) 分配到不同的区域之中储存。Java堆划分为不同的区域之后,垃圾收集器每次可以只回收其中一部分区域。
JVM设计者至少会将Java堆划分为 新生代(Young Generation) 和 老年代(Old Generation)。新生代和老年代的内存占比为 1 : 2。
新生代:每次都有大量对象”死去“,少量存活的对象被逐步晋升到老年代中存储。
简单划分内存区域所带来的问题:
由简单划分内存区域所带来的问题而添加了第三条法则:
标记-清除算法:标记出需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。(也可以标记不需要收集的对象)
标记-复制算法:(主要应用于新生代)
半区复制 (Semispace Copying) 垃圾收集算法:将内存区域划分为相等的两部分,每次只使用其中一块,进行垃圾回收时将不需要回收的对象移到另一半内存空间中,然后将原空间全部回收。
Apple 式回收:
新生代:分为一块较大的Eden区域和两块较小的Survivor区域,每次分配内存只使用其中的Eden区域和一块Survivor区域,将其中任然存活的对象复制到另一块Survivor区域中,然后整个回收Eden区和原来的一块Survivor区域。

Eden区域和 Survivor区域的内存占比是 8 : 1 : 1。如果另一块Survivor区域放不下 Minor GC后任存活的对象时,需要借助其他区域 (老年区) 进行存储。

标记-整理算法 :(主要应用与老年代)
HotSpot虚拟机中关注吞吐量的 Parallel Old收集器是标记-整理算法的。
而关注延迟的CMS则采用了“融合”版方法。