一、 如何确定堆中对象是否“存活”?
1. 引用计数算法
给对象添加一个引用计数器,当有一个地方引用它,计数器加一;引用失效计数器减一,当计数器为0则对象不可能再被使用。
但是,Java语言并没有选用引用计数法来管理内存,主要原因时它很难解决对象之间相互循环引用的问题
2. 根搜索算法(GC Roots Tracing)
通过一系列名为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(图论也就是不可达),则证明此对象是不可用的。
可作为GC Roots对象的:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中的类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Native方法)引用的对象
二、引用
1. 强引用StrongReference
Object obj = new Object();
只要强引用还在,GC永不会回收掉该对象
2. 软引用SoftReference
还有用,但并非必须的对象
当系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收,如果内存依然不够,则抛出异常
3. 弱引用WeakReference
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论下次GC内存是否足够
4. 虚引用PlantomReference
一个对象是否有虚引用存在,完全不会影响其生存时间,也无法通过虚引用得到该对象的实例。唯一作用是能够得到这个对象被GC时收到一个系统通知。
三、清除过程
1)根搜索算法中不可达的对象,被清除至少需要经历两次标记过程:某对象无引用链,它会被第一次标记并进行一次筛选,(如果该对象没有覆盖finalize()方法或finalize()方法已经被虚拟机调用过,这两种情况都没有必要执行),如果有必要执行,则存入F-Queue队列。
2)稍后GC对队列中对象进行二次标记,如果该对象在finalize()中重新被调用,则移出队列,否则被GC。
四、回收方法区
方法区在(HotSpotJVM中又称为永久代),其GC效率低,主要回收:废弃常量和无用的类。
1. 废弃常量
与JVM堆类似,无引用则发生回收
2. 无用的类
需要满足:
1)该类的所有实例都已经被回收,即JVM堆中不存在该类实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
五、垃圾收集算法
1. 标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。其是基于之前的引用计数法实现的。
缺点:一是效率问题,标记和清除过程的效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2. 复制算法(多用于新生代,但是升级版本)
将可用内存按容量划分为大小相等的两块,每次使用一块。当一块使用完后将存活的对象复制到另一块上,在对使用过的内存空间一次性清理掉。
优势:内存分配时不用考虑内存碎片等复杂情况,只需推动堆顶指针,按顺序分配内存即可。
缺点:内存缩小为原来一半,代价太高
3. 标记-整理算法(多用于老年代)
起初标记过程与“标记-清除”算法一致,后续并不直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。(解决了内存碎片问题)
4. 分代收集算法
根据对象的存活周期不同将内存划分,一般讲Java堆分为新生代和老年代,根据特点对症施药。新生代,更新换代快用复制算法;老年代,则使用“标记-清除”或“标记-整理”算法
六、垃圾收集器
1. Serial收集器
单线程收集器,进行垃圾收集时,必须暂停其他所有的工作进程
优势:简单高效,没有线程交互的开销,可获得最高的单线程收集效率,是JVMClient模式下默认的新生代收集器。当JVM管理内存不大时,停顿时间可控制几十毫秒最多一百多毫秒,可以接受。
2. ParNew收集器
多线程收集器,Serial升级版,进行垃圾收集时,必须暂停其他所有的工作进程
优势:是运行在Server模式下的JVM首选的新生代收集器,当可使用的CPU数量增加时,系统资源的利用比较好
3. Parallel Scavenge
多线程收集器,但它的关注点不再是尽可能的缩短垃圾收集时用户线程的停顿时间,而是达到一个可控制的吞吐量;也就是尽可能的提高CPU的有效运行时间,减少垃圾收集时间的占比。
优势:适用于在后台运算而不需要太多交互的任务,而且其还有动态调整参数的自适应调节手段
4. Serial Old
Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法。主要在Client模式下使用,如果在Server模式下,主要用途有:1. jdk1.5及以前与Parallel Scavenge搭配使用;2. 作为CMS收集器的后备预案
5. Parallel Old
Parallel Scavenge老年代版本,使用多线程和“标记-整理”算法,从jdk1.6开始使用。“吞吐量优先”的收集器终于有了名副其实的组合Parallel Scavenge+Parallel Old
6. CMS收集器(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,多线程并发工作,收集器线程可以和用户线程一起工作。
收集过程:
1)初始标记
指示标记一下GC Roots能直接关联的对象,速度很快
2)并发标记
进行GC Roots Tracing
3)重新标记
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4)并发清除
CMS的缺点:
1)对CPU资源很敏感,很因为CPU资源的占用而导致应用程序变慢,总吞吐量降低
2)无法处理浮动垃圾,可能出现Concurrent Mode Failure导致另一次Full GC的产生;此时启用Serial Old来重新进行老年代的垃圾回收
3)采用“标记-清除”算法,会产生内存碎片
7. G1收集器
采用“标记-整理”算法,不会有内存碎片问题;并不进行新生代和老年代的全范围的收集,而是细分成多个小区域,并后台建立一个维护优先列表,优先回收垃圾最多区域,保证在不牺牲吞吐量的情况下完成低停顿的内存回收,可以在有限时间内获得最高收集效率
七、内存分配与回收策略
1. Minor GC与Full GC
Minor GC:发生在新生代,回收速度快,频繁
Full GC:发生在老年代,回收速度慢
2.内存分配过程
1)对象的内存分配优先在Eden分配
对象往往在新生代的Eden区分配,当Eden区没有足够空间时,发生一次Minor GC
2)大对象直接进入老年代
虚拟机可以设置一个参数值,让大于该参数的对象直接在老年代中分配,避免在Eden区及两个Survivor区之间发生大量的内存拷贝
3)长期存活的对象将直接进入老年代
JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor区,将对象年龄设置为1。对象在Survivor区没经历一次Minor GC,年龄加一,到一定程度后就会晋升到老年代。
4)动态对象年龄判定
JVM并不总是要求对象年龄到达某值才能晋升老年代,如果在Survivor区相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
5)空间分配担保
在发生Minor GC时,JVM会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如大于,改为直接进行一次Full GC;如果小于,查看JVM是否允许担保失败,如允许,只进行Minor GC,否则改为进行Full GC。
担保失败:
我们知道使用复制算法对新生代进行GC时,会将存活对象移入Survivor区,如果请求空间大于Survivor现有空间,则会将对象移入老年区。老年区判断剩余空间是否满足对象移入的依据是之前每一次一次移入对象的大小的平均值,如果剩余空间小于上一次移入对象大小的平均值,则JVM判定老年区空间不足,需要进行一次Full GC,这种情况就是担保失败。