垃圾回收主要关注堆和方法区,其他的(程序计数器虚拟机栈和本地方法栈)大体可以认为在编译器就已知分配的内存。而堆和方法区要运行起来才知道。
垃圾回收主要确定三个问题:
- 哪些内存需要被回收
- 什么时候回收
- 怎么回收
哪些内存需要被回收
-
引用计数法的缺陷
引用计数法就是给每个对象添加一个引用计数器,当一个地方已用对象时这个对象的计数器就+1,当一个引用失效时计数器-1,当计数器 为0时,代表这个对象不能再使用的。但是这个算法有一个缺陷:很难结局循环引用问题。java虚拟机不采用这种算法来判断对象是否已死。 -
JVM采用的可达性分析算法
通过一系列称为GCRoots的对象作为起始点,从这个节点向下搜索,所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链的时候,这个对象就被判定为可回收对象。在java中可作为GCRoots的对象包括
- 虚拟机栈中(栈帧中的本地变量表)引用的对象
- 方法区中类静态属性引用的对象(static)
- 方法区中常量引用的对象(final)
- 本地方法栈Native方法引用的对象
引用
从上面可知无论哪种发放判断对象是否可被回收都与引用有关,那么java中的。
java把引用分为 强引用,软引用,弱引用和虚引用
- 强引用
就是类似 Object obj = new Object()这类的引用,只要有强引用存在,垃圾收集器就永远不会回收掉被强引用的对象。 - 软引用
描述还有用但非必须对象,系统会在将要发生内存溢出之前,将会把这些对象列进回收范围进行第二次回收。 - 弱引用
描述非必须对象,强度比软引用弱,被弱引用关联的对象,只能生存到下一次垃圾回收发生之前。就是无差别回收,不管内存是否溢出。 - 虚引用
最弱的引用关系,不能通过虚引用获得一个对象的实例,虚引用也不会影响一个对象的生存时间,唯一目的是在这个对象被收集器回收时收到一个系统通知。
对象是否已死:
- 对象是否与GCRoots相连
如果没有与之相连,就看是否有必要执行finlize()方法,如果有必要就放入F-Queue中 这是一个低优先级队列,GC对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中拯救自己,只需要与引用链上任何一个对象建立关系即可。那在第二次标记时,它就会被移除即将回收的集合,如果这个时候对象还没有逃脱,就真的被回收了。
回收方法区
永久代的回收效率很低,通常主要回收两个部分 废弃常量和无用的类。
废弃常量
就是没有任何String对象引用常量池中的某常量,也没有其他地方引用了这个字面量。
废弃的类
- 类中所有的实例都已经被回收,java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经回收
- 该类的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾回收算法
标记清除算法
标记需要回收的对象,标记完成后统一回收所有被标记的对象。
存在的问题:
- 效率低
- 清除后会产生大量不连续的内存碎片。
复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块内存中去,然后再把已使用过的内存空间一次清理掉。
优点:
不用考虑内存碎片等复杂情况,实现简单运行高效。
缺点:
内存缩小为原来一半,代价高。
改进的方法就是不使用1:1分配区域
而是分为一块较大的eden区,和两块较小的suvivor区,每次回收将eden和suvivor中还存活的对象一次性复制到另一个suvivor中,然后清理刚刚的eden和suvivor区 eden:suvivor:suvivor=8:1:1 每次新生代中可用内存空间为整个新生代容量的90% hotspot中。当suvivor空间不够存放10%还活着的对象时,就要依赖老年代进行分配担保,这些对象就通过这个分配担保机制进入老年代。
标记整理算法
老年代一般不选用复制算法,根据老年代的特点提出了标记-整理算法,标记过程与标记清除算法一样,之后让所有存活的对象都向一段移动然后直接清理掉端边界以外的内存,这样做可以使回收后不存在零碎空间。
分代收集算法
把java堆分为新生代和老年代,新生代中每次垃圾回收有大批对象死去,只有少量存活对象,所以只需要付出少量存活对象的复制成本就可以完成收集,所以采用复制算法。老年代因为对象存活率高,没有额外的空间进行分配担保,就必须使用标记清除或标记整理来进行回收。
算法实现
可达性分析算法可作为GCRoot的主要是在全局性的引用(类静态属性和常量)与执行上下文(栈帧中的本地方变量表)中。另外可达性分析不可以出现在分析过程中对象的引用关系还在不断变化的情况,否则分析结果不准确。意味着枚举根节点必须要停顿java所有的执行线程。 这个事件被称为 Stop The Wordld
OopMap这个数据结构可以在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。 就不用停顿的时候一个不漏的检查所有执行上下文和全局的引用位置,只需要查看oopmap即可。
HotSpot并没有为每个指令生成Oopmap。只是在特定的位置记录这些信息
这个特定的位置就是安全点(safepoint),只有在安全点才能停顿下来开始GC。 产生安全点的地方需要满足让程序长时间执行,长时间执行的最明显特征就是指令序列复用,例如方法调用,循环,异常跳转等。
抢先式中断在GC发生时让所有线程全部中断,如果发现线程中断的地方不在安全点上,就恢复线程让它跑到安全点。几乎没有虚拟机这样实现。
主动式中断设置一个标志,线程去主动轮询这个标志,当标志为真时就主动中断挂起,轮询标志的地方和安全点重合还有创建对象需要分配内存的地方。
安全区域是指一段代码片段中,引用关系不会发生变化,在这个区域任意的开始GC都是安全的。 线程进入安全区域会标识自己已经入,离开的时候检查是否已经完成了GC过程,如果完成了旧继续执行,否则要等到收到可以离开安全区域的信号为止、
线程处于 sleep和 blocked状态的时候,程序没有执行没有CPU时间 线程无法响应JVM中断请求,所以就无法到达安全点中断挂起,JVM不可能等待这种线程被分配到时间后再到达安全点后执行根节点的枚举,于是就有了安全区域。
垃圾收集器
Serial收集器
单线程新生代收集器,它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:简单高效,对于单个CPU来说没有现成交互的开销对于Client模式下的虚拟机来说是一个很好的选择 适合单核CPU Client模式下的新生代收集器
ParNew收集器
是Serial收集器的多线程版本,Server模式下虚拟机中首选的新生代收集器,除了Serial收集器目前只有它能与CMS收集器配合工作,默认开启的收集线程与CPU的数量相同。GC线程和用户线程并发执行。
ParallelScavenge收集器
吞吐量优先
使用复制算法的收集器,也是并行的多线程收集器,表面上看与parNew无异。与其他收集器的关注点尽可能缩短用户线程停顿的时间,它的关注点是达到一个可控制的吞吐量。 无法与CMS收集配合工作。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短越适合需要与用户交互的程序,高吞吐量则可以高效率利用CPU时间 主要适合在后台运算而不需要太多交互的任务。
-XX:UseAdaptiveSizePolicy GC自适应调节策略,这个策略也是和ParNew收集器的一个重要区别。 打开这个开关,就不需要手动调节 新生代大小-Xmn Eden大小 和Suvivor区的比例,晋升老年代对象的大小 -XX:pretenureSizeThereshold 等细节了。虚拟机会根据当前系统的运行动态调节这些参数。
SerialOld收集器
是Serial收集器的老年代版本,单线程收集器,使用标记整理算法,主要是给Client模式下的虚拟机使用
Parallel Old收集器
ParallelScavenge收集器的老年代版本,使用多线程和标记整理法。与parallelScavenge配合使用达到吞吐量优先。
CMS收集器
以获得最短停顿时间为目标的收集器。重视服务器响应速度,能够给用户带来较好的体验,concurrent mark swep 所以采用的是标记清除算法。运作过程包括
- 初始标记 去标记GCRoots直接关联的对象
- 并发标记 GCRoots tracing过程标记不是直接关联的对象
- 重新标记 就是在并发标记过程中程序继续运作导致标记产生变动的对象进行标记
- 并发清除 使用标记清除去清除。
整个过程耗时最长的并发标记及和并发清除过程收集器线程都可以和用户线程一起工作,所以总体上来说 CMS收集器的内存回收过程是与用户线程一起并发执行的
优点:并发收集 低停顿
缺点:
- 对CPU资源非常敏感,占用了一部分线程而导致应用程序变慢,总吞吐量降低。
- CMS 无法处理浮动垃圾。因为cms清理阶段用户线程还在运行,伴随程序的运行还会有新的垃圾不断产生,CMS无法在当次收集中处理掉它们,只好等待下一次GC时在清理掉。导致CMS收集器不能向其他收集器那样等到老年代几乎完全被填满了再进行清理,需要预留一部分空间提供并发收集时程序运作使用。
- 因为基于标记清除算法,所以收集接受时会产生大量空间碎片为大对象分配带来麻烦。为了解决这个问题CMS收集器提供了一个 -XX:UseCMSCompactAtFullGC开关 ,用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程。这样空间碎片没有了但是停顿时间变长了,所以又提供了另一个参数-XX:CMSFullGCsBeforeCompaction 这个参数使那内存碎片合并间断性的发生。
G1收集器
面向服务端应用的垃圾收集器。
特点:
- 并行与并发:能够利用多CPU多核环境下的硬件优势,使用多个CPU来缩短停顿时间,G1收集器可以通过并发的方式让JAVA程序继续执行。
- 分代收集:不需要其他收集器配合就能独立管理整个GC堆
- 空间整合:整体来看基于标记整理算法实现,局部上是基于复制算法实现。意味着G1收集器收集过程中不会产生内存碎片
- 可预测停顿:与CMS 一样降低停顿时间是他们的关注点,但是G1更优秀的地方在于 他还能建立可预测的停顿时间模型,能让使用者明确的指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 N/M
G1之前的其他收集器进行收集的范围都是整个新生代或者老年代。 而G1收集器,将Java堆划分为大小相等的独立区域(Region)。
G1在后台维护了一个优先列表:这个列表里面存放着各个region进行垃圾回收得到的内存空间大小和回收所需要的时间,每次根据允许的时间,优先回收价值最大的Region。 Garbage-First 价值最大的region先回收,就是在给定的时间里可以回收得到最大的内存的就是优先级最高的。
以上的使用Region划分内存区域以及使用优先级回收区域的方式,保证了G1收集器的高效率。
避免全堆扫描的方法 rememberedSet
在G1收集器中 每个Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机使用rememberedSet来避免全堆扫描。
这个RememberedSet主要是 虚拟机发现程序在对Reference类型数据进行写操作时会产生一个WriteBarrier暂时中断写操作,然后检查这个Reference引用的对象是否处于不同的Region中,如果是就通过CardTabel把相关的引用信息记录到被引用对象所属的region的 rememberSet中。
就是在对引用类型进行写的时候会产生一个写屏障,然后检查这个引用的对象是否在其他的region里面,如果是的话就把这个对象的信息记录到该对象所处的region的rememberset里面。
进行内存回收的时候,在GC根节点的枚举范围中加入rememberSet既可保证不用全堆扫描 又不会有遗漏。
G1收集器的过程
- 初始标记 枚举根节点 标记GCRoots能够直接关联到的对象,需要停顿线程
- 并发标记 从GCroot开始对堆中的对象进行可达性分析找出存活对象,耗时长但是可以与用户线程并发执行
- 最终标记 修正并发标记期间因用户线程程序继续运作而导致标记产生变动的那一部分记录 ,需要停顿线程但是可并发执行。
- 筛选回收 对region回收价值进行排序,达到最优。 对每个region进行回收成本值和回收价值的排序,根据用户期望的停顿时间来执行收集
内存分配的规则
-
对象优先在Eden分配 Eden:suvivor:suvivor=8:1:1
新生代GC MinorGC 发生频繁 回收快
老年代GC FullGC 比MinorGC慢10倍以上 -
大对象直接进入老年代,可以通过参数进行这个数值,目的在于避免在Eden及两个Survivor区之间发生大量的内存复制。
-
长期存活的对象直接进入老年代
怎么识别哪些对象应放在新生代哪些对象应放在老年代中呢?虚拟机给每个对象定义了一个对象年龄计数器
如果对象在Eden出生并经过一次MinorGC仍然存活 则设Age为1.
对象在Suvivor区每熬过一次MinorGC Age+1,
年龄增加到一定程度就进入老年代。这个默认值为15 可以通过
-XX:MaxTenuringTheresHold来设置,但是实际情况不是只有达到这个值才晋升老年代。详情如下 -
动态对象年龄判断
如果在Suvivor空间中相同年龄所有对象大小总和大于Suvivor空间的一半,年龄大于或等于这个年龄的接近进入老年代。 -
空间分配担保
虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于则MinorGC是安全的。如果不大于并且HandlePromotionFailure允许担保失败,则会检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小(动态检查),如果大于则尝试进行一次有风险的MinorGC。否则就进行一次FullGC。
允许担保失败可以使FullGC不那么频繁。
网友评论