深刻理解JVM以内存回收机制

背景

C、C++等语言中,内存的分配和释放由程序代码来完成,容易出现因为程序员漏写内存释放代码引发的内存泄露,最终致使系统内存耗尽。
Java代码运行在JVM中,由JVM来管理 堆Heap 内存的分配和回收(Garbage Collection),把程序员从繁琐的内存管理工做中释放出来,更专一于业务开发。Java内存回收工做由标记(识别可回收对象)和回收(释放可回收对象)两个步骤组成。
和程序代码释放内存相比,内存自动管理会占用一部分CPU时间,Stop The World特色回暂停业务程序运行,很是影响执行效率。Java各版本中,一直致力于内存管理算法的优化,造成了一套针对各类内存分区(新生代、老年代)和运行场景(单核、多核、客户端、服务端)的特色而针对性设计的内存回收算法。程序员

这里说的内存回收机制,主要是指针对 堆Heap元空间Metaspace内存的回收,线程相关内存(栈、本地栈、程序计数器)内存随线程建立和回收,直接内存的释放由其在堆内存中引用释放时触发。

内存标记算法

在内存被回收前,系统必须标记哪些内存已经没有人使用能够释放,这个工做就由内存标记算法的来完成,在Java各版本中,使用过以下几种标记算法。算法

引用计数法(Reference Counting Collector)

这是早期的内存标记算法,每一个堆中分配的对象都有一个引用计数器,计数一个对象被引用的次数。当对象建立并赋值给变量时,计数为1,当有其余变量引用该对象时,引用计数+1;但引用此对象的变量超出存活范围或释放对对象引用(包括变量引用了其余对象或变量被设置为null等),引用计数-1。对象的引用计数为0时,表示此对象可被垃圾收集器回收。
引用计数法的有点是简单,执行速度快,只要变量一遍对象检测引用计数是否为0便可判断是否可回收;肯定是没法检测出循环引用而致使内存没法回收。多线程

跟踪算法(Tracing Collector)

采用跟搜索算法,搜索算法引入了图论,把全部对象间的关系当作一张图,内存标记从一组根节点(GC Root Set)开始,经过递归搜索,创建对象的引用关系图,当搜索完毕后,图外的对象就是可回收对象。这是目前Java中使用的内存标记算法
GC Root Set.PNG并发

可做为GC Root的对象包括框架

  1. 栈中局部变量引用的对象
  2. 类静态变量引用的对象
  3. 常量引用的变量
  4. 本地方法栈引用的对象

内存回收方式

标记-清理算法(Mark and Sweep)

采用跟踪算法标记内存对象后,再扫描堆内存中未被标记的对象,进行回收。此算法不移动对象,仅对不存活对象进行回收,在存活对象占比高的状况下处理效率高,但不移动对象会引发内存碎片。性能

标记-整理算法(Compacting)

此方法和标记-清理算法使用相同标记算法,但在对不存活对象回收时,会把存活对象向内存前部空闲区域移动,同时更新对象的指针。此方法在清理的基础上,会对对象进行移动,执行成本较高,但可解决内存碎片问题。基于此算法的内存回收实现,通常会增长句柄和句柄表。优化

复制-清除算法(Copying)

该算法把内存分为空闲区和对象区,新建对象存储到对象区中。当对象区满时,先采用跟踪算法对对象进行标记,再把存活对象拷贝到空闲区,清空原对象区,空闲区和对象区互换角色。在拷贝过车中,程序须要暂停,此算法适用于存活对象叫少的状况,能够解决内存碎片问题。spa

分代回收策略

JDK8中,堆中移除了永生代区域,堆内存主要由新生代老年代两部分组成。其中新生代由一个伊甸园(Eden)和两个幸存者Survivor From和Survivor To 3部分组成,新建立对象首先保存在Eden中,当Eden中对象达到必定数量时,JVM触发Minor GC,GC时,先把Eden和From中的存活对象拷贝到Survivor To区,再清除Eden和From两个区域的数据,最后From和To互换身份,完成一次内存回收。新生代区域对象数量大,存活时间短,通常采用复制-清除算法,经过这种结构和回收方式来提升垃圾回收效率,减小内存碎片。
通过若干(默认15)次后还存活的对象,将进入老年代区,当老年代数据满时,会触发Major GC(又称Full GC),此时新生代、老年代、元区域、直接内存区域都会执行GC操做。
JVM堆信息.PNG
老年代:新生代的内存大小默认比例为2:1。Eden和两个Survivor的比例为8:1:1。线程

垃圾收集器

上述的内存标记算法、回收方式和分代策略是垃圾回收的方法,根据这些方法,针对不一样的用户场景(Server、Client)和系统配置(单线程、多线程),JVM实现了适用于各场景的垃圾回收器。设计

  1. 年轻代收集器

    • Serial(复制-清除)
    • ParNew(复制-清除)
    • Parallel Scavenge(复制-清除)
  2. 老年代收集器

    • Serial Old(标记-整理)
    • Parallel Old(标记-整理)
    • CMS(Concurrent Mark Sweep)(标记-清理)
  3. 混合收集器

    • G1(标记-整理)应用于整个堆

年轻代收集器

Serial(复制-清除)

Serial是单线程收集器,Serial收集器只能使用单个线程进行收集工做,在收集的时候必须得停掉其它线程,等待收集工做完成其它线程才能够继续工做。
Serial收集器是JVM中最先的垃圾收集器,也是JDK1.3前的惟一收集器,再也不适用于现代多核CPU和Server(服务端)场景,可是很是的适合单核CPU和Client场景。
Serial GC.PNG

ParNew(复制-清除)

ParNew是Serial的升级版,其工做的流程和Serial基本一致,主要的改进是支持多线程同时执行垃圾回收工做,即上图中的GC Thread支持多线程,能够充分利用多核CPU的性能。它是HotSpot上第一个真正意义实现并发的收集器。GC默认开启线程数等于CPU数量,可经过 -XX:ParallelGCThreads 来控制垃圾收集线程的数量。

Parallel Scavenge(复制-清除)

Parallel Scavenge是吞吐量优先的收集器,其工做方式和ParNew基本同样,可是它以提升系统吞吐量(Throughput)为设计目标,吞吐量=业务运行时间/系统总运行(业务+GC)时间。
ParNew等收集器的关注点是尽可能缩小垃圾回收的停顿时间,而缩短停顿时间必然须要提升垃圾回收的频率,致使业务线程和GC线程间频繁的切换,从而增长CPU在现场切换上的损耗。
而以吞吐量为设计目标的Parallel Scavenge收集器,能够经过扩大新生代内存容量,减小垃圾回收发生的次数,虽然提升了单次GC的时长,但减小了线程切换开销,从总体上能够提升系统的吞吐量。
Parallel Scavenge GC.PNG

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是:

参数 做用 说明
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间 单次GC的最大毫秒数
-XX:GCTimeRatio 设置吞吐量大小 业务:GC时间比例,默认为99,即GC时间占比为 1/(1+99)=1%

单次GC时间参数并不是设置的越小越好,而是一把双刃剑,若是减小单次GC时间,必然致使GC频率的上升;而设置的增大,则必然须要更大的内存来支撑。

因为Parallel Scavenge和其余收集器(Serial、ParNew、CMS等)使用了不用的设计框架,致使其没法和CMS协同工做。

老年代收集器

Serial Old(标记-整理)

工做模式基本和新生代的Serial同样为单线程,它采用标记-整理算法,这个模式主要是给Client模式下的JVM使用。若是是Server模式有两大用途:

  1. JDK5前和Parallel Scavenge搭配使用,JDK5前也只有这个老年代收集器能够和它搭配
  2. 做为CMS收集器的后备

Parallel Old(标记-整理)

Parallel Scavenge的老年版本,JDK6开始出现,采用标记-整理算法。Parallel Old的出现结合Parallel Scavenge,真正的造成“吞吐量优先”的收集器组合。JDK7和8中,做为老年代默认的收集器。

在JDK6之前,新生代的Parallel Scavenge只能和Serial Old配合使用,而Serial Old为单线程,Server模式下没法充分利用多核CPU,这种组合没法让应用的吞吐量最大化。

CMS(Concurrent Mark Sweep)(标记-清理)

CMS收集器是以最短回收停顿时间为目标的收集器。重视响应,以带来好的用户体验,是并发低停顿收集器,经过-XX:+UseConcMarkSweepGC参数启用CMS收集器。
CMS采用支撑多线程并发的标记-清除算法,它的运做分为4个阶段:

  1. 初始标记(Initial Mark)
    标记GC Root Set直接关联的对象
  2. 并发标记(Concurrent Mark)
    以初始标记对象为基础,并发标记其关联的对象,直到全部对象标记完成,标记进程和用户进程并发执行。
  3. 从新标记(Remark)
    为了修正因并发标记期间用户程序运行而产生变更的那一部分对象的标记记录,暂停用户进程对这部分对象从新标记。
  4. 并发清除(Sweep)
    将前面标记对象的内存回收,这个阶段GC线程与用户线程并发运行。

CMS GC.PNG
CMS在初始标记和从新标记阶段须要暂停业务线程,在执行时间上,初始标记 < 从新标记 < 并发标记,因此时间最长的并发标记,业务线程和GC线程并发运行,因此用户感觉上,GC暂停的时间很短。但其也存在几个缺点,具体以下:

  1. CMS默认配置启动的时候垃圾线程数为 (CPU数+3)/4,性能很容易受CPU核数影响
  2. CMS没法处理浮动垃圾,可能致使Concurrent Mode Failure(并发模式故障)而触发Full GC
  3. CMS采用标记-清除算法,存在垃圾碎片的问题
为了解决CMS致使的内存碎片问题,CMS模式提供了 -XX:+UseCMSCompactAtFullCollection 选项,选项默认开启,用于CMS要进行Full GC时进行内存碎片整理,因为内存整理的过程没法并发,须要中止业务进程,因此启这个选项会影响性能。
相关文章
相关标签/搜索