转载

java学习笔记-4 JVM垃圾回收(GC)

jvm垃圾回收相关的问题是老生常谈的问题了,相信大家都有所了解,这里再进行相关的探讨,以加深理解。若文中有不正之言,望不吝指正。

本文将围绕以下几个点展开

1.为什么要进行垃圾回收

我们知道jvm的内存结构中,依赖内存是否可共享,将内存划分为线程专享区和线程共享区,其中程序计数器、虚拟机栈、本地方法栈作为线程独享的内存,生命周期跟线程相关,比如栈中的栈帧需要分配多少内存一般在类结构确定时就已经确定好了,线程结束时内存就跟着回收了,不需要我们过多的关心垃圾回收。但是作为线程共享的内存区域,堆和方法区,java new出来的对象大部分是在堆中分配的,而且只有在程序运行期间才能知道会创建出哪些对象,在这部分内存的分配和回收是我们关心的。其实归根结底就是一句话,知道底层的jvm怎么进行垃圾回收的,对我们排查内存溢出和泄露的问题有很大帮助,而且对于提高程序的性能也有很大帮助。

2.哪些内存需要回收

判断哪些内存需要回收有两种经典的算法
  1.引用计数器法
     引用计数器法的实现是给对象添加一个引用计数器,当对象被引用时,计数器值就+1,对象引用失效时计数器值-1,当引用计数器值为0时,可认为对象无引用,此时可被回收。但是这个算法不能解决对象循环引用的问题,所以虚拟机并不是采用这种方式去判断是否回收对象的。
  2.可达性分析算法
    可达性分析算法是将可作为GC Roots的对象作为起始点,展开搜索,搜索所经过的路径,对一个对象到GC Roots 没有任何引用,则意味着对象不可达,此时对象可被回收。
java学习笔记-4 JVM垃圾回收(GC)
其中的GCRoots,其实就是一组 必须活跃的引用

,大致包括以下几种

  1. 虚拟机栈中引用的对象
  2. 方法区中静态属性引用的变量
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象

方法区垃圾回收:其实方法区中也是可以进行垃圾回收的,只是回收的性价比很低,就是说执行一次垃圾回收的动作并不能回收到很多的空间。但是满足一定条件还是可以进行回收的,一般回收的是无用的类和废弃常量。其中判断常量是否废弃,没有引用即可;而判断类无用需满足 1.java堆中没有任何该类的实例;2.加载该类的类加载器已被回收;3.该类的java.lang.Class对象没有在任何地方被引用 ,不能通过反射获取该类的方法。

3.何时进行垃圾回收

即触发GC的时间,在新生代的Eden区满了,会触发新生代GC(MinorGC),经过多次触发新生代GC存活下来的对象就会升级到老年代,升级到老年代的对象所需的内存大于老年代剩余的内存,则会触发老年代GC(FullGC)。当程序调用System.gc()时也会触发Full GC。

4. 垃圾收集算法

1. 标记清除算法
    见名知意,该算法分为两个阶段,标记和清除。如下图所示,标记就是根据之前的GCRoots判断对象是否还有引用可达。但是这个算法的缺点也很明显,第一就是标记和清除效率都很低;第二就是这种算法会造成大量不连续的内存碎片的存在,当程序需要分配大的内存空间给对象的时候,很可能因为无法找到连续的大的内存空间,再一次触发GC。图如下:
java学习笔记-4 JVM垃圾回收(GC)

图中对象b没有引用,会被回收

java学习笔记-4 JVM垃圾回收(GC)

图中可以看出,回收后有大量不连续的内存空间。

2.复制算法
    基于上一个算法,为了解决效率问题,引申出“复制”算法。基本思想就是将内存划分为大小相等的两块,每次只使用其中的一块,当一块用完将整个活着的对象全部复制到另一个半区,此时不需要考虑内存碎片的问题,只需移动指针即可,简单高效。但是需要浪费一半的内存。由于java堆内存对象存活的特点,大部分新生代中的对象存活时间都比较短,所以主流的虚拟机会将内存分为一块较大的Eden区和两块较小的Survivor区,比例是8:1:1。每次分配对象到Eden区和一个Suivivor区,垃圾回收时将整个Eden区和刚才用到的Survivor区存活的对象一次性“复制”到未被使用的那个Survivor区,最后清理掉Eden区和之前的分配的Survivor区。但是并不能保证在将Eden去和Survivor区复制到另一个Survivor区的时候内存空间一定是充足的,此时需要依赖其它内存进行分配担保。分配担保策略指明,当Eden区和之前的Survivor区之存活的对象复制到另一块Survivor区时,内存空间若不够,则直接将内存分配到老年代。
java学习笔记-4 JVM垃圾回收(GC)

复制的时候只需移动相应的指针即可

java学习笔记-4 JVM垃圾回收(GC)
3.标记整理算法
     从上面的“复制”算法可以得知,当对象的存活率很高时,进行复制操作的时候效率将会变低。这时有人提出标记整理算法,标记过程较之前相同,只是后面不直接对对象进行清除,而是将存活的对象都向一边移动,并更改对应的指针,然后清理掉右端以外的内存。但是这个算法效率也不高因为,因为包括了标记 + 移动,但是也解决了内存碎片的问题。
java学习笔记-4 JVM垃圾回收(GC)
java学习笔记-4 JVM垃圾回收(GC)

4.分代收集算法

其实就是上面算法的综合。根据对象的存活周期,一般把java堆分为新生代和老生代。新生代中对象存活率较低,选用复制算法,只需付出少量存活对象的复制成本即可完成收集;老年代中对象存活率高,没有额外的内控空间进行担保,必须使用标记清除或者标记整理算法来进行垃圾回收。

5.几种垃圾收集器比较

在进行比较之前先介绍下衡量的参数

  1. 吞吐量: 所谓吞吐量就是 CPU 用于运行代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

  2. 停顿时间 :jvm在运行垃圾回收时,需停顿用户程序,这里指的是停顿用户程序的时常

java学习笔记-4 JVM垃圾回收(GC)

Serial收集器

单线程的收集器,采用 复制算法,进行垃圾回收时需要将用户的所有线程全部暂停直到垃圾回收动作的完成。交互体验不是很好,但是在单核cpu或者虚拟机运行在client模式下,停顿时间可以控制到毫秒级,是可以接受。

Serial Old收集器

Serial Old是Serial的老年代版本,使用   标记整理算法
java学习笔记-4 JVM垃圾回收(GC)

ParNew收集器

ParNew收集器采用复制算法,其实就是Serial收集器的多线程版本,是server模式下首选的新生代收集器。在多核cpu下有着比Serial更好的表现。
java学习笔记-4 JVM垃圾回收(GC)

Parallel Scavenge收集器

采用复制算法,也是一个新生代的收集器。较于ParNew收集器,Parallel Scavenge收集器更多的目标是达到一个可控制的吞吐量。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集 停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参 数

Parallel Old收集器

Parallel Old收集器采用  标记整理算法 是ParallelScavenge收集器的老年代版本。在注重吞吐量以及cpu资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
java学习笔记-4 JVM垃圾回收(GC)

CMS(Concurrent Mark Sweep)收集器

采用标记清除算法,是一个老年代的并行收集器,是一个以获取最短回收停顿时间为目标的处理器,具有高并发、低停顿的特点。包括四个过程:初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记都需要暂停用户所有线程。
java学习笔记-4 JVM垃圾回收(GC)

但是这个收集器也有明显的缺点, 1. 前面介绍过的标记清除算法讲到会产生大量的内存碎片,很可能老年代空间有富余,但是新生代没有足够大的内存空间分配对象。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开 关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并 整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长; 2.CMS收集器无法处理浮动垃圾(由于cms并发清理阶段用户线程还在运行,这段时间内出现的新的垃圾称之为浮动垃圾) 3.cms对cpu的资源很敏感,虽然收集器是并发的,但是因为占用了一部分线程会导致应用程序变慢,总吞吐量降低。

G1收集器

G1收集器基于 标记整理算法 是面向服务端的,是面向新生代和老生代的收集器。收集过程主要包括:1.初始标记:标记GCRoots能关联的对象;
2.并发标记:从GC Roots开始对对象进行可达性分析,找出存活的对象,耗时较久,可与用户线程同时进行 3.最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录 4.筛选回收:首先对各个region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,优先处理最需要回收的。
java学习笔记-4 JVM垃圾回收(GC)
原文  https://juejin.im/post/5b2efb4451882574cf66a837
正文到此结束
Loading...