jvm-垃圾回收

参考资料:详解CMS垃圾回收机制(强烈推荐)

一:为什么需要垃圾回收?

jvm把内存管理权从开发人员收回,开发人员只需要创建数据对象即可,内存的分配和回收都由jvm自动完成。

程序只管创建对象,不管对象的回收,内存最终会被耗尽。


二:怎么判断对象为垃圾?

如果要实现垃圾回收,首先必须能判断哪些对象是垃圾。

对象不再被使用就认为是垃圾。jvm自动回收垃圾,但它如何才能知道一个对象是否不再被使用?

常见的策略有如下两种:引用计数器 、可达性检测

2.1 引用计数器:

即如果一个对象被外部引用则计数器加 1, 反之减 1。如果计数器为0,则说明当前对外象没有被任何外部使用,则认为是垃圾。

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

优点:实现简单

缺点:无法解决循环引用的问题;如:对象A,B相互引用,除此再没有被其它对象引用,那么它们两个都是垃圾,但计数器却均为1,而无法回收。

注意事项:引用计数器只是一个理论方案,从来没有一个主流的jvm使用这种方式

2.2 可达性检测

引用计数器无法解决循环引用的问题,因此更好的办法是通过可达性分析。jvm中的任何非垃圾对象通过引用链向上追溯,都可以到达一些根对象(法方区的静态变量、常量、栈中的变量),这些根对象都是存活的对象,那么被活对象引用的对象很有可能会继续使用,因此反过来,从根对象向下追溯到的对象都可以认为是存活的对象。这种从根对象追溯的方法称为可达性分析。


如下:从根对象向下追溯,红色标记的对象是不可达的,因此它们就是垃圾,会被GC回收。



  2.3 根对象种类

可以做为GC root(根对象)的对象有以下几种:

  • 虚拟机栈(栈帧中变量引用的对象)
  • 方法区中静态属性(static 属性)
  • 方法区中的常量(static final),(jdk8及以上,为元数据区)
  • 本地方法栈中引用的对象

       

三:垃圾回收算法

标记出哪些对象是垃圾后,就需要对这些垃圾对象进行回收。

常用的回收算法有:标记-清除、复制、标记-整理

3.1 标记-清除

通过标记、清除两个阶段回收垃圾对象。因为标记的是存活对象,清除的是非存活对象,所以需要两个阶段:先标记,再遍历所有对象,过滤出非存活对象。

如下图:(绿色-存活对象红色-垃圾白色-空闲

首先,通过可达性分析,标记出存活的对象(绿色块)

其次,遍历堆中所有对象,把非存活的对象全部清空。


优点:实现简单,并且是其它算法的基础

缺点:A:标记效率不高,清除算法也不高(遍历所有对象进行清除).

         B:产生大量内存碎片


3.2 复制算法

为了解决标记-清除 算法的效率问题,使用复制算法。

复制算法需要一块同样大小额外的内存做为中转。

因为复制的是存活对象,不需再次遍历。

步骤:通过可达性分析,标记出存活对象,并同时把存活对象复制到另一块对等内存。

         当所有存活对象都复制完后,直接清空原内存块(不需要遍历,直接移动堆顶指针即可)。


优点: 不需要两阶段,存活对象少时效率高。

   没有内存碎片

缺点:需要额片内存,同一时间总有一块内存处于备用状态-浪费内存。

         存活对象很多时效率也不高(主要是因为对象复制代价高昂)

使用场景:存活对象多,内存紧张的场景。


复制算法变种

复制算法最大的缺点是需要一个相同大小的内存块,为了减少内存浪费,复制算法还有一种变种。

如果对象中存活的很少,就不需要一个相同大小的额外内存块,而只需要两个小内存块,交替做为中转站就可以完美解决。


前提:存活的对象很少,IBM研究表明新生代90%以上甚至98%的对象朝生夕死。

步骤:

A:设置三块内存,第一块大内存块,第二第三为两个相等的小内存块

B:创建对象分配置在大内存块和 两小内存块中的任一个,另外一小内存块保持空闲备用。

        C:回收:通过可达性分析,标记出第一块和其中使用的小块内存中存活对象,同时把存活对象复制到备用的另一块小内存中

         D:清空大内存块和被回收的小块内存。此时:大内存被清空,其中两块小内存:一块清空,一块保存了上次存活的数

         E:然后交替使用两块小内存块做为清空大内存和另一块小内存的中转。


优点:减少了内存浪费,同时又保持了复制算法的优点。

缺点:未完全杜绝内存浪费,同时大数据量时,效率低;存活对象数量占比较大时,小内存块无法做为中转站。

使用场景:在存活对象较少,追求高效率,内存无碎片的场景。


3.3 标记-整理

标记清除算法效率低,碎片严重; 复制算法存活对象少时效率高,无碎片,但内存浪费;为了折中两种算法的优点,有人提供另一种算法:标记-整理算法。

步骤:

A:根据可达性分析,标记出所有存活的对象

B:遍历所有对象,过滤出非存活的对象,并把这些对象一个一个,从内存的某一个角落顺序排列。



优点:没有内存浪费,无碎片

缺点:效率最低,小于标记清除(需要两个阶段<标记,移动>;移动类似复制,代价高于直接清除,存活对象越多,移动代价越大)


四:分代算法

准确的讲,分代算法不是一种回收算法,它只是按对象生命周期和特点不同,合理选用以上三种回收算法的手段。

内存模型中,我们大概了解了堆内存的分代结构如下:


为什么需要分代?

因为不同的对象生命周期不同,有的很长(如:session),有的很短(如:方法中的变更);如果不分代,每次可达性分析标记时,都要遍历暂时不会回收的老对象,当老对象越来越多时,重复对老对象的无用遍利检查,会严重影响回收性能。

如果把对象按年龄隔离,分成新生代和老年代,老年代保存生命周期长的对象,新生代保存新创建的对象,那么老年代就可以长时间不回收,而新年代大部分是朝生夕死,就可以频繁回收。即保证了效率,又保证了新生代内存的及时回收。

总结:新生代:时间换空间(频繁回收:由于存活的数据量少,频繁回收的代价也可以接受)

         老年代:空间换时间(需要时回收:存活的多,频繁回收严重影响性能;有些对象可能已经变垃圾了,但仍然存在老年代中,等到新生代不够或其它条件时,才回收老年代)


如何区分新老对象?

这个与垃圾回收器实现有关,对应回收器有相关的配置。

主要有几种情况:

  • 大对象直接进行老年代
  • 年龄达到一定阀值(每经历过一次回收还活着:年龄加1,  默认阀值为:15,可配置)
  • survivor空间中相同年龄所有对象大小的总和超过survivor空间的一半时,即使没达到年龄阀值


五:垃圾回收器

垃圾回收算法只是垃圾回收的理论基础,垃圾回收器是对垃圾回算法的实现。

垃圾回收器关注三个方法:A:垃圾回收算法选择  B:串行和并行(并发)的选择  C:新老代的选择

下面先了解一下jvm中的垃圾回收器种类:



垃圾回收器根据新老代不同区分,一部分只用于新生代回收(Serial、ParNew、Parallel),一部分只用于老年代(Serial old、CMS、Parallel old); G1是一个特殊的存在,后续再讲。


下面我一个一个分析各自的原理及特点,然后分析他们为什么只能使用新生代或老年代;以及实战中如何选择。

5.1 Serial

serial/serial old 收集示意图(图片来自:JVM系列之垃圾回收(二)


  • 使用于:新生代
  • 垃圾回收算法:复制算法
  • 串行/并行/并发:串行,单线程
  • stw:是
serial是一个单线程,且用于新生代的垃圾回收器。它运行时,需要stw,暂停所有用户线程。所以,堆配置过大,且垃圾太多时,会导致程序有明显的停顿。

由于新生代是存活量少,回收频繁,所以必须使用最高效的回收算法-复制算法;复制算法大量存活数据,且需要额外内存的情况下是不符合老年代的,因此当前回收器只能用于新生代。


注意:此收集器,只适用于client模式,不适用于生产环境的server模式(当今服务器已经很少有单cpu,单线程在多cpu下,会浪费过多cpu资源,导致垃圾回收停顿时间过长和频繁)

5.2 ParNew

(图片来自:JVM系列之垃圾回收(二)


  • 分代:用于新生代
  • 垃圾回收算法:复制算法
  • 串行/并行/并发:并发,多线程
  • stw:是

ParNew是serial收集器的多线程模式,除此之外没有任何区别。多线程大大提高了多cpu服务器的垃圾回收效率,减少停顿时间。

5.3 Parallel Scavenge

  • 分代:用于新生代
  • 垃圾回收算法:复制算法
  • 串行/并行/并发:并行,多线程
  • stw:是

Parallel Scavenge 与 ParNew一样也是多线程,但是与ParNew不同的是,它关注的点是垃圾回收的吞吐量(用户线程时间/(用户线程时间 + 垃圾回收时间)),也就是:它期望尽可能压榨cpu,多用于业务捃,它关注的是整体,而不是一次。

如:假如每分钟执行1000次垃圾回收,每次的停顿时间很短,但1000次总停顿时间要高于 每分种100次的时间。那么100次垃圾回收就是Parallel Scavenge期望的。

5.4 Serial old


  • 分代:用于老年代
  • 垃圾回收算法:标记-整理算法
  • 串行/并行/并发:串行,单线程
  • stw:是

由于老年代,活的多,死的少,且最好没有碎片:标记整理算法;

跟Serial收集器一样,当前收集器也是单线程,因此也不适合多核时代的服务器上,是默认的client模式,同时做cms收集器失败时的备选收集器(因为cms是并发的,如果并发失败,就不要并发了,所以使用了serial Old)。

5.5 CMS

(图片来自:JVM系列之垃圾回收(二)


  • 分代:用于老年代
  • 垃圾回收算法:标记-清除算法,有碎片
  • 串行/并行/并发:多线程
  • stw:初始标记stw; 重新标记stw

CMS是首个并发收集器,垃圾回假步骤中的部分阶段可以与用户线程并发执行。

垃圾回收器的最终目标就是:减少垃圾回收对用户线程的影响(停顿频率小、停顿时间少)。

为此,CMS把垃圾回收分为四个阶段,把不需要停顿的阶段与用户线程一起执行:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清理
初始标记:从GC ROOTS只标记一级对象(存活的),所以速度很快;但需要stw。
并发标记:从一级对象开始向下追塑引用链,标记引用链上的对象;不需要stw,与用户线程并发执行。速度是慢。
重新标记:修正并发标记过程中,因用户线程继续进行而导致标记变更的那部分对象;速度比初始标记慢,但比并发标记快很多。(但是:到底是修正了标记存活的对象还是其它?如果是修改存活的,那么可以做为浮动垃圾等到下一次回收即可阿???)
并发清理:垃圾回收线程与用户线程并发执行,清除垃圾(如果标记的活着对象,那么不stw如果清除垃圾,此时如果用户线程又产生对象了?通过ooM?暂时没想通)

优点:单次停顿的时间更短
缺点:有碎片

5.6 Parallel old

(图片来自:JVM系列之垃圾回收(二)


  • 分代:用于老年代
  • 垃圾回收算法:标记-整理算法
  • 串行/并行/并发:多线程
  • stw:是

六:GC日志


七:jvm配置

猜你喜欢

转载自blog.csdn.net/yangspgao/article/details/79084208