JVM——4.垃圾回收

这篇文章我没来讲一下JVM中的垃圾回收。这是比较重要,内容也比较多的一篇文章。

目录

1.垃圾回收概述

2.如何判断对象可以回收

2.1引用计数法

2.2可达性分析算法

2.2.1GCRoot的选取

2.3再谈引用

2.3.1强引用

2.3.2软引用

2.3.3弱引用

2.3.4虚引用

2.3.5终结器引用

2.3.6引用小结

3.垃圾回收算法

3.1分代收集理论

3.2标记清除算法

3.3标记复制算法

3.4标记整理算法

3.5GC的相关参数

4.相关的垃圾回收器

4.1垃圾回收器概述

4.2串行垃圾回收器

4.3吞吐量优先垃圾回收器

4.4响应时间优先的垃圾回收器

4.5CMS垃圾回收器

4.6G1垃圾回收器

5.内存分配与回收策略

5.1对象优先在Eden进行分配

5.2大对象直接进入老年代

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

5.4动态对象年龄判定

5.5空间分配担保

6.GC调优

6.1调优概述

6.2新生代调优

6.3老年代调优

7.小结


1.垃圾回收概述

垃圾回收,简称GC。在前面的文章中,我们讲述了JVM的内存分配,其中有一块区域叫,它是用来存放创建出来的对象的。我们一个程序在运行的过程中需要创建许多对象,而我们的JVM的内存空间是有限的,那么就必然会回收一下创建出来了,但是没有用到或者说后面不会再用到的对象,这就是jvm堆的垃圾回收概念

在jvm的垃圾回收中,我们主要讲解以下五方面内容:如何判断对象可以回收;垃圾回收算法;JVM的垃圾回收器;内存分配与垃圾回收策略;垃圾回收调优。其中每方面又包含许多小的方面的内容。

下面,我们一起来看一下具体的内容。

2.如何判断对象可以回收

JVM要进行垃圾回收,首先我们就要判断一个对象是不是垃圾,是不是可以回收。JVM给出了两种方法:引用计数法,可达性分析算法。

2.1引用计数法

很多教科书判断对象是否存活的算法是这样的:**在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。**他们对于这个问题给予的都是这个答案。

优点: 引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但 它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。

缺点:**** 这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题

解释:

代码如下所示:

如上图所示,A对象引用了B对象,B对象引用了A对象,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。

实际上现在的JVM已经不使用这种方法了。

2.2可达性分析算法

为了解决引用计数法中的问题,人们提出了可达性分析算法。

当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是 通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

核心思路:通过 一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为**"引用链"**(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的

**如下图所示:**对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

解释: 可达性分析算法首先要确定一些跟对象,即GC Root,我们可以将这个GC Root理解为是肯定不会被当做垃圾回收的对象。在进行垃圾回收之前,JVM会对堆中的所有元素进行一遍扫描,看这些元素是否被跟对象直接或间接的引用,如果有引用,那就不被当做垃圾回收,如果没有引用,就当做垃圾回收

2.2.1GCRoot的选取

可达性分析算法的关键就是选取GCRoot,下面我们来看一下哪些对象可以被选取作为GCRoot:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

以上列举的对象就是可以作为跟对象的对象,需要了解。除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象"临时性"地加入,共同构成完整GC Roots集合。

2.3再谈引用

在JDK 1.2版之前,Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有"被引用"或者"未被引用"两种状态,对于描述一些"食之无味,弃之可惜"的对象就显得无能为力。

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

下面通过一张图来看一下这几种引用:

2.3.1强引用

强引用是最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj=new Object()"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

**解释:**简单来说,就是我们的对象被跟对象直接或间接的引用,这就是强引用。只要强引用还在,那么GC时就不会回收该对象

2.3.2软引用

软引用是用来描述一些还有用,但非必须的对象只被****软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

解释:如果一个对象只被 软引用所引用,那么在垃圾回收时,**如果回收后内存空间依然不够,那么就进行第二次垃圾回收,这次垃圾回收就要回收这些软引用。但是,如果垃圾回收时,回收后内存空间够了,那么软引用所引用的对象就不会被回收。**注意:这只是针对只有软引用所引用的对象的,如果该对象还有强引用引用,那么不管垃圾回收后内存是否足够,该对象都不会被回收。

2.3.3弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,**被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。**当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

**解释:**弱引用和强引用非常像,唯一区别就是,只要垃圾回收时,那些只被弱引用所引用的对象就会被回收。

2.3.4虚引用

虚引用也称为**"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知**。在JDK1.2版之后提供了PhantomReference类来实现虚引用

2.3.5终结器引用

我们知道,所有的类都有一个父类,即Object类,Object类中有一个方法叫finallize(),当我们的对象重写了这个方法时,如果此对象没有强引用直接引用,那么JVM给给其创建一个终结器引用对象,在进行垃圾回收的时候,终结器引用对象会先进入引用队列中,然后会有一个优先级很低的线程来查看引用队列,如果这个引用队列中有终结器引用,那么这个线程就会更加终结器引用找到这个对象,然后执行它的finallize()方法,然后在下一次垃圾回收的时候,会将这个对象的空间进行回收。这就是终结器引用。

2.3.6引用小结

下面就对上面写到的几种引用小结一下:

3.垃圾回收算法

上面我们讲的只是如何判断这个对象是不是死的,即如何判断这个对象是不是可以回收的,讲了两个方法,然后提到了5中引用。下面,我们来看一下如何回收这些死去的对象,即回收这些垃圾的时候用到的算法。

3.1分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了**"分代收集"** (Generational Collection)的理论进 行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分 出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ------因而才有了"Minor GC"、"Major GC"、"Full GC"这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法。

分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为**新生代 (Young Generation)和老年代(Old Generation)**两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放

其实我们只要仔细思考一下,也很容易发现分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用

假如要现在进行一次只局限于新生代区域内的收集(Minor GC) ,但新生代中的对象是完全有可 能被老年代所引用的 ,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象 的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。

这其实是可根据前两条假说逻辑推理得出的隐含推论:**存在互相引用关系的两个对象,是应该倾 向于同时生存或者同时消亡的。**举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以 消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录 每一个对象是否存在及存在哪些跨代引用,**只需在新生代上建立一个全局的数据结构(该结构被称 为"记忆集",Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会 存在跨代引用。**此后当发生Minor GC时,**只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。**虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数 据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

3.2标记清除算法

下面,我们来看一下标记清除算法

核心思路:如它的名字一样,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

**缺点:**它的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;**第二个是内存空间的碎片化问题,**标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

下面用一张图来表示一下:

3.3标记复制算法

标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年提出了一种称为"半区复制"(Semispace Copying)的垃圾收集算法。

核心思路:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。**当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。**如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

**优点:**实现简单,运行高效

缺点:这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点

下面用图来解释一下:

**现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公司曾有一项专门研 究对新生代"朝生夕灭"的特点做了更量化的诠释------新生代中的对象有98%**熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。

在1989年,Andrew Appel针对具备"朝生夕灭 "特点的对象,提出了一种更优化的半区复制分代策 略 ,现在称为"Appel式回收"

具体做法:新生代 分为一块较大的Eden空间(即伊甸园)和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被"浪费"的。

当然,98%的对象可被回收仅仅是"普通场景"下测得的数据,**任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,**因此Appel式回收还有一个充当罕见情况的"逃生门"的安 全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实 际上大多就是老年代)进行分配担保(Handle Promotion)。

内存的分配担保和银行贷款一样,如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。

3.4标记整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果 不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,人们提出了标记整理算法。

核心思路:该算法其中的标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记-清除算法与标记-整理算法的本质差异****在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机 设计者形象地描述为"Stop The World" 。

但如果跟标记-清除算法 那样完全不考虑移动和整理存活对象的话,**弥散于堆中的存活对象导致的 空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。**内存的访问是用户程序最频繁的操作,甚至都没有之 一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。

基于以上两点,是否移动对象都存在弊端,**移动则内存回收时会更复杂,不移动则内存分配时会 更复杂。**从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算

下面用一张图来看一下:

3.5GC的相关参数

下面介绍一下GC的相关参数:

都是可以在编辑器中进行配置的。

4.相关的垃圾回收器

前面我们介绍了如何判断对象已死,讲了2中方法,然后讲了一下引用,之后我们讲了如何进行垃圾回收,即垃圾回收算法,讲了3种,其中重点要掌握标记复制算法,特别是标记复制算法在新生代中的运用。下面,我们再来看一下垃圾回收器。

4.1垃圾回收器概述

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规 范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含 的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。

我们可以对垃圾回收器做出下面的简单分类:

解释:

  1. 串行的垃圾回收器,它的底层是一个单线程的垃圾回收器,它在运行的时候要求暂停别的所有线程,然后该垃圾回收器开始进行垃圾回收。
  2. 吞吐量优先的垃圾回收器就是要求在单位时间内,STW的时间最短,举例说明,假设一个小时内发生了2次垃圾回收,它不要求每一次的垃圾回收时间最短,它要求的是次数最少。
  3. 响应时间优先的垃圾回收器与吞吐量优先的垃圾回收器相反,它要求的是每一次垃圾回收的STW时间最短,它不要求一小时内的垃圾回收次数。

下面就详细的看一下。

4.2串行垃圾回收器

下面用一张图来看一下串行的垃圾回收器:

其中串行收集器最典型的就是Serial收集器。

4.3吞吐量优先垃圾回收器

先用一张图来了解一下吞吐量优先的垃圾回收器

典型的吞吐量优先的垃圾回收器就是Parallel Scavenge和Parallel Old收集器。

4.4响应时间优先的垃圾回收器

典型的响应时间优先的垃圾回收器就是CMS。

4.5CMS垃圾回收器

前面,我们介绍了垃圾回收器的分类,分为三类。下面我们来介绍两种具体的垃圾回收器。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

从名字(包含"Mark Sweep")上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要"Stop The World"。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。 由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿

但是CMS有三个缺点

**首先,**CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏 感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能 力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。为了缓解这种情况,虚拟机提 供了一种称为"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种, 所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样, 是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的 时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变 慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式的CMS收集器效果很一般,从 JDK 7开始,i-CMS模式已经被声明为"deprecated",即已过时不再提倡用户使用,到JDK 9发布后iCMS模式被完全废弃。

**其次,**由于CMS收集器无法处理"浮动垃圾"(Floating Garbage),有可能出现"Con-current Mode Failure"失败进而导致另一次完全"Stop The World"的Full GC的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为"浮动垃圾"。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。

**最后,****因为CMS是基于标记清楚算法的一种垃圾回收器,所有它会产生大量的空间碎片,**空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

以上就是CMS垃圾回收器的主要内容。

4.6G1垃圾回收器

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式

G1是一款主要面向服务端应用的垃圾收集器

作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起"停顿时间模型 "的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特了。

那具体要怎么做才能实现这个目标呢?首先要有一个思想上的改变,在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理 论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区 域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它**将Region作为单次回收的最小单元,**即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。**更具体的处理思路是让G1收集器去跟踪各个Region里面的垃 圾堆积的"价值"大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,**每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是"Garbage First"名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

G1收集器的 运作过程大致可划分为以下四个步骤

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
  2. **并发标记(Concurrent Marking):**从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  3. **最终标记(Final Marking):**对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
  4. **筛选回收(Live Data Counting and Evacuation):**负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

以上便是G1垃圾回收器的内容。

5.内存分配与回收策略

前面我们讲了如何判断一个对象是否已死,讲了怎么回收死去的对象,也讲了具体是由什么来回收死去的对象即垃圾回收器。下面,我们再来看一下对象在内存中究竟是如何如何分配的,在回收这些对象的时候又要遵循哪些策略。

5.1对象优先在Eden进行分配

大多数情况下,对象会在新生代的Eden区中进行分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。Minor GC所用到的GC算法是标记复制算法。

这一点内容可以参考这标记复制算法来看。因为新生代的Eden区和标记复制算法常常是要联系在一起的。

5.2大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,**比遇到一个大对象更加坏的消息就是遇到一群"朝生夕灭"的"短命大对象",我们写程序的时候应注意避免。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。**HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。

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

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存 活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

5.4动态对象年龄判定

为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代**,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,**无须等到-XX: MaxTenuringThreshold中要求的年龄。

5.5空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败;如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC

解释一下"冒险"是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ------最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对 象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。

6.GC调优

下面来看一下GC调优。

GC调优的范围就很广了,首先你需要掌握GC相关的VM参数,然后你需要掌握相关的工具,之后就是要明确一点,调优跟应用、环境有关,没有放之四海皆准的发则。说白了,就是要具体问题具体分析。

这里,我们只是介绍一下调优的基本理论基础。至于实践,大家可以自己试一下。

6.1调优概述

我们调优一般就是要从以下几方面进行思考:

  • 内存
  • 锁竞争
  • cpu占用
  • io

调优之前,我们还要确定调优的目标:

  • 【低延迟】还是【高吞吐量】,选择合适的垃圾回收器
  • CMS,G1,ZGC
  • ParallelGC
  • Zing

除此之外,我们还要明确一点:最好的GC是不发生GC

我们在查看FullGC前后的内存占用,需要考虑下面几个问题:

以上,就是GC调优的概述,下面来看一下具体的。

6.2新生代调优

首先,我们需要了解的就是新生代的调优

下面看一下新生代调优规则:

  • 新生代不是越大越好,新生代能容纳所有【并发量*(请求-响应)】的数据
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让长时间存活对象尽快晋升

6.3老年代调优

下面看一下老年代调优:

7.小结

这篇文章,我们讲述了jvm中很重要的一项内容:垃圾回收(GC),下面小结一下。

JVM的垃圾回收,回收哪里的垃圾?主要回收堆中的垃圾。要进行垃圾回收,首先我们需要判断哪些对象是垃圾,怎么判断呢?有两种方法,引用计数法和可达性分析算法。引用计数法的优缺点是什么,可达性分析算法的原理是什么,哪些对象可以作为可达性分析算法的跟节点?可达性分析算法中的可达性可以理解为一种引用,所以我们需要再谈一下引用。现在,知道了如何判断一个对象是否是垃圾了,接下来就要回收它。怎么回收?就有哪些回收算法?有三种,每种的优缺点是什么样的?适用于哪些场景。现在知道怎么回收了,接下来就要具体的回收,这就要依靠JVM的垃圾回收器,主要有两种CMS和G1,每种的特点是什么,工作流程是什么,都要清楚。

现在,垃圾回收的主体内容讲述完了,我们再对其增砖加瓦。垃圾回收是回收堆中的对象,那么对象在堆中是怎么存储的?即jvm的堆内存分配策略是什么,我们在对其进行垃圾回收的时候又有哪些回收策略?都要清除。现在,当我们回收结束了,我们要如何对这次的垃圾回收进行调优?这也要了解。

以上就是我们jvm垃圾回收的主要内容。

相关推荐
东阳马生架构15 小时前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥21 小时前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇21 小时前
JavaAgent技术应用和原理:JVM持久化监控
jvm
程序员志哥1 天前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
聊聊volatile的实现原理?
java·jvm·redis
_LiuYan_1 天前
JVM执行引擎JIT深度剖析
java·jvm
王佑辉1 天前
【jvm】内存泄漏的8种情况
jvm
工业甲酰苯胺1 天前
JVM简介—1.Java内存区域
java·jvm·python
yuanbenshidiaos2 天前
c++---------数据类型
java·jvm·c++