jvm垃圾收集器与内存分配策略
1、对象是否死亡
1.1 引用计数算法
给对象中添加一个引用计数器,每当有一个地方没引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。优点:实现简单,判定效率也很高,在大部分情况下他都是一个不错的算法。缺点:很难解决对象之间的相互循环引用的问题,所以很多虚拟机没有选用这种算法。
1.2 可达性分析算法
在主流的商用程序语言的主流实现中,都是通过可达性分析来判断是否存活的。基本思想就是通过一系列的成为“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表) 中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(一般说的Native方法)引用的对象
引用:
强引用:在程序代码中普遍存在的,类似Object obj = new Object();这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出溢出异常。
弱引用:也是描述非必需对象的,比弱引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用引用关联的对象。
虚引用:最弱的一种引用,是否有需引用的存在,完全不会对其生存时间构成影响,也无法通过需引用来取得一个对象实例。设置的目的就是能在这个对象被收集器回收时收到一个系统通知。
1.3 回收方法区
方法区(或者HotSpot虚拟机中的永久代)。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常类似。例子:字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象是叫做"abc"的,就是没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用了这个字面量,这时发生内存回收并且必要,就会被系统清理出常量池。
无用的类的判断:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2、垃圾收集算法
2.1 标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。不足:效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除 算法示意图
2.2 复制算法
为了解决效率问题,“复制”收集算法出现,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存快用完了,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉。每次都是对整个半区进行回收,也不用考虑内存碎片等复杂问题,实现简单,运行高效。代价时将内存缩小了原来的一半,在对象存活率较高时要进行较多的复制操作。现在的商业虚拟机都是采用这种算法来回收新生代。
复杂算法 示意图
2.3 标记- 整理算法(老年代)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存。
标记- 整理算法 示意图
2.4 分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法。根绝对象存活周期把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率搞、没有额外的空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
3、垃圾收集器
HotSpot虚拟机的垃圾收集器
如果两个收集器之间存在连线,就说明他们可以搭配使用。虚拟机所处的区域,则表示他是属于新生代收集器还是老年代收集器。
3.1 Serial 收集器
单线程收集器:不仅仅只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的时在它进行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束,Stop The World。
Serial/Serial Old 收集器运行示意图
依然是虚拟机运行在Client模式下的默认新生代收集器,优点:简单高效(与其他收集器的单线程比)对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然获得最高的单线程收集效率。
3.2 ParNew收集器
ParNew收集器其实是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器完全一样。
ParNew/Serial Old 收集器 运行示意图
是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有他能与CMS收集器配合工作。
3.3 Paraller Scanvenge 收集器
一个新生代收集器,使用复制算法的收集器,又是并行的多线程收集器,特点是他的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Paraller Scanvenge收集器的目标是达到一个空控制的吞吐量,所谓吞吐量就说CPU用于运行用户代码的时间与CPU总消耗的时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾手机时间)。
3.4 Serial Old 收集器
老年代收集器,单线程收集器,使用“标记-整理“算法,主要意义也是在给点Client模式下的虚拟机使用。
Serial/Serial Old 收集器运行示意图
Paraller Scavenge/Paraller Old 收集器运行示意图
3.5 CMS收集器
一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现的。他的运作相对于前面几种收集器来说更复杂,整个过程分为4个步骤:
初始标记
并发标记
重新标记
并发清除
初始标记、重新标记这两个步骤仍然需要“Stop The World",初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器运行示意图
优点:并发收集、低停顿。
缺点:CMS收集器对CPU资源非常敏感
CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure 失败而导致另一次Full GC的产生
大量碎片
3.6 G1收集器
G1是一款面向服务端应用的垃圾收集器。特点:
并行与并发:能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来所缩短停顿时间。
分代收集
空间整合:整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,无论如何都不会产生内存空间碎片
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时Java的垃圾收集器的特征了。
G1的运作步骤:
初始标记、并发标记、最终标记、筛选回收
5 内存分配与回收策略
5.1 对象优先在eden区分配
目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
Minor Gc和Full GC 不同:
新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
5.2大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
为什么要这样呢?
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
5.3长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
5.4 动态对象年龄判定
为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。