目录
-
哪些内存需要回收?
-
什么时候回收?
-
如何回收?
哪些对象需要回收
引用计数算法
可达性分析算法(JVM使用)
可以作为GC Roots的对象包括以下四种:
-
虚拟机栈(本地变量表)中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI引用的对象
引用类型
-
强引用 (Object obj = new Object())
-
软引用 (有用非必须,内存不够就回收)
-
弱引用(非必须,只能生存一代,无论内存够不够都回收)
-
虚引用(追踪一个对象是否被收集器回收)
如何标记?
要真正回收一个对象,至少经历两次标记过程,若对象在进行可达性分析后发现与GC ROOTs均无相连接的引用链,它将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。稍后GC将对F-Queue中的对象进行二次标记,若对象重新与引用链上任何一个对象建立关联,二次标记时则会将它移出队列,否则就会被回收。
回收方法区
方法区主要回收两部分内容:废弃常量和无用的类。
-
该类所有的实例都已经被回收,Java堆中不存在该类的任何实例。
-
加载该类的ClassLoader已经被回收
-
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法。
垃圾收集算法
标记清除
-
效率低
-
内存碎片
复制算法
-
实现简单,运行高效
-
内存缩小为原来一半,代价太高
分代垃圾回收
Eden : Survivor From : Survivor To = 8 : 1 : 1
每次使用Eden和其中一块survivor,当回收时,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上(采用的是复制算法),最后清理掉Eden和刚才用过的Survivor空间。当另一块Survivor空间不足时,需要依赖老年代进行内存担保。直接进入老年代。老年代中采用"标记-清理"或"标记-整理"算法来进行回收。
标记-整理算法
标记存活的对象,将存活的对象都向一端移动,然后清理掉端边界以外的内存。
HotSpot的算法实现
枚举根节点
枚举根节点费时,GC停顿——一致性
准确式GC: 当stop the world后,无需一个不漏地检查完所有的上下文和全局的引用位置。
如何直接得知哪些地方存放着对象引用?
使用OopMap数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。
安全点(再看几遍,没怎么看懂)
为每一条指令生成对应的OopMap非常耗费空间,如何解决?
答:只在“特定的位置”记录这些信息,这些位置称为安全点。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。
问:如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来?
答:两种方案
-
抢先式中断:GC发生,所有线程中断,若线程不在安全点上,就恢复线程。几乎没有虚拟机使用这种方式
-
主动式中断:当GC需要中断线程的时候,不直接对线程进行操作。仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外在加上创建对象需要分配内存的地方。
安全区域
安全点并没有完美解决如何进入GC的问题,该怎么办呢?
安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入的GC安全点。但是当程序“不执行”的时候呢?当线程处于sleep或blocked状态时无法响应JVM的中断请求,怎么办?
答:使用安全区域
问:什么是安全区域
答:在一段代码片段之中,引用关系不会发生变化,这个区域中的任一点开始GC都是安全的。
垃圾收集器
Serial收集器
是虚拟机运行在Client模式下的默认新生代收集器。简单而高效,对于限定单个CPU的环境来说,它没有现成交互的开销,效率很高。桌面应用收集的垃圾一般很小,停顿时间在几十毫秒左右,可以接受。
新生代采用复制算法(需要STW),老年代采用标记-整理算法(需要STW)。
ParNew收集器
是Serial收集器的多线程版本。其他没什么区别。在单CPU的环境中并不适用。
Parallel Scavenge收集器
多线程,与ParNew收集器不同点在于:其他收集器的关注点是尽可能地缩短垃圾收集时用户线程停顿的时间。
关注更高的吞吐量,吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集收件)。让吞吐量更高。
可以高效地利用CPU,尽快完成程序运算任务,适合不需要太多交互的任务。
Serial old收集器
是Serial收集器的老年版,单线程,Client模式。是CMS收集器的后备预案。
Parallel Old收集器
吞吐量优先,新生代的Parallel Scavenge收集器
CMS收集器
-
初始标记(STW)
标记一下GC Roots能直接关联到的对象,速度很快。
-
并发标记
进行GC Root Tracing
-
重新标记(STW)
修正并发表及期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长于初始标记,短于并发标记。
-
并发清除,使用“标记-清除”算法。为什么用标记清除算法?CMS主要关注低延迟,因而采用并发方式,清理垃圾时,应用程序还在运行,如何采用压缩算法,则涉及到要移动应用程序的存活对象,此时不停顿,是很难处理的,一般需要停顿下,移动存活对象,再让应用程序继续运行,但这样停顿时间变长,延迟变大,所以CMS采用清除算法。
缺点:
-
对CPU资源非常敏感。=>增量式并发收集器
-
基于“标记-清除”算法实现,产生大量空间碎片。
G1收集器
G1收集器的运作大致划分为以下几个步骤
-
初始标记(STW),和CMS一样
-
并发标记:对堆中对象进行可达性分析,找出存活对象。
-
最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。
-
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
特点
-
并行和并发:能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW的停顿时间。
-
分代收集
-
空间整合
-
可预测的停顿。如何实现的?因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的手机时间,优先回收价值最大的Region(这也是G1名称的来由)。这种使用Region划分空间内存以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden去没有足够空间进行分配时,虚拟机将发起一次Minor GC。
Major GC的速度一般会比Minor GC慢10倍以上
大对象直接进入老年代
设置PretenureSizeThreshold即可,大于该阈值的对象都会直接在老年代分配,不过这个参数只对Serial和ParNew两款收集器有效。
长期存活的对象将进入老年代
GC分代年龄,保存在对象头中。默认Age>=15岁时,把它晋升到老年代中。对象晋升老年代的年龄阈值可以通过参数
-XX: MaxTenuringThreshold设置
动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若成立,则此次Minor GC安全。反之,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。允许,就检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,大于则执行Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,则改为Full GC。