第3章 垃圾收集器与内存分配策略
3.1 如何判定对象是“活着”或者“死去”?
-
引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不在被使用的对象。
缺点:很难解决循环引用问题。 -
可达性分析算法:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,这个对象就是不可用的。
可作为GC Roots的对象有:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态变量属性引用的对象。
3.方法区中常量引用的对象。
3.2 引用
Java中引用的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
在定义之下对象只有被引用或者没有被引用两种状态,但是我们希望有一些中间态的对象,当空间足够的时候保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张的话可以抛弃这些对象。所以Java中才有了,强引用,软引用,弱引用,虚引用。
3.3 垃圾收集算法
- 标记 - 清除:
首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
缺点:效率低,会产生大量不连续的内存碎片,可能会导致需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次GC。 - 复制算法:
将可用内存的容量划分为大小相等的两块,每次只用其中一块,当这一块的内存用完了,就将还活着的对象复制到另外一块上,然后再把使用过的这块清空。
缺点:内存缩减为原来的一半。
大部分虚拟机都是用这种算法,但是内存比例是按照8:1:1来分配的。即将内存分为较大的Eden空间,和两块较小的From Survivor和To Survivor空间,每次使用Eden和From Survivor。当回收时,将Eden和From Survivor中还存活的对象一次性的复制到To Survivor中,最后清理掉Eden和From Survivor。当To Survivor内存空间不够用来收集存活的对象时,需要依赖老年代进行分配担保。 - 标记 - 整理(一般针对老年代):
首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 - 分代收集算法(主流):
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
3.4 垃圾收集器
-
Serial收集器
单线程收集器,它在进行垃圾收集时必须暂停其他所有的工作线程,直到它收集结束。它是虚拟机运行在Client模式下的默认新生代收集器。
算法:复制算法 -
ParNew收集器
就是Serial收集器的多线程版本,目前只有它可以和CMS收集器配合工作。单CPU的环境中效果一定不会有Serial收集器效果好,双CPU也不一定能保证。但是CPU越多效果越好。默认开启的收集线程数与CPU数量相同。
算法:复制算法 -
Parallel Scavenge收集器
新生代的收集器,又是并行的多线程收集器,特点是它的关注点与其他的收集器不同,其他的收集器的关注点是尽可能的缩短垃圾收集时间用户线程的停顿时间。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量是CPU用户运行代码的时间与CPU总消耗时间的比值。公式:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
Parallel Scavenge收集器与ParNew还有一个重要区别就是Parallel Scavenge收集器可以设置自适应的调节策略。Parallel Scavenge收集器架构中是有收集器来进行老年代收集的,但是这个收集器的实现和Serial Old非常接近 -
Serial Old收集器
Serial Old是Serial收集器的老年代版本,同样是单线程收集器,这个收集器的主要意义在于给Client模式下的虚拟机使用,一般作为CMS收集器的后备预案。
算法:标记 - 整理 -
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器加Parallel Old收集器的组合。
算法:标记 - 整理 -
CMS收集器
老年代收集器,CMS收集器是一种以获取最短回收停顿时间为目标的收集器。优点,并发收集,低停顿。
算法:标记 - 清除
运作过程分为:初始标记,并发标记,重新标记,并发清除4个步骤。其中初始标记和重新标记这两个步骤仍然需要Stop The World。
初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快、
并发标记:就是进行CG Roots Tracing(找引用链)的过程。
重新标记:是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记的时间长,但是远低于并发标记的时间。
并发清除:回收垃圾。
缺点:1.虽然不会导致用户线程停顿,但会因为占用了一部分线程导致应用程序变慢,总吞吐量降低。2.CMS无法处理浮动垃圾(并发清除中用户线程还在运行,也就还会有新的垃圾出现,这一部分垃圾没有被标记过,无法在当次收集中处理掉,这一部分垃圾称为浮动垃圾)。正是因为清除的时候用户线程还在运行,所以需要预留一部分空间给用户线程在这期间产生的垃圾使用,如果预留的空间不够用,虚拟就就会临时启用Serial Old收集器来重新进行老年代的垃圾收集,停顿时间就很长了。3.由于算法实现的原因会产生大量的空间碎片。 -
G1收集器
面向服务端的收集器,优点:并行与并发,分代收集,空间整合,可预测停顿。它将整个java堆划分为多个大小相等的独立的区域(Region),新生代和老年代不再是物理隔离,他们都是一部分Region(不需要连续)的集合。
运作过程分为:初始标记,并发标记,最终标记,筛选回收4个步骤。
3.5 内存分配
1.对象优先在Eden分配(当Eden没有足够空间时,虚拟机发起一次Minor GC)。
2.大对象直接进入老年代。
3.长期存活的对象进入老年代(默认年龄15)。
4.如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代。
5.空间分配担保:在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那Minor GC安全。如果小于,接着查看HandlePromotionFailure的值是否允许失败,如果设置允许失败,会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试进行一次Minor GC。如果小于或者HandlePromotionFailure的值设置不允许,那这时需要进行一次Full GC。