第3章 垃圾收集器与内存分配策略

3.1 概述
垃圾收集(Garbage Collection,GC)。
当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此中几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

3.2 对象已死吗
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。

3.2.1 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

3.2.2 可达性分析算法
商用的程序语言主流实现都是称通过可达性分析来判定对象是否存活的。
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包括:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.2.3 再谈引用
引用分类:
1)强引用
在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用
2)软引用
一些还有用但并非必需的对象。
在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
3)弱引用
非必需对象。
只能生存到下一次垃圾收集发生之前。
4)虚引用
唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3.2.4 生存还是死亡
要真正宣告一个对象死亡,至少要经历两次标记过程。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
有必要执行finalize()方法的对象放在F-Queue队列中。
finalize()方法是对象逃离死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记。
*对象可以在被GC时通过finalize()方法自我拯救
*这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次

3.2.5 回收方法区
方法区(HotSpot虚拟机中的永久代)。
垃圾收集主要回收两部分内容:废弃常量和无用的类。
“无用的类”:满足下面3个条件:
1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
2)加载该类的ClassLoader已经被回收
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.3 垃圾收集算法

3.3.1 标记-清除算法
分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
两个不足:
1)效率问题,标记和清除两个过程的效率都不高
2)空间问题,标记清除之后会产生大量不连续的内存碎片

3.2.2 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
用复制算法来回收新生代。将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。

扫描二维码关注公众号,回复: 1749398 查看本文章

3.3.3 标记-整理算法
标记-整理算法,标记过程依然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

3.3.4 分代收集算法
分代收集,根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。

3.4 HotSpot的算法实现

3.4.1 枚举根节点
作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如果要逐个检查这里面的引用,必然会消耗很多时间。
可达性分析工作必须在一个能确保一致性的快照中进行,这点是导致GC进行时必须停顿所有Java执行线程。
准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。

3.4.2 安全点
开始GC,程序在执行时只有到达安全点才能暂停。

3.4.3 安全区域
扩展了的安全点。

3.5 垃圾收集器
这里写图片描述

3.5.1 Serial收集器
这里写图片描述

对于运行在Client模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本。
这里写图片描述

运行在Server模式下的虚拟机中首选的新生代收集器。

3.5.3 Parallel Scavenge收集器
新生代收集器,使用复制算法、并行的多线程收集器,和ParNew类似。
但是关注点为达到一个可控制的吞吐量,而不是尽可能地缩短垃圾收集时用户线程的停顿时间。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

3.5.4 Serial Old收集器
Serial Old是Serial收集器的老年代版本。

3.5.5 Parallel Old收集器

3.5.6 CMS收集器
一种以获取最短回收停顿时间为目标的收集器。很大一部分的Java应用集中在互联网站或者B/S系统的服务端上。
这里写图片描述
并非收集、低停顿。

3.5.7 G1收集器
收集器技术发展的最前沿成果。
G1收集器的运作大致可划分为以下几个步骤:
1)初始标记: 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
2)并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
3)最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记变动的那一部分标记记录。
4)筛选回收:对各个Region的回收价值和成本进行排序根据用户所期望的GC停顿时间来制定回收计划。
这里写图片描述

3.5. 8 理解GC日志

3.5.9 垃圾收集器参数总结

3.6 内存分配与回收策略

3.6.1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区域没足够空间进行分配时,虚拟机将发起一次Minor GC。

3.6.2 大对象直接进入老年代
典型大对象(很长的字符串以及数组)。大对象直接在老年代分配,避免Eden区及两个Survivor区之间发生大量的内存复制。

3.6.3 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每”熬过“一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

3.6.4 动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到要求的年龄。

3.6.5 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机查看是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者不允许冒险,那这时也要改为进行一次Full GC。

3.7 本章总结

猜你喜欢

转载自blog.csdn.net/u010819416/article/details/80220251
今日推荐