天天看点

jvm中GC知识详细讲解

一、 JVM垃圾回收算法与分配策略

在Java的运行时数据区中,程序计数器、虚拟机栈、本地方法栈这三个区域都是线程私有的,跟着线程一起创建,当线程或者方法结束后,内存也被回收。但是Java堆和方法区不一样,这部分内存是动态分配和回收的,垃圾回收器关注的是这部分内存。

GC主要弄明白以下三个问题:

  • 那些内存需要被回收?
  • 什么时候回收?
  • 如何进行回收?

那些内存需要被回收?

垃圾回收首先要判断那些对象是可以被回收的,有两种垃圾回收算法:

  • 引用计数法:给对象添加一个引用计数器,每当有一个地方引用时,计数器+1,当引用失效时,计数器-1;当计数器为0时,被回收。但是这种方法不能解决对象之间循环引用的问题
  • 可达性分析算法(GC ROOT):通过一系列GC ROOTS对象作为起点,向下遍历,能够被遍历到的对象认为可以存活,没有被遍历到的需要被回收。

在Java种,可以作为GC Roots对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

两次标记与finalize()方法

即使在可达性分析算法中不可达的对象,也不是一定会死亡的,他们都会处于一种缓刑阶段,要让一个对象真正被回收,至少需要经历两次标记过程:

如果对象在进行可达性分析后,发现没有与GC Roots相连接的引用链,则该对象第一次标记,并且进行筛选,筛选的条件是此对象是否有必要执行finaliza()方法,当对象没有此方法或者被调用过后,虚拟机都会认为没有必要执行。如果有必要执行,会将此对象放置在一个F-Queue队列中,饭后建立一个低优先级的线程去执行它。

什么时候开始回收?

对于Minor GC,当Eden空间满了,就会触发一次Minor GC。但是Full GC相对复杂,而且耗时长,JVM调优也是较少Full GC的次数。其触发条件大致以下几种:

  • 调用System.gc():此方法建议JVm进行Full GC,但是不一定会执行。
  • 老年代空间不足:当大对象直接进入老年代,长期存活的对象进入老年代等,执行完Full GC后空间任然不足,此时会抛出 Java.lang.OutOfMemoryError: Java heap space。
  • 空间分配担保失败:使用复制算法的Minor GC需要老年代空间作为担保,如果担保失败,则会触发Full GC

如何进行回收?

主要有四种垃圾回收算法:标记 - 清除、标记 - 整理、复制算法、分代回收算法

标记 - 清除算法

此算法分成标记、清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记 - 清除算法的缺点:

  • 空间问题:标记清除后会产生大量的不连续内存碎片,当太多可能会在分配较大对象时因为没有连续的空间而触发GC
  • 效率问题:因为内存碎片,操作更加费时

示意图如下:

jvm中GC知识详细讲解

复制算法

为了解决标记 - 清除算法的效率问题,出现了一种复制算法:将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这一块内存使用完,久将还存活的对象复制到另一块上面,然后将已经用过的这一块全部清除。

这样可以每次都是对整个半区进行内存回收,不需要考虑内存碎片等情况。但是缺点是将可用内存缩小为原来的一半。执行过程如图:

jvm中GC知识详细讲解

Minor GC过程:新生代将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次Minor GC都会将Eden和From Survivor中还存活的对象一次性的复制到另外一块To Survivor空间上,然后清理掉Eden和刚使用过的Survivor空间。当对象经历15次GC还存活后,将会转入老年区。在每次Minor GC结束后,Eden空间是空的,两个Survivor空间其中一个是空的,另一个存储着存活的对象。

分配担保:

当Minor GC进行内存回收时,如果另外一块Survivor上没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。如果老年代分配担保不足,会触发full GC

标记 - 整理算法

复制算法在对象存活率较高的时候需要进行较多的复制操作,效率就会变低。根据老年代的特点,标记 - 整理算法被提出来:此算法鱼的标记过程与标记 - 清除算法一样,但是后面是清除之后对对象进行整理,保证没有内存碎片。如图:

jvm中GC知识详细讲解

分代收集算法

主要思想:根据对象存活周期的不同将内存划分为不同区域,一般Java将堆分为新生代和老年代,这样可以根据不同区域选择不同的收集算法:

  • 新生代:每次垃圾收集都有大量对象死去,只有极少数存活,选择复制算法。
  • 老年代:对象存活率高,没有额外的空间进行担保,就必须使用标记 - 清除或者标记 - 整理算法来进行回收。

内存分配策略

对象优先在Eden区分配

大多数情况下,对象在新生代的Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC

大对象直接进入老年代

大对象指需要大量连续的内存空间的Java对象。经常出现大对象很容易导致内存还有不少空间就提前触发GC以获取足够的连续空间来存储大对象。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法回收内存)

长期存活的对象进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

二、 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。在1.8垃圾收集器主要有以下几种:

jvm中GC知识详细讲解

上图7中作用于不同代的收集器,如果两个收集器之间有着连线,则可以搭配使用。

并行和并发

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

新生代收集器

Serial收集器:

jvm中GC知识详细讲解

采用复制算法的新生代收集器,是一个单线程的,在进行垃圾收集时,必须暂停其他所有的工作,直至收集器结束。

ParNew收集器:

ParNew收集器就是Serial收集器的多线程版本,新生代收集器,采用多线程进行垃圾收集

ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作

Parallel Scavenge 收集器

jvm中GC知识详细讲解

并行的多线程新生代收集器,使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,它不能与CMS收集器配合使用。

老年代收集器

Serial Old收集器

jvm中GC知识详细讲解

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

Parller Old收集器

jvm中GC知识详细讲解

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

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

G1收集器

jvm中GC知识详细讲解

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。