JVM-2 垃圾收集器和内存分配策略

2020-03-22  本文已影响0人  巴巴11

哪些内存需要回收?
什么时候回收?
如何回收?

1 GC如何判定对象“已死”?

1.1 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1,引用失效时,计数器-1。
任何时刻计数器为0的对象就是不可能再被使用的对象。

实现简单,判断效率也很高。

很多JVM没有选用引用计数法来管理内存,主要原因就是很难解决对象间的循环引用问题。

1.2 可达性分析算法

主流的JVM实现中,都是通过可达性分析来判定对象是否存活。

思路:
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为“引用链”,
当一个对象的GC Roots没有任何引用链相连时,从图论的角度说就是从GC Roots到这个对象不可达,
那么这个对象就是不可用的。
4种引用:
- 1 强引用:
在程序代码中普遍存在的,例如Object o = new Object();
- 2 软引用:
用来描述一些还有用但非必须的对象。
对于软引用,在系统将要发生OOM之前,将会把这些对象列进回收范围之中进行第二次回收
JDK中,SoftReference实现。
- 3 弱引用:
用来描述非必须对象。强度比软引用更弱一点。只能生存到下一次GC发生之前。
当GC工作时,无论内存是否足够,都会进行回收。
JDK中,WeakReference实现
- 4 虚引用:
最弱的一种引用。
不影响对象的生存时间。也无法通过虚引用来取得一个对象的实例。
PhantomReference实现。

生存还是死亡?

在可达性分析中,不可达的对象,进入缓刑阶段。
还要经历两次标记过程。
如果GC Roots不可达,进行第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者已经执行过了,则不GC。
如果被判断有必要执行finalize()方法,那么对象就会进入回收队列中。

finalize()方法是对象逃脱死亡命运的最后一次机会。

永久代或者方法区的GC主要是废弃常量和无用的类。

该类的所有实例都被回收了。
加载该类的classloader已经被回收了。
类对应的java.lang.Class对象没有在任何地方被引用。

2 垃圾回收算法

2.1 标记-清除算法

最基础的收集算法。

不足:
效率问题,效率不高。
空间问题,标记清除后会产生大量的内存碎片。

image.png

2.2 复制算法

现将内存分为大小相等的两块。
每次只使用其中一块。
当这块用完了,将还存活的对象复制到另一块上去,然后把已使用过的内存空间清理掉。

优点:
每次只针对半块内存进行回收。
不用考虑内存碎片等复杂情况。
实现简单,运行高效

缺点:
代价是将内存缩小为原来的一半。

image.png
现在的商业虚拟机都采用这种收集算法来回收新生代,
IBM公司的专门研究表明,新生代中的对象98%是“朝生夕灭”的,
所以并不需要按照 1:1 的比例来划分内存空间,
而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,
最后清理掉Eden和刚才用过的Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,
也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
当然,98%的对象可回收只是一般场景下的数据,
我们没有办法保证每次回收都只有不多于10%的对象存活,
当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

2.3 标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

image.png

2.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。

一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

3 HotSpot的算法实现

涉及到的几个重点概念:
GC停顿。
Stop The World。
安全点。

3.1 枚举根节点

OopMap

3.2 安全点

Safepoint

3.3 安全区域

Safe Region

4 垃圾收集器

HotSpot提供的商业G1收集器:


image.png

(A)图3-5展示了7种作用于不同分代的收集器:

Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

(B)虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器;

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

(C)如果两个收集器之间存在连线,就说明它们可以搭配使用。

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scanvenge/Parallel Old、G1;

(D)其中Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备预案

Minor GC和Full GC的区别

Minor GC:
又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

Full GC:
又称为Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC
(不是绝对,Parallel Scavenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上。

4.1 Serial收集器

是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择,使用复制算法。

这个收集器是一个单线程的收集器,但它的”单线程“的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,
更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
”STOP The World“这项工作是由虚拟机在后台自动发起和自动完成的,

在用户不可见的情况下把用户正常工作的线程全部停掉,这对 很多应用来说都是难以接受的。

下图示意了Serial/Serial Old收集器的运行过程。


image.png

4.2 ParNew收集器

ParNew(ParNew是parallel new的简写)收集器其实就是Serial收集器的多线程版本,
除了使用多条线程进行垃圾收集之外,
其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、
收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,
也同样使用复制算法,在实现上,这两种收集器也共用了相当多的代码。

ParNew/Serial Old收集器的工作过程如图所示:


image.png

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

4.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

Serial Old收集器的工作过程如图所示:


image.png

4.5 Paralle Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。


image.png

4.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

4.7 G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。

G1是一款面向服务端应用的垃圾收集器。
HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式取处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

5 内存分配与回收策略

基本是在堆上分配。

对象主要分配在新生代的Eden区上。
对象优先在Eden分配。当Eden没有足够空间时,JVM发起一次Minor GC。

大对象直接进入老年代。

长期存活的对象将进入老年代。
JVM给每个对象定义一个对象年龄计数器。
上一篇下一篇

猜你喜欢

热点阅读