判定对象是否存活
引用计数法
- 优点
- 简单,容易实现
- 缺点
- 无法解决循环引用的问题,代码如下
- 当r1 r2 的实例仅仅是互相引用的时候,如果有引用计数器他们不会被回收,而看gc日志,很明显会被回收。
存在的问题源码123456789101112131415161718192021public class ReferencingCountingGC {public static int _1M = 1024 * 1024;public Object instance = null;private byte[] value = new byte[2 * _1M];public static void main(String[] args) {ReferencingCountingGC r1 = new ReferencingCountingGC();ReferencingCountingGC r2 = new ReferencingCountingGC();r1.instance = r2;r2.instance = r1;r1 = null;r2 = null;System.gc();}}
[GC (System.gc()) [PSYoungGen: 11055K->1351K(38400K)] 11055K->1359K(125952K), 0.0025286 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1351K->0K(38400K)] [ParOldGen: 8K->1241K(87552K)] 1359K->1241K(125952K), [Metaspace: 3370K->3370K(1056768K)], 0.0081339 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
可达性分析算法
原理
- 当一个对象到GC ROOTS 没有任何引用链相连时,则证明此对象不可用。
- 即上文讲的r1 r2 互相关联,但是他们并没有被GC ROOTS关联,所以他们会被回收。
GC ROOTS的对象
- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象
- 方法去中常量引用的对象。
- 本地方法栈中JNI(即一般说的native方法)引用的对象。
问题
上文的r1 r2 本身也是虚拟机栈中的引用,本身可以成为gc roots,那么对象为什么还会被回收呢。回答
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。例如说,这些引用可能包括: - 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
- VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
- JNI handles,包括global handles和local handles
- (看情况)所有当前被加载的Java类
- (看情况)Java类的引用类型静态变量
- (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
- (看情况)String常量池(StringTable)里的引用
注意,是一组必须活跃的引用,不是对象。
活跃很重要,r1 r2 是非活跃的所以没有被划分为gc roots
gc roots 详细解释
关于引用
- 强引用:只要强引用还存在就不会被回收
- 软引用(SoftReference):在内存即将溢出之时,对这些对象做二次回收,如果还没有足够内存才会内存溢出异常。
- 弱引用(WeakReference): 之前做jdk动态代理,weak cache的时候有接触,下一次gc之前被回收
- 虚引用(PhantomReference): 一个对象是否有虚引用的存在,不会对生存周期构成影响,也无法通过虚引用来取得一个对象实例。对一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
关于finalize
- 不可达的对象,要经历两次标记过程。
- 实现finalize方法且没有被调用的对象,会被放置在一个F-Queue的队列(由虚拟机自动建立,低优先级的Finalizer线程去执行)之中,但是不保证会执行完成(可能会含有死循环等异常。)
- 不推荐使用finalize方法
回收方法区
- 废弃常量
- 与堆类似
- 废弃类
- 所有实例已被回收。
- classloader已被回收。
- 对应的Class没有任何地方被引用,无法再任何地方通过反射访问该类。(用spring这条基本都是不符合了吧。。。)
- 满足上述条件,仅仅是可回收。
垃圾回收算法
- 标记-清除算法(统一标记,统一删除)
- 效率不高。(GC ROOTS标记的是存活对象,查找死亡对象还需一次遍历?)
- 容易产生内存碎片。
- 复制算法(将内存分为两部分,每次只使用其中的一块,gc时将存活的对象复制到另外一块上去,清空原来的内存空间)。
- 每次只能使用一半的内存
- 虚拟机通过这个算法来回收新生代,分为eden : survivor1 : survivor2 = 8:1:1 (98%的对象在回收时都会被回收,每次将 eden + survivor 复制到 另一个survivor,如果超过了10%则需要老年代来做担保)。
- 标记-整理算法(存活的对象都向一端移动,避免碎片)
- 不需要额外的空间
分代收集算法
- 新生代 存活率低 适合复制算法
- 老年代 存活率高 适合标记-整理 或者 标记-清理算法。
安全点 & 安全区域
- 不能再所有地方
- 程序运行到safe point才能进行gc
- 为防止blocking等长时间没有分配cpu的情况
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
可以把oopMap简单理解成是调试信息。 在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。
对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,所以会缺少OopMap信息。那么GC碰到这样的栈帧该如何维持准确性呢?
HotSpot的解决方法是:所有经过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI需要调用Java API的时候也必须自己用句柄包装指针。在这种实现中,JNI方法里写的“jobject”实际上不是直接指向对象的指针,而是先指向一个句柄,通过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不需要扫描它的栈帧了——只要扫描句柄表就可以得到所有从JNI方法能访问到的GC堆里的对象。
但这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是导致JNI方法的调用比较慢的原因之一。
垃圾收集器
serial收集器
- 采用复制算法
- 仅使用一个线程或者一个cpu区完成垃圾收集工作。
- 垃圾收集时会暂停其他所有的工作线程。
- 适用于client端(应该说是单CPU情况下),简单高效
ParNew收集器
- 采用复制算法
- 多线程的Serial收集器
- 除Serial之外仅有的能与CMS收集器配合工作的收集器
- 随着cpu的增加性能会好于Serial收集器(垃圾收集可看做是计算密集型,线程切换会有开销)
Parallel Scavenge
- 采用复制算法
- 多线程收集
- 目的:达到可控制的吞吐量(吞吐量=用户运行时间/(用户运行时间+垃圾收集时间)),简而言之就是减少停顿
- 参数可调:GCTimeRatio & MAXGCPauseMillis ,可自适应(GC停顿时间是牺牲吞吐量和新生代空间换来的,新生代越小收集的越快,但是系统损耗的占比越高,其实收集效率越低)
SerialOld 收集器
- Serial老年版
- 与 Parallel Scavenge 可以一起使用
Parallel Old收集器
- Parallel Scavenge 老年版
- 标记整理算法
- 在注重吞吐量和CPU资源敏感的场合可以考虑使用。
CMS
- 目标-减少停顿时间
- 初始标记(仅遍历GC ROOTS,速度快)
- 并发标记(GC ROOTS TRACING,速度慢)
- 重新标记(修正并发标记产生变更的部分)
- 并发清楚(清除数据,速度慢)
- 并发标记和并发清楚可以与用户线程一起执行,停顿就会减少
- 缺点:
- CMS在并发标记和并发清楚的时候会占用cpu
- 并发清理阶段产生的新垃圾无法及时清楚,可能会引起再一次的full gc(因为应用线程同时执行,所以要预留空间,cms触发要比其他收集器早)
- 基于标记-清理的内存碎片的问题。
G1
- TODO
其他
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代(版本号,默认15)
- survivor空间中相同版本的所有对象大小总和大于Survivor控件的一半时,直接进入老年代