JVM之垃圾收集器与内存分配策略

概述

使用Java的同窗都知道在Java中垃圾自动回收,可是就算如此,咱们也得知道Java是如何实现垃圾自动回收,本文咱们就来学习JVM的垃圾收集和内存分配。java

名词解释

在了解回收器以前,咱们先来了解下几个名词
算法

  • 吞吐量-----指CPU用于运行用户代码的时间和CPU运行时间的总值的比值,好比虚拟机总共运行了100分钟,用户代码运行了99分钟,垃圾回收时间1分钟,则吞吐量就是99%。
  • 停顿时间-----指回收器正在运行,用户程序却在暂停的时间。对于独占的回收器而言,停顿时间可能会比较长,使用并发回收器,因为垃圾回收线程和用户线程交替运行,程序的停顿时间会很短,可是因为其效率极可能不如独占垃圾回收器(因为线程上下文的切换须要耗费CPU资源),故系统的吞吐量可能会较低
  • Minor GC-----指发生在新生代的垃圾回收动做,由于Java对象大多都具有朝生夕死的特性,因此Minor GC一般很频繁,通常回收速度也比较快
  • Major GC-----指发生在老年代的垃圾回收动做,出现Major GC常常伴随至少一次的Minor GC。Major GC通常会比Minor GC慢10倍以上。
  • 串行-----单线程进行垃圾回收工做,但此时用户线程仍处于等待状态
  • 并发-----指用户线程和垃圾回收线程交替执行
  • 并行-----指多条垃圾收集线程并行工做,但此时用户线程仍处于等待状态

如何判断对象已死?

Java堆中存放着Java世界中全部的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要肯定这些对象有哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用)。数组

引用计数法

  • 给对象添加一个引用计数器,每有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。
  • 可是引用计数法有一个致命的缺点,若是在一个对象A中引用了对象B,而对象B中又引用了对象A,这就出现了循环引用的问题,虽然两个对象都已经能够进行清除了,可是因为他们互相引用着,因此他们的引用计数器都不为0,都没法被回收

根搜索法

  • 经过一系列名为“GC Root”的对象为起始点,从这些个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连时,则证实此对象是不可用的
  • 如下几种对象能够看成GC Roots:虚拟机栈中的引用对象;方法区中的类静态属性引用的对象;方法区中的常量引用的对象;本地方法栈中Native方法引用的对象(其实都是垃圾回收不会做用的区域的对象)

引用类型

  • 强引用:指在程序代码中广泛存在的,相似于“Object obj = new Object()”这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用:用来描述一些还有用,但并不是必需的对象。对于软引用关联着的对象,只有在系统没有足够的内存时,垃圾回收才会将其回收,不然不会回收。
  • 弱引用:也是用来描述非必需对象的,可是它的强度比软引用更弱一些,只要垃圾回收器就行工做,无论当前内存是否足够,都会回收只被弱引用关联的对象
  • 虚引用:最弱的一种引用关系,彻底不会对对象的生存时间构成影响,只是为了在这个对象被回收的时候收到一个系统通知

对象的自救

  • 在根搜索算法中不可达的对象,也并不是是“非死不可”的,若是该对象覆盖了finalize()方法且没执行过的话,能够在finalize()中实现自救,若是该方法已经执行过了,那都会被认为这个对象是“死”的了。
  • (方法区的垃圾回收)判断一个类是不是“无用的类”的三个决定性条件:该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

垃圾收集算法

垃圾收集算法是垃圾回收的核心,关系到垃圾收集的效果和效率bash

标记-清除算法

标记-清除算法从根集合进行扫描,并对存活的对象进行标记。标记完成以后,再扫描整个空间中未被标记的对象进行直接回收,以下图所示: 服务器

标记-清除算法
该算法主要有两个缺点:

  • 效率问题,标记和清除过程的效率都不高
  • 空间问题,标记-清除以后会产生大量不连续的内存碎片

复制算法

复制算法将内存划分为两个区间,使用此算法时,全部动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空闲区间)则是空闲的。
复制算法一样从根集合扫描,将存活的对象复制到空闲区间。当扫描完毕活动区间后,会将活动区间一次性所有回收。此时本来的空闲区间变成了活动区间,下次GC的时候又会重复刚才的操做,以此循环。 多线程

复制算法
复制算法的特色:

  • 优势是实现简单,运行高效,存活对象较少的时候,极为高效
  • 缺点是空间利用率低

标记-整理算法

标记-整理算法采用标记-清除算法同样的方法进行对象的标记,但在回收不存活的对象占用空间后,会将全部的存活的对象往一端空闲空间移动,并更新对应的指针。 并发

标记-整理算法
标记-整理算法的特色:

  • 由于有一个存活对象压缩的操做,解决了内存碎片的问题
JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Minor GC)主要采用复制算法。
而对于老年代内存的回收(Major GC),大多采用标记-整理算法。
复制代码

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法:post

  • 将Java堆分为新生代和老年代,根据各个年代的特色采用最适当的收集算法
  • 新生代大多数都是朝生夕死的对象,适合使用复制算法,高效,由于每一次垃圾回收的时候,对象存活率很低
  • 老年代通常都是采用标记-清除或者标记-整理算法

垃圾收集器

在目前的主流JVM中,具体由Serial、ParNew、ParallelScavenge、Serial old、Parallel Old、CMS、G1等七种垃圾回收器,下图中,表示出了不一样的垃圾回收器适用于不一样的内存区域以及各个垃圾回收器之间的配合使用关系。 学习

垃圾收集器
图中的七种垃圾回收器,分别用于不一样的分代的垃圾回收:

  • 新生代:Serial、ParNew、Parallel Scavenge
  • 老年代:Serial old、Parallel old、CMS
  • 全堆:G1

Serial收集器 (JVM参数:-XX:UseSerialGC)

  • Serial收集器是最基本、历史最悠久的收集器
  • 工做在新生代,因此采用的是复制的垃圾回收算法
  • 是一个单线程收集器,“单线程”的意义不只仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工做,而是说在它进行垃圾收集时,必须暂停其余全部的工做线程("Stop The World")
  • Stop The World意为着在进行垃圾回收的时刻只能进行垃圾回收,用户的工做线程会被暂停,直到垃圾回收过程完成,这对于服务器端的JVM来讲是不能够忍受的
  • Serial因为没有线程的切换去耗费CPU资源和时间,因此它是效率很是高的,在通常的Client端是可使用这个Serial收集器的
    serial收集器
  • 由上图可见,单线程采用复制而且暂停全部其余线程

ParNew收集器 (-XX:UseParNewGC)

  • Serial收集器的多线程版本
  • 做用于新生代,采用复制回收算法,也得Stop The World
  • 根据CPU核数,开启不一样的线程数
    ParNew

Parallel Scavenge收集器 (XX:+UseParallelGC)

  • 新生代收集器,使用复制回收算法
  • 多线程回收器,与ParNew不一样是更关注程序运行的吞吐量(用户代码运行时间占总运行时间的百分比)

Serial Old收集器 (-XX:+UseSerialGC)

  • Serial的老年代版本,一样仍是单线程收集器
  • 采用标记-整理算法,主要也是在Client模式下的虚拟机使用

Parallel Old收集器 (-XX:+UseParallelOldGC)

  • Parallel Scavenge收集器的老年代版本,使用多线程标记-整理算法
  • 一样考虑吞吐量优先指标,很是适合注重吞吐量CPU资源敏感的场合

CMS收集器

  • 最短回收停顿时间为前提的回收器,属于多线程回收器,采用标记-清除算法
  • 相比以前的回收器,CMS回收器的运做过程比较复杂:
  • 初始标记-----仅仅是标记GC Root能直接关联的对象,这个阶段很快,但仍需Stop The World
  • 并发标记-----进行的是GC Tracing,从GC Root开始对堆进行可达性分析,找出存活对象
  • 从新标记-----为了修正并发期间因为用户程序继续运做致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长,但远比并发标记的时间短,也须要Stop The World
  • 并发清除-----开始并发清除前面标记的能够回收的对象,垃圾回收的线程与用户线程并发执行,因此能达到停顿时间短这个目第 固然,CMS回收器可定也会有相应的缺点:
  • 采用的是标记-清除算法,会产生内存碎片
  • 在并发清除的阶段,用户线程也在继续运做,这个时候所产生的垃圾(浮动垃圾)没法在此次的回收过程当中回收,必须得等到下一次的垃圾回收
  • 对CPU资源很是依赖,过度依赖于多线程环境,默认状况下,开启的垃圾回收的线程数为(CPU的数量 + 3)/ 4,当CPU数量少于4个时,CMS对用户查询的影响很大

G1收集器

G1是JDK1.7中正式投入使用的用于取代CMS的压缩回收器。它虽然没有在物理上隔断新生代与老生代,可是仍然属于分代垃圾回收器。G1仍然会区分年轻代与老年代,年轻代依然有Eden区和Survivor区。
G1首先将堆分为分为大小相等的Region,避免全区域的垃圾回收。而后追踪每一个Region垃圾堆积的价值大小,在后台维护一个优先列表,根据容许的回收时间优先回收价值最大的Region。同时G1采用Remembered Set来存放Region之间的对象引用,从而避免全堆扫描。G1的分区示例以下图所示: 优化

这种使用Region划份内存空间以及有优先级的区域回收方式,保证G1回收器在有限的时间内能够得到尽量高的回收率
G1和CMS运做过程有不少类似之处,整个过程也分为4个步骤:

  • 初始标记-----仅仅是标记GC Root能直接关联的对象,这个阶段很快,但仍需Stop The World
  • 并发标记-----进行的是GC Tracing,从GC Root开始对堆进行可达性分析,找出存活对象
  • 从新标记-----为了修正并发期间因为用户程序继续运做致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长,但远比并发标记的时间短,也须要Stop The World
  • 筛选回收-----首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。这个阶段能够与用户线程一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅提升回收效率。
    与其余的GC回收器相比,G1具有以下4个特色:
  • 并行与并发-----使用多个CPU(并行)来缩短Stop The World的停顿时间,部分其余回收器须要停顿用户线程来进行GC动做,而G1回收器仍能够经过并发的方式让用户线程继续执行
  • 分代回收-----与其余回收器同样,分代概念在G1中依然得以保留。虽然G1能够不须要其余回收器配合就能独立管理整个GC堆,但它可以采用不一样的策略去处理新建立的对象和已经存活一段时间、熬过屡次GC的旧对象,以获取更好的回收效果。新生代和老年代再也不是 物理隔离,是多个大小相等的独立Region。
  • 空间整合-----与CMS的标记-清理算法不一样,G1从总体来看是基于标记-整理算法实现的回收器。从局部上来看是基于复制算法实现的;但不管如何,两种算法都意味着G1运行期间不会产生内存碎片
  • 可预测的停顿-----这是G1相对于CMS的另外一大优点,下降停顿时间是G1和CMS共同关注点。G1除了追求低停顿以外,还能创建可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片断内,消耗在垃圾回收上的时间不得超过M毫秒

垃圾收集器总结

从前面的介绍咱们能够直到,在运行使用JVM时,咱们能够根据不一样使用环境下选择不一样的垃圾回收器(根据设置JVM参数),固然咱们须要直到的是,随着时间的推移,越日后的垃圾回收器确定会越智能,越好,因此咱们平时使用的通常都是G1垃圾回收器,由于它是一个很是强大的垃圾回收器。

内存配与回收策略

Java技术体系中所提倡的自动内存管理最终能够归结为自动化的解决了两个问题:

  • 给对象分配内存
  • 回收分配对象的内存
    前面咱们花了很大的篇幅去学习虚拟机中的垃圾收集器体系及其运做原理,如今咱们来看看如何给对象分配内存

对象优先在Eden区分配

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

大对象直接进入老年代

所谓的大对象就是指,须要大量连续内存空间的java对象,最典型的就是那种很长的字符串及数组,在给大对象分配内存时应直接将其放至老年代

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

若是对象在Eden区出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增长1岁,当它的年龄增长到必定程度(默认为15岁,能够经过参数-XX:MaxTenuringThreshold来设置)时,就会被晋升到老年代中

动态对象年龄断定

为了可以更好地适应不一样程序地内存情况,虚拟机并不老是要求对象地年龄必须达到MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄

空间分配担保

在发生Minor GC时,虚拟机会监测以前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,若是大于,则改成直接进行一次Full GC。若是小于,则查看HanlePromotionFailure设置是否容许担保失败;若是容许,那只会进行Minor GC;若是不容许,则也要进行一次Full GC

参考资料

周志明--《深刻理解Java虚拟机++JVM高级特性与最佳实践》和JVM系列(六)-JVM垃圾回收器

相关文章
相关标签/搜索