三个问题:
程序计数器,虚拟机栈,本地方法栈生命周期和线程相同;栈中的栈帧随着方法进入和退出进行入栈和出栈操作。栈帧需要内存基本上在编译器可知,因此一般这几个区域的内存分配和回收都具备已知性,不多考虑回收的问题。
而堆中,一个接口的实现类需要的内存不一样,一个方法的多个分支需要的内存也不一样,只有在程序运行时才能知道分配那些内存。因此垃圾回收器关注的主要是这部分内存
1. 哪些内存需要回收
1.1 引用计数法
给对象添加一个引用器,有一个地方引用就加1,引用失效就减1;任何时刻计时器为0的对象就不被使用。
- 实现简单,效率高
- 很难解决对象之间相互循环引用的问题
- Java虚拟机没有使用这种方法
1.2 可达性分析(Reachability Analysis)算法
通过一系列被称为“GC Roots”的对象作为起始点,从节点开始搜索,搜索经过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),证明该对象不可用。
- Java虚拟机使用了这种方法
- Java虚拟机中作为GC Roots的对象包括:
- 虚拟机栈(栈帧中本地对象变量表)中引用的对象
- 方法区中静态属性应用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即Native方法)引用的对象
1.3 优化的引用
未优化的引用:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,则表示这块内存代表这一个引用。
希望的引用:当内存空间充足,留在内存中;如果内存空间在进行垃圾回收之后还是紧张,放弃这些对象。
JDK1.2之后的优化,将引用分为以下四种:
- 强引用(Strong Reference):例如"Object obj = new Object()",只要强引用存在,GC永远不会回收被引用的对象。
- 软引用(Soft Reference):用来描述还有用但非必须的对象。在OOM之前,将对象列入回收范围之中进行第二次回收,如果此次回收还是没有足够内存,才抛出OOM。有SoftReference类实现软引用
- 弱引用(Weak Reference):用来描述非必须对象,但是比软引用更弱,弱引用的对象只生存到下一次GC之前,无论GC时内存是否足够,都会被回收。有WeakReference来实现弱引用。
- 虚引用(Phantom Reference):最弱。对象是否有虚引用存在,不会影响其生存时间,也无法通过虚引用来获得对象实例。其唯一目的就是对象被GC之前收到一个系统通知。有PhantomReference来实现虚引用。
1.4 方法区的回收
- 方法区(永久代)的回收效率较低
- 方法区的垃圾回收主要在两部分:
- 废弃常量:和回收Java堆中对象很类似,以是否有引用的方式判断。
- 无用的类:同时满足三个条件:
- 该类所有的实例都已经被回收,堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象已经没有在任何地方被应用,无法再任何地方通过反射访问该类的方法
2. 何时回收
在可达性分析算法中得到的对象需要进过两次标记:
- 在可达性分析后发现没有与GC Roots的引用链,被第一次标记并筛选此对象有否必要执行finalize()方法
- 如果对象没有覆盖finalize()方法,或者finalize()方法被调用过,虚拟机都是为没有必要执行,进行回收,注意即使覆盖了finalize()方法,它也只执行一次!
不建议使用finalize():1. 不稳定,不能确定何时执行;2. 方法中的工作可以在try/finally块中进行。
3. GC算法(如何回收)
3.1 标记 - 清除(Mark-Sweep)
先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足在于:
- 效率低
- 会产生大量不连续的空间。这样账号后程序的运行需要分配较大对象时,无法找到足够的连续内存会更频繁的触发GC
3.2 复制算法(Copying)
将内存按照容量分为大小相同的两块,每次只使用其中一块,当这一块内存使用完了,将存活的对象复制到另一块上面,然后把用过的内存空间一次清掉。
3.3 标记-整理算法(Mark-Compact)
标记出所有需要回收的对象,完成后所有存活的对象往一侧移动,然后清理掉端边界以外的内存。
3.4 分代收集算法(Generational Collection)
根据对象存活周期将不同将算法划分为几块(一般分为新生代和老年代),根据年代特点选择合适的收集算法:
- 新生代中每次垃圾回收有大量对象回收,只有少量保留,就选用复制算法,只需要少量存货对象的复制成本。
- 老年代中对象存活概率高,没有额外空间进行分配担保,采用“标记-清理/整理”算法。
4. HotSpot的GC
4.1 枚举根节点
可达性分析需要用到GC Roots,存在的问题有两个:
- 方法区很大的时候,GC Roots节点检查过程需要时间
- 分析期间执行系统需要冻结
HotSpot对问题的解决(准确式GC):
使用OopMap的数据结构,在类加载完成时,把对象内多少偏移量对应着什么类型的数据,在JIT编译(just in time, 即时编译技术,将字节码编译成本机机器代码)过程中,也对特定位置记录下栈寄存器中那些位置是引用。这样,虚拟机是直接知道哪个地方存放着对象引用的。在执行系统停顿后,不需要检查完所有执行上下文和全局引用位置。
4.2 安全点
OopMap可以帮助HotSpot快而准的完成GC Roots的枚举,但是出现问题:
引起OopMap内容变化(引用关系变化)指令很多,都生成对应的OopMap会提高GC的空间成本。
所以,HotSpot只在特定位置(安全点,Safepoint)生成了OopMap。
如何选定安全点呢?
“是否具有让程序长时间执行的特征”——最明显就是指令序列复用,如方法调用,循环调转,异常跳转等功能指令会产生安全点。
如何在GC是让所有线程(不包括JNI线程——Java Native Interface,实现了Java和其他语言的通信)都等到最近的安全点再停顿呢?
- 抢先式中断(Preemptive Suspension),不需要线程的执行代码主动配合,GC时所有线程全部中断,如果线程中断点不是安全点,就恢复线程,让它运行到安全点上。没有虚拟机采用这种方式。
- 主动式中断(Voluntary Suspension),不直接对线程进行操作,设置一个标志,各线程主动轮询这个标志,发现中断标志为真就主动中断挂起。轮询标志点包括安全点和创建对象需要分配内存的点。
4.3 安全区域
安全点遇到的问题:线程处于Sleep,Blocked等状态,无法走到安全点响应JVM的中断请求。
通过安全区域(Safe Region)解决:在一段代码中,引用关系不会发生变化,在这个区域中任意地方GC都是安全的。
- 线程执行到这些代码,先标记自己进入了Sage Region。
- JVM在GC时不处理标记为Safe Region状态的线程。
- 线程离开Safe Region时检查JVM是否完成了根节点枚举(或者GC全过程),如果完成了。线程继续执行,否则等待知道收到可以安全离开Sage Region信号为止。