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


最近学习了周志明老师的《深入理解Java虚拟机》,收获颇多,留下一些学习笔记,供以后复习用。

一.学习目标

1.对象存活判断

2.GC(garbage collection)算法学习

3.垃圾回收器

4.内存分配与回收策略

对于GC学习,我们主要考虑三个问题:哪些内存需要回收 -> 什么时候回收 -> 如何回收

二.那些内存需要回收

  内存运行时,程序计数器、本地方法栈和虚拟机栈是随着线程的产生而产生,随着线程的消亡而消亡的,这几部分的内存分配和回收是确定好了的,随方法结束或线程结束时,内存就紧跟着回收了。而Java堆和方法区不一样。一个接口中多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在运行期间才知道会创建哪些对象,故内存回收与分配重点关注的是堆内存方法区内存

三.什么时候回收(判断对象的存活)

  对于方法区,永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

  对于堆,其中存放的是对象实例,对于对象实例的回收,我们首先要判断哪些对象是“存活的”,对于那部分“死亡的”对象,就是我们要回收的。判断对象的存活有两种方法:

  • 引用计数算法
  • 可达性分析算法

 引用计数算法:给对象添加一个引用计数器, 每当有一个地方引用它时, 计数器值+1, 引用失效, -1, 为0的对象不能被使用。

  • 优势:实现简单,效率高。
  • 缺点:无法解决对象相互引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。

 可达性分析算法:通过一系列的称为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的。如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。(注意:不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记)

 在Java, 可作为GC Roots的对象包括:

  • 方法区: 类静态属性引用的对象;
  • 方法区: 常量引用的对象;
  • 虚拟机栈(本地变量表)中引用的对象.
  • 本地方法栈JNI(Native方法)中引用的对象。

四.如何回收

垃圾收集算法

1.标记清除算法

  分为标记和清除两个阶段,先标记出需要回收的对象(可达性分析算法或者引用计数算法),在标记完成后统一回收所有被标记的对象。

不足之处:效率问题,标记和清除效率都不高。空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

2.复制算法

  将可用内存划分为大小相等的两块,每次只使用其中的一块。当这块用完了,就将还存活的复制到另一块上,然后将这一块一次性清除。商业虚拟机都是采用该方法来回收新生代,新生代98%都是朝生夕死的。将内存分为较大Eden和两个较小的survivor空间。每次使用其中一块Eden和survivor,回收时将存活的对象一次性地复制到另一块survivor中,再清理掉之前的。HotSpot虚拟机Eden与Survivor默认的大小比例为8:1:1。survivor空间不够时,需要依赖其他内存(老年代)进行分配担保,即让对象进入老年代。

3.标记整理算法

  复制在对象存活率较高时效率很低。根据老年代的特点提出该算法。标记过程同标记清除一样,但不是直接对可回收对象进行清理,而是让存活对象朝着一端移动,然后直接清理掉端边界外的内存。

4.分代收集算法

  根据各年代特点分别采用最适当的GC算法。在新生代:中每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集。在老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用标记—清理标记—整理算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。即:

  • 新生代:存活率低,使用复制算法
  • 老年代:存活率高,使用“标记-整理”或“标记-清除”算法

垃圾收集器

 重点笔记:

  • 并行(Parallel):多条垃圾收集线程
  • 并发(Concurrent):用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户线程在继续运行,而垃圾收集程序运行于另外一个CPU上。
  • 新生代:Serial收集器  ParNew收集器  Parallel Scavenge收集器

  • 老年代:Serial Old收集器  Parallel Old收集器  CMS收集器

新生代

1. Serial收集器

  最悠久,最基本的收集器;单线程收集器,只使用一个CPU或一条收集线程去完成垃圾收集工作,且在收集时,必须暂停其他所有的工作线程,直到收集结束。新生代采取复制算法。老年代采取标记整理算法。在进行垃圾收集时必须暂停其他所有的工作线程,即“Stop The World”。依然是虚拟机运行在Client模式下的默认新生代收集器。简单而高效(与其他收集器单线程相比),对于单个CPU环境来说,Serial收集器由于没有线程交互的开销,专注于垃圾回收,因此能获得最高的单线程收集效率。

2.ParNew收集器

  Serial收集器的多线程版。多条线程进行垃圾收集,采用复制算法。其余和Serial收集器一样。目前唯一能与CMS收集器配合工作。

3. Parallel Scavenge收集器

  新生代收集器,多线程并行、使用复制算法

  CMS的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而PS收集器的目的则是达到一个可控制的吞吐量。吞吐量即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间))。

老年代

4.Serial Old收集器  

  Serial Old同样是单线程收集器,使用“标记-整理”算法。可以与JDK1.5及之前的Parallel Scavenge搭配使用;也可以作为CMS收集器的后备预案,在并发收集发生Concureent Mode Failure时使用。

5.Parallel Old收集器 

  Parallel Old收集器是多线程,使用“标记-整理”算法。

  JDK1.6前,Parallel Scavenge只能与老年代收集器Serial Old(PS MarkSweep)组合,由于Serial Old无法充分利用服务器多CPU的处理能力,会拖累整体性能。

  JDK1.6后,Parallel Scavenge可与Parallel Old组合,达到名副其实的“吞吐量优先”,在注重吞吐量以及CPU资源敏感的场合可以优先考虑这个组合。

6.CMS收集器

  基于“标记—清除”算法,低停顿,并发收集。以获取最短回收停顿时间、低延迟为目标,适用于重视服务响应速度的应用。

 主要过程为一下四步:

  • 初始标记;Stop the World,仅标记GCRoots能关联的对象,速度很快。
  • 并发标记;进行GCRootsTracing的过程。
  • 重新标记;修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。比1长但远比2短。
  • 并发清除;并发清除与并发标记耗时最长。收集器线程都可以与用户线程一起工作。并发清理以后重置线程。

CMS收集器的内存回收过程是与用户线程一起并发执行的

 主要缺点:

  • CMS对CPU资源非常敏感。
  • CMS无法处理浮动垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
  • CMS是标记清除,会产生大量碎片空间,对大对象内存分配带来麻烦。

7.G1收集器

  与其他基于分代的收集器不同,G1将整个Java堆划分为多个大小相等的独立区域Region,新生代和老年代不再是物理隔离的。

  • 从整体来看:“标记-整理” 算法
  • 从局部(两个Region之间)来看:“复制”算法

五.内存分配与回收策略

 重点笔记:

  • 新生代GC(Minor GC):发生在新生代的垃圾收集动作,非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,一般会伴随Minor GC 速度一般比Minor GC慢上10倍以上。

 1. 优先在Eden区分配(如果启动本地线程分配缓冲TLAB-Thread Local Allocation Buffer,则优先在TLAB)如果Eden区满,则触发一次Minor GC

 2. 大对象直接进入老年代;大对象,即大量连续内存空间的Java对象,最典型的是那种很长的字符串及数组。

 3. 长期存活的对象将进入老年代;设置对象年龄计数器。对象在Eden出生并经过第一次MinorGC后仍存活,年龄+1,移入Survivor区。以后每经过一次MinorGC年龄加一,当达到15时(默认的)就进入老年代

 4. 动态对象年龄判定,并不是对象年龄必须达到最大阈值才会进入老年代。如果survivor空间中相同年龄所有对象大小总和大于其空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到阈值时才进入。

 5. 空间分配担保;发生minorGC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,成立,MinorGC可以确保是安全的。不成立,则检查HandlePromotionFailure设置值是否允许担保失败。允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。大于将尝试MinorGC,小于或者不允许冒险,也要进行一次FullGC。老年代分配担保,将survivor无法容纳的对象直接进入老年代。依然担保失败,则只好在失败后重新发起一次Full GC。

参考链接:

http://www.importnew.com/23035.html
https://www.cnblogs.com/znicy/p/6918767.html
https://www.cnblogs.com/zjxiang/p/9218209.html
https://blog.csdn.net/nalanmingdian/article/details/77426845
?