对象存活检测算法

在研究对象的回收之前,我们需要先看一下如何进行判断对象是否还有存活价值,即要先判断对象是否还有被引用。
堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)。在主流的商用程序语言中(Java和C#等),都是使用可达性分析算法(Reachability Analysis)来判断对象是否存活的,但是又有很多人认为是用引用计数算法(Reference Counting)来判断。接下来将 分别介绍这两种算法。

引用计数(Reference Counting)算法

引用计数算法实现思想如下:给对象中添加一个引用计数器,每当有一个地方引用该对象时,其计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法实现简单,判定高效,在大部分情况下她都是一个不错的算法,也有一些比较著名的应用案例,如微软公司的COM(Component Object Model)技术、Python语言和在游戏脚本领域被广泛应用的Squirrel都使用引用计数算法进行内存管理。但是,引用计数算法无法解决对象之间循环引用的问题,也即ABBA问题。
之所以在COM技术、Python等使用该技术,是因为其已找到对应的解决策略或定位循环引用的方法。能想到的一种简单的方法是通过分层的概念,来避免循环引用。强制约束同层之间不能相互引用。也可以通过判断算法判断是否存在ABBA这种场景。还有一种方法就是规定一些根结点,这个根结点只能被外部引用,不存在引用他人的场景(这里将其成为引用原子性),这种算法思想也是“可达性分析算法”的基础。

可达性分析(Reachability Analysis)算法

在主流的商用程序语言中,都是通过可达性分析来判断对象是否存活的。这个算法的思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连接时(用图论的话来说,就是从GC Roots到这个对象不可达),就证明此对象是不可用的。示例如下:

在Java语言中,可以作为GC Roots对象的有:
(1)虚拟机栈(栈帧的本地变量表)中引用的对象;
(2)本地方法栈中JNI(Native方法)引用的对象;
(3)方法区中类静态属性(static修饰)引用的对象;
(4)方法区中常量(final修饰)引用的对象。

引用级别划分

引用计数算法和可达性分析算法通过管理引用来判断该对象是否存活。为了更好的进行内存管理,丰富对象的引用状态,更好的刻画现实世界,在JDK 1.2后,Java对引用的概念进行了扩充。根据引用对应垃圾回收的力度,引用可分为四种。从强到弱依次是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
(1)强引用
如果一个对象具有强引用,那垃圾回收器绝不会回收它。使用方式如下:

Object o = new Object(); // 强引用

如果不使用,尽量通过如下方式来弱化引用:

o = null; // 帮助垃圾收集器回收此对象

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,就可回收对象。
在方法内部的强引用,会在方法运行完成后退出虚拟机栈或本地方法栈,引用消失。此后这个Object可被回收。如果这个对象是全局变量,就需在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
(2)软引用
如果一个对象只具有软引用,当内存空间不足时,该引用对应的对象的内存将被回收。软引用可用来实现内存敏感的高速缓存。

String str=new String("abc");
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

当内存不足时,等价于:

String str=new String("abc");
if(JVM.内存不足()) {str = null;  // 转换为软引用System.gc(); // 垃圾回收器进行回收
}

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了:1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。这时就可以使用软引用。实例如下:

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){ rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了sr = new SoftReference(prev);       // 重新构建
}

(3)弱引用
如果一个对象只具有弱引用,不管内存空间足够与否,都会在执行GC时,回收对应内存。使用方式如下:

String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);

执行GC时,等价于:

String str=new String("abc");
str = null;
System.gc();

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那可用 Weak Reference 来引用此对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。
这个引用不会在对象的垃圾回收判断中产生任何附加的影响。
(4)虚引用
如果一个对象只具有虚引用,那么在任何时候,都有可能被GC。与其他几种引用都不同,虚引用并不会决定对象的生命周期。虚引用主要用来跟踪对象被垃圾回收器回收的活动,从而在对象被垃圾回收器回收时收到一个系统通知。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

垃圾收集算法

不同垃圾回收器,可能采用不同的垃圾回收算法。这里,重点介绍几种常用的垃圾回收算法。
原文链接

标记-清除(Mark-Sweep)算法

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。该算法的思想是:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。
标记-清除算法存在两点不足:1.标记和清除过程效率不高;2.会产生空间碎片问题。
1.标记和清除过程效率低下问题
使用“对象存活检测算法”对需要回收的对象进行标记的过程,以及从Java堆中遍历需要被清除的对象,并进行清除。因需要进行遍历,所以效率不高。
2.空间碎片问题
清除可回收对象仅仅是将该对象占用的内存回收,所以会产生不连续空间碎片。当需要分配较大对象时,无法找到足够的内存而不得不提前触发另一次垃圾回收。
标记-清除算法使用的方法是最简单的。但标记阶段完成后,未被访问到的对象需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的性能。
标记-清除算法执行过程实例如下:

复制算法

复制算法是为了解决标记-清除算法的效率问题。其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当其中一块内存用完或不足以分配给下一个对象时,就将该内存块中存活的对象复制到另一个内存块上面=,然后再把该内存块的空间清除。
优点:
内存分配时,顺序分配内存,无需考虑内存碎片问题。实现简单,运行高效。
缺点:
该算法的代价是将内存缩小为原来的一半,内存利用率过低。
复制算法执行实例如下:

现代商业虚拟机都采用该算法来回收新生代。由于新生代中绝大部分对象存活时间较短,所以无需按照1:1的比例来划分内存空间。而是将内存分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。HotSpot虚拟机中默认Eden和Survivor的大小比例是8:1。

Eden 区

IBM 公司的专业研究表明,98%的对象是朝生夕死。所以,大多数情况下,对象会在Eden分配,当Eden没有足够空间时,虚拟机会发起一次Minor GC。在Minor GC中,Eden 会被清空,对于无需回收的对象将会进入到Survivor的From区。

Survivor 区

Survivor 区相当于Eden 和 Old 区的缓冲,暂存存活时间不是很多的对象。
(1)为什么需要 Survivor 区?
Eden 区回收后存活对象如果直接进入Old区,会导致Old 区很快沾满,且存活对象在第二次或三次回收时,就可被清除。
Survior 区进行预筛选,只有经历16次Minor GC的对象才能被送到老年代。
一个例外:如果Survivor 区不足以存放Eden区和另外一个Survivor区的存活对象,那么这些对象将直接进入Old区。
(2)为什么需要两个Survivor区?
如果只有一个Survivor区,在执行Minor GC时,既要考虑将Eden 区存活对象放置到该Survivor区,还需要考虑Survivor区中剩余存活对象。因为这种场景下,只能使用标记清除算法,会带来内存碎片问题。
如果有两个Survivor区,则可将Eden 区存活对象和其中一个Survivor区的剩余存活对象(上一次Minor GC存活对象的再次筛选或存活的对象)复制到另一个Survivor区。
(3)为什么不是多个Survivor区?
Survivor区数量越多,每个Survivor区的Size就越小,越容易导致Survivor区满。两个Survivor区是权衡后的最佳方案。
(4) 无需回收的对象的Size大于Survivor怎么办?
如果无需回收对象的Size大于Survivor(Survivor无法完全存储无需回收的对象),部分无法安置的对象会直接进入老年代。(内存担保机制)

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的内存空间,就需要额外的空间进行分配担保,以应对内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法与标记-清除算法类似,只是在标记后,不是对未标记的内存区域进行清理,而是让所有的存活对象都向一端移动,然后清理掉边界外的内存。
标记-整理算法实例如下:

分代收集算法

分代收集算法就是根据对象存活周期的不同,将内存划分几块。一般是将Java堆分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,经常选用复制算法。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法来进行回收。
分代收集算法实例如下:

图片链接
参考链接
Java堆主要分为两个区域-新生代和老年代,其中新生代占据1/3的内存空间,老年代占据2/3的内存空间。

HotSpot发起内存回收简介

商用虚拟机在实现GC时,必须对算法的执行效率进行考量,以保证虚拟机高效运行。在发起内存回收时,HotSpot采用以下优化策略:枚举根节点、定义安全点、定义安全区域。
可达性分析算法虽然能定位可回收对象,但是存在以下问题:
(1)明确GC Roots耗时
可达性分析的基础是明确GC Roots节点。可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
(2)GC停顿不可避免
可达性分析对执行时间的敏感还体现在GC停顿上。因为这项分析工作必须在一个能确保一致性的快照中进行。这里的“一致性”是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有
Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

枚举根节点

由于目前的主流Java虚拟机使用的都是准确式GC(参考Exact VM对Classic VM的改进),所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。
在HotSpot中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
这样,算法的时间复杂度就从O(n)优化到O(1),且不会占用过多的额外内存。

定义安全点(Safepoint)

OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
实际上,HotSpot并没有为每条指令都生成OopMap,而是在“特定的位置”记录这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。安全点的选定是以“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
对于Sefepoint,还需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。
抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
主动式中断是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

定义安全区域(Safe Region)

Safepoint机制保证在程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”(没有分配CPU时间,也即线程处于Sleep状态或者Blocked状态),这时线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
在线程执行到Safe Region代码时,首先标识自己已经进入了Safe Region。当在这段时间里JVM要发起GC时,就不用管标识为Safe Region状态的线程。在线程要离开Safe Region时,要检查系统是否已经完成根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

常用垃圾收集器简介

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定。基于JDK1.7 Update 14 之后的HotSpot虚拟机,所包含的收集器如下图所示:

可见,HotSpot采用了七种种垃圾收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。Hotspot之所以实现如此多的收集器,是因为目前并无完美的收集器出现,只能选择对具体应用最适合的收集器。
参考链接1
参考链接2

Serial收集器

Serial(串行)收集器是最基本、发展历史最悠久,基于复制算法的新生代收集器,是JDK 1.3.1之前新生代收集器的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接受的。Serial收集器的运行示意图如下:

尽管“Stop The World”会导致服务不可用,且HotSpot开发团队为消除或减少停顿而不断努力(从Parallel收集器到Concurrent Mark Sweep收集器,再到Garbage first收集器),在桌面级别应用场景,待收集内存不会太大,停顿时间完全可控制在几十毫秒,最多一百多毫秒。而且,作为单线程收集器,Serial收集器可以获得最高的单线程收集效率。

ParNew收集器

ParNew同样用于新生代,是Serial的多线程版本,并且在参数、算法(同样是复制算法)上也完全和Serial相同。
Par是Parallel的缩写,但它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作,然后多线程执行垃圾回收。ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
ParNew收集器相比Serial收集器,仅实现基于多线程的GC,但它却是Server模式下的首选新生代收集器。其中一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器配合工作。
ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
支持参数控制,以及自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。
此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
(1)在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
它的工作流程与Serial收集器相同。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。该收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:

CMS(Concurrent Mark Sweep)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。CMS收集器工作的整个流程分为以下4个步骤:
(1)初始标记(Initial mark):仅标记GC Roots能直接关联到的对象,速度很快(准确式内存),需要“Stop The World”。
(2)并发标记(Concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长,并发执行。
(3)重新标记(Remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
(4)并发清除(Concurrent sweep):对已标记的垃圾进行GC,并发执行。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS收集器的运作步骤中并发和需要停顿的时间:
CMS是一款优秀的收集器,起主要优点体现在:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。 但是CMS收集器存在以下缺点:
(1)对CPU资源非常敏感。因面向并发设计程序,所以对CPU资源比较敏感。在多核时代,这个缺点已转换成优点。但在单核处理器场景下,则要慎重考虑。
(2)无法处理浮动垃圾(Floating Garbage)。 “浮动垃圾”是指在CMS并发清理阶段,用户线程运行产生的新垃圾。这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。“浮动垃圾”会导致出现“Concurrent Mode Failure”,进而引发另一次Full GC。同时,由于垃圾收集阶段用户线程还需运行,所以必须预留足够的内存空间给用户线程使用。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
(3)标记-清除算法导致的空间碎片。CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来不便。可以通过设置参数,决定执行合并整理的时机。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的可预测停顿时间的垃圾收集器。G1具备如下特点:
(1)并行与并发。G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间。
(2)分代收集。分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
(3)空间整合。G1从整体来看是基于“标记-整理”算法的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
(4)可预测的停顿。这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)垃圾收集器的特征。
横跨整个堆内存。G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离,而都是一部分Region(不需要连续)的集合。
建立可预测的时间模型。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描——Remembered Set。G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运作大致可划分为以下几个步骤:
(1)初始标记(Initial Mark)。仅标记 GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region中创建对象,此阶段需要停顿线程(Stop the World),但耗时很短。
(2)并发标记(Concurrent Mark)。从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final Mark)。为修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程(Stop the World),但可并行执行。
(4)筛选回收(Live Data Count and Evacuation)。 对各个Region中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
G1 收集器的运作步骤如下:

内存分配与回收优化策略

对象优先在Eden分配

大对象直接进入老年代

超过3MB的对象 ? == 大对象

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

超过15岁 == 16岁

Survivor空间过半原则

同年龄对象达到Survivor空间的一半,可直接进入老年代

空间分配担保

使用老年代进行担保,当Survivor无法容纳的对象直接进入老年代 # 对象存活检测算法
在研究对象的回收之前,我们需要先看一下如何进行判断对象是否还有存活价值,即要先判断对象是否还有被引用。
堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)。在主流的商用程序语言中(Java和C#等),都是使用可达性分析算法(Reachability Analysis)来判断对象是否存活的,但是又有很多人认为是用引用计数算法(Reference Counting)来判断。接下来将 分别介绍这两种算法。

引用计数(Reference Counting)算法

引用计数算法实现思想如下:给对象中添加一个引用计数器,每当有一个地方引用该对象时,其计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法实现简单,判定高效,在大部分情况下她都是一个不错的算法,也有一些比较著名的应用案例,如微软公司的COM(Component Object Model)技术、Python语言和在游戏脚本领域被广泛应用的Squirrel都使用引用计数算法进行内存管理。但是,引用计数算法无法解决对象之间循环引用的问题,也即ABBA问题。
之所以在COM技术、Python等使用该技术,是因为其已找到对应的解决策略或定位循环引用的方法。能想到的一种简单的方法是通过分层的概念,来避免循环引用。强制约束同层之间不能相互引用。也可以通过判断算法判断是否存在ABBA这种场景。还有一种方法就是规定一些根结点,这个根结点只能被外部引用,不存在引用他人的场景(这里将其成为引用原子性),这种算法思想也是“可达性分析算法”的基础。

可达性分析(Reachability Analysis)算法

在主流的商用程序语言中,都是通过可达性分析来判断对象是否存活的。这个算法的思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连接时(用图论的话来说,就是从GC Roots到这个对象不可达),就证明此对象是不可用的。示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KlhGN7cY-1611731986177)(…/img/可达性分析.png)]
在Java语言中,可以作为GC Roots对象的有:
(1)虚拟机栈(栈帧的本地变量表)中引用的对象;
(2)本地方法栈中JNI(Native方法)引用的对象;
(3)方法区中类静态属性(static修饰)引用的对象;
(4)方法区中常量(final修饰)引用的对象。

引用级别划分

引用计数算法和可达性分析算法通过管理引用来判断该对象是否存活。为了更好的进行内存管理,丰富对象的引用状态,更好的刻画现实世界,在JDK 1.2后,Java对引用的概念进行了扩充。根据引用对应垃圾回收的力度,引用可分为四种。从强到弱依次是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
(1)强引用
如果一个对象具有强引用,那垃圾回收器绝不会回收它。使用方式如下:

Object o = new Object(); // 强引用

如果不使用,尽量通过如下方式来弱化引用:

o = null; // 帮助垃圾收集器回收此对象

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,就可回收对象。
在方法内部的强引用,会在方法运行完成后退出虚拟机栈或本地方法栈,引用消失。此后这个Object可被回收。如果这个对象是全局变量,就需在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
(2)软引用
如果一个对象只具有软引用,当内存空间不足时,该引用对应的对象的内存将被回收。软引用可用来实现内存敏感的高速缓存。

String str=new String("abc");
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

当内存不足时,等价于:

String str=new String("abc");
if(JVM.内存不足()) {str = null;  // 转换为软引用System.gc(); // 垃圾回收器进行回收
}

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了:1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。这时就可以使用软引用。实例如下:

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){ rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了sr = new SoftReference(prev);       // 重新构建
}

(3)弱引用
如果一个对象只具有弱引用,不管内存空间足够与否,都会在执行GC时,回收对应内存。使用方式如下:

String str=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(str);

执行GC时,等价于:

String str=new String("abc");
str = null;
System.gc();

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那可用 Weak Reference 来引用此对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。
这个引用不会在对象的垃圾回收判断中产生任何附加的影响。
(4)虚引用
如果一个对象只具有虚引用,那么在任何时候,都有可能被GC。与其他几种引用都不同,虚引用并不会决定对象的生命周期。虚引用主要用来跟踪对象被垃圾回收器回收的活动,从而在对象被垃圾回收器回收时收到一个系统通知。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

垃圾收集算法

不同垃圾回收器,可能采用不同的垃圾回收算法。这里,重点介绍几种常用的垃圾回收算法。
原文链接

标记-清除(Mark-Sweep)算法

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。该算法的思想是:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。
标记-清除算法存在两点不足:1.标记和清除过程效率不高;2.会产生空间碎片问题。
1.标记和清除过程效率低下问题
使用“对象存活检测算法”对需要回收的对象进行标记的过程,以及从Java堆中遍历需要被清除的对象,并进行清除。因需要进行遍历,所以效率不高。
2.空间碎片问题
清除可回收对象仅仅是将该对象占用的内存回收,所以会产生不连续空间碎片。当需要分配较大对象时,无法找到足够的内存而不得不提前触发另一次垃圾回收。
标记-清除算法使用的方法是最简单的。但标记阶段完成后,未被访问到的对象需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的性能。
标记-清除算法执行过程实例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZDxifxR-1611731986184)(…/img/标记-清除算法.jpg)]

复制算法

复制算法是为了解决标记-清除算法的效率问题。其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当其中一块内存用完或不足以分配给下一个对象时,就将该内存块中存活的对象复制到另一个内存块上面=,然后再把该内存块的空间清除。
优点:
内存分配时,顺序分配内存,无需考虑内存碎片问题。实现简单,运行高效。
缺点:
该算法的代价是将内存缩小为原来的一半,内存利用率过低。
复制算法执行实例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mx7bG8L0-1611731986187)(…/img/复制算法.jpg)]
现代商业虚拟机都采用该算法来回收新生代。由于新生代中绝大部分对象存活时间较短,所以无需按照1:1的比例来划分内存空间。而是将内存分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。HotSpot虚拟机中默认Eden和Survivor的大小比例是8:1。

Eden 区

IBM 公司的专业研究表明,98%的对象是朝生夕死。所以,大多数情况下,对象会在Eden分配,当Eden没有足够空间时,虚拟机会发起一次Minor GC。在Minor GC中,Eden 会被清空,对于无需回收的对象将会进入到Survivor的From区。

Survivor 区

Survivor 区相当于Eden 和 Old 区的缓冲,暂存存活时间不是很多的对象。
(1)为什么需要 Survivor 区?
Eden 区回收后存活对象如果直接进入Old区,会导致Old 区很快沾满,且存活对象在第二次或三次回收时,就可被清除。
Survior 区进行预筛选,只有经历16次Minor GC的对象才能被送到老年代。
一个例外:如果Survivor 区不足以存放Eden区和另外一个Survivor区的存活对象,那么这些对象将直接进入Old区。
(2)为什么需要两个Survivor区?
如果只有一个Survivor区,在执行Minor GC时,既要考虑将Eden 区存活对象放置到该Survivor区,还需要考虑Survivor区中剩余存活对象。因为这种场景下,只能使用标记清除算法,会带来内存碎片问题。
如果有两个Survivor区,则可将Eden 区存活对象和其中一个Survivor区的剩余存活对象(上一次Minor GC存活对象的再次筛选或存活的对象)复制到另一个Survivor区。
(3)为什么不是多个Survivor区?
Survivor区数量越多,每个Survivor区的Size就越小,越容易导致Survivor区满。两个Survivor区是权衡后的最佳方案。
(4) 无需回收的对象的Size大于Survivor怎么办?
如果无需回收对象的Size大于Survivor(Survivor无法完全存储无需回收的对象),部分无法安置的对象会直接进入老年代。(内存担保机制)

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的内存空间,就需要额外的空间进行分配担保,以应对内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法与标记-清除算法类似,只是在标记后,不是对未标记的内存区域进行清理,而是让所有的存活对象都向一端移动,然后清理掉边界外的内存。
标记-整理算法实例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ucDEA72-1611731986190)(…/img/标记-整理算法.jpg)]

分代收集算法

分代收集算法就是根据对象存活周期的不同,将内存划分几块。一般是将Java堆分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,经常选用复制算法。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法来进行回收。
分代收集算法实例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYAQOyoH-1611731986191)(…/img/分代收集算法.jpg)]
图片链接
参考链接
Java堆主要分为两个区域-新生代和老年代,其中新生代占据1/3的内存空间,老年代占据2/3的内存空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVoIJi3W-1611731986192)(…/img/Java堆空间分配.png)]

HotSpot发起内存回收简介

商用虚拟机在实现GC时,必须对算法的执行效率进行考量,以保证虚拟机高效运行。在发起内存回收时,HotSpot采用以下优化策略:枚举根节点、定义安全点、定义安全区域。
可达性分析算法虽然能定位可回收对象,但是存在以下问题:
(1)明确GC Roots耗时
可达性分析的基础是明确GC Roots节点。可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
(2)GC停顿不可避免
可达性分析对执行时间的敏感还体现在GC停顿上。因为这项分析工作必须在一个能确保一致性的快照中进行。这里的“一致性”是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有
Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

枚举根节点

由于目前的主流Java虚拟机使用的都是准确式GC(参考Exact VM对Classic VM的改进),所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。
在HotSpot中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
这样,算法的时间复杂度就从O(n)优化到O(1),且不会占用过多的额外内存。

定义安全点(Safepoint)

OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
实际上,HotSpot并没有为每条指令都生成OopMap,而是在“特定的位置”记录这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。安全点的选定是以“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
对于Sefepoint,还需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。
抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
主动式中断是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

定义安全区域(Safe Region)

Safepoint机制保证在程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”(没有分配CPU时间,也即线程处于Sleep状态或者Blocked状态),这时线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
在线程执行到Safe Region代码时,首先标识自己已经进入了Safe Region。当在这段时间里JVM要发起GC时,就不用管标识为Safe Region状态的线程。在线程要离开Safe Region时,要检查系统是否已经完成根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

常用垃圾收集器简介

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定。基于JDK1.7 Update 14 之后的HotSpot虚拟机,所包含的收集器如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LfYpRlSd-1611731986196)(…/img/HotSpot的垃圾收集器.jpg)]
可见,HotSpot采用了七种种垃圾收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。Hotspot之所以实现如此多的收集器,是因为目前并无完美的收集器出现,只能选择对具体应用最适合的收集器。
参考链接1
参考链接2

Serial收集器

Serial(串行)收集器是最基本、发展历史最悠久,基于复制算法的新生代收集器,是JDK 1.3.1之前新生代收集器的唯一选择。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接受的。Serial收集器的运行示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiXaXkAN-1611731986197)(…/img/Serial收集器.jpeg)]
尽管“Stop The World”会导致服务不可用,且HotSpot开发团队为消除或减少停顿而不断努力(从Parallel收集器到Concurrent Mark Sweep收集器,再到Garbage first收集器),在桌面级别应用场景,待收集内存不会太大,停顿时间完全可控制在几十毫秒,最多一百多毫秒。而且,作为单线程收集器,Serial收集器可以获得最高的单线程收集效率。

ParNew收集器

ParNew同样用于新生代,是Serial的多线程版本,并且在参数、算法(同样是复制算法)上也完全和Serial相同。
Par是Parallel的缩写,但它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作,然后多线程执行垃圾回收。ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7xfswgDF-1611731986200)(…/img/ParNew收集器.jpeg)]
ParNew收集器相比Serial收集器,仅实现基于多线程的GC,但它却是Server模式下的首选新生代收集器。其中一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器配合工作。
ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
支持参数控制,以及自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。
此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
(1)在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
它的工作流程与Serial收集器相同。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。该收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sEosJRRR-1611731986201)(…/img/Parallel-Old收集器.jpeg)]

CMS(Concurrent Mark Sweep)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。CMS收集器工作的整个流程分为以下4个步骤:
(1)初始标记(Initial mark):仅标记GC Roots能直接关联到的对象,速度很快(准确式内存),需要“Stop The World”。
(2)并发标记(Concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长,并发执行。
(3)重新标记(Remark):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
(4)并发清除(Concurrent sweep):对已标记的垃圾进行GC,并发执行。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS收集器的运作步骤中并发和需要停顿的时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43BPkfmn-1611731986202)(…/img/CMS收集器.jpeg)]
CMS是一款优秀的收集器,起主要优点体现在:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。 但是CMS收集器存在以下缺点:
(1)对CPU资源非常敏感。因面向并发设计程序,所以对CPU资源比较敏感。在多核时代,这个缺点已转换成优点。但在单核处理器场景下,则要慎重考虑。
(2)无法处理浮动垃圾(Floating Garbage)。 “浮动垃圾”是指在CMS并发清理阶段,用户线程运行产生的新垃圾。这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。“浮动垃圾”会导致出现“Concurrent Mode Failure”,进而引发另一次Full GC。同时,由于垃圾收集阶段用户线程还需运行,所以必须预留足够的内存空间给用户线程使用。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
(3)标记-清除算法导致的空间碎片。CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来不便。可以通过设置参数,决定执行合并整理的时机。

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的可预测停顿时间的垃圾收集器。G1具备如下特点:
(1)并行与并发。G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间。
(2)分代收集。分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
(3)空间整合。G1从整体来看是基于“标记-整理”算法的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
(4)可预测的停顿。这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)垃圾收集器的特征。
横跨整个堆内存。G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离,而都是一部分Region(不需要连续)的集合。
建立可预测的时间模型。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描——Remembered Set。G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运作大致可划分为以下几个步骤:
(1)初始标记(Initial Mark)。仅标记 GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region中创建对象,此阶段需要停顿线程(Stop the World),但耗时很短。
(2)并发标记(Concurrent Mark)。从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记(Final Mark)。为修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程(Stop the World),但可并行执行。
(4)筛选回收(Live Data Count and Evacuation)。 对各个Region中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
G1 收集器的运作步骤如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUvfMeZ6-1611731986205)(…/img/G1收集器.jpeg)]

内存分配与回收优化策略

对象优先在Eden分配

大对象直接进入老年代

超过3MB的对象 ? == 大对象

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

超过15岁 == 16岁

Survivor空间过半原则

同年龄对象达到Survivor空间的一半,可直接进入老年代

空间分配担保

使用老年代进行担保,当Survivor无法容纳的对象直接进入老年代

Java虚拟机垃圾回收相关推荐

  1. JAVA虚拟机垃圾回收机制和JAVA排错三剑客

    一.Java虚拟机逻辑回收机制 1.Java垃圾回收器 Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Al ...

  2. Java虚拟机垃圾回收相关知识点全梳理(下)

    2019独角兽企业重金招聘Python工程师标准>>> 一.前言 上一篇文章<Java虚拟机垃圾回收相关知识点全梳理(上)>我整理分享了JVM运行时数据区域的划分,垃圾判 ...

  3. 了解java虚拟机—垃圾回收算法(5)

    引用计数器法(Reference Counting) 引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器减1.只要对象A的引用计数器的 ...

  4. Java虚拟机 —— 垃圾回收机制

    在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中.如果在Java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致 ...

  5. Java虚拟机-垃圾回收简介

    一.如何判定对象为垃圾对象 -verbose:gc 打印垃圾回收简单信息参数 -xx:+PringDCDetail 打印垃圾回收的详细信息 引用计数法 引用计数算法很简单,它实际上是通过在对象头中分配 ...

  6. JAVA虚拟机垃圾回收算法原理

    除了释放不再被引用的对象外,垃圾收集器还要处理堆碎块.新的对象分配了空间,不再被引用的对象被释放,所以堆内存的空闲位置介于活动的对象之间.请求分配新对象时可能不得不增大堆空间的大小,虽然可以使用的总空 ...

  7. 深入理解Java虚拟机垃圾回收机制

    文章目录 什么是垃圾回收 哪些内存需要被回收?什么时候回收?如何回收? 哪些内存需要被回收?什么时候回收? 引用计数算法 可达性分析算法 如何回收?(垃圾收集算法) 标记-清除算法 复制算法 标记-整 ...

  8. 老生常谈Java虚拟机垃圾回收机制(必看篇)

    垃圾收集 垃圾收集主要是针对堆和方法区进行. 程序计数器.虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收. 判断一个对 ...

  9. java虚拟机垃圾回收被误解的7件事

    对Java垃圾回收最大的误解是什么?它实际又是什么样的呢? 当 我还是小孩的时候,父母常说如果你不好好学习,就只能去扫大街了.但他们不知道的是,清理垃圾实际上是很棒的一件事.可能这也是即使在Java的 ...

  10. 一篇文章教你弄懂java CMS垃圾回收日志

    文章目录 一.CMS垃圾回收器介绍 二.CMS JVM运行参数 三.CMS收集器运行过程 1.初始标记(CMS initial mark) 2.并发标记(CMS concurrent mark) 3. ...

最新文章

  1. wget 命令用法详解
  2. python把桢写入txt_Java 字节码与字节码分析
  3. 征战蓝桥 —— 2016年第七届 —— C/C++A组第10题——最大比例
  4. cvpr 深度估计_干货 | 2019 到目前为止的深度学习研究进展汇总
  5. “央视boys” 四人带货超5亿:权来康康,撒开了买
  6. 添加class值_Java 虚拟机(二) - Class 文件结构
  7. c语言自定义函数案例情景,第4周 C语言及程序设计提高例程-3 体验自定义函数...
  8. 支持了Unicode及各国字符集编码识别]改善IDA6.8对中文等非英语国家的ANSI字符串显示支持不佳的问题...
  9. mssql 查询无记录时sum_SQL常见面试题查询
  10. Java内存模型中的happen-before是什么?
  11. 浪潮存储实至名归,通用存储用户评测排名全球榜首
  12. 多伦多大学计算机专音乐专业,终于懂了加拿大音乐专业学院推荐
  13. 论文阅读:Regularizing Deep Networks with Semantic Data Augmentation
  14. 【转】如何评价 Apple 新推出的编程语言 Swift?
  15. MATLAB 与 音频处理 相关内容摘记
  16. 史上最简单的rar压缩包文档解密方法,rar压缩包权限密码如何解开?
  17. html中蝴蝶飞飞怎么制作,【幼儿园折纸蝴蝶教案】手工折纸蝴蝶教案_幼儿园手工蝴蝶教案_亲亲宝贝网...
  18. 官网下载最新版本Spring
  19. Docker swarm 通过 docker-compose 部署应用
  20. [6 函数子类及函数] 42. 确保less<T>与operator<具有相同的语义(POLA)

热门文章

  1. 容器技术Docker K8s 34 容器服务ACK基础与进阶-安全管理
  2. 极客大学架构师训练营 网络通信协议 非阻塞网络I/O NIO 数据库架构原理 第16课 听课总结
  3. 单片机用C语言锯齿波,试用c语言编写一个能输出锯齿波信号的单片机c51程序
  4. Lattice - 规划模块 1.采样轨迹 2.计算轨迹cost 3 循环检测筛选轨迹
  5. 求小球落地5次后所经历的路程和第5次反弹的高度
  6. html5绘制警告牌,2.10 创建自定义图形:绘制扑克牌花色 - HTML5 Canvas 实战
  7. Linux进程突然挂死,小玩意:如何让linux上挂死的进程重启?
  8. 前端为什么有的接口明明是成功回调却执行了.catch失败回调_前端战五渣学JavaScript——Promise...
  9. modelsim安装_XLINUXFPGA开发工具篇modelsim的安装
  10. 【POJ 3057】Evacuation【最大流+二分】