JVM----垃圾收集器
1.根搜索算法
1.可达性分析算法
该算法是用来判断一些对象是否存活的,这个算法的起点就是图中的GC Root,,从根开始往下搜索,搜索走过的路径称之为“引用链”,如果一个对象到根节点没有任何一条引用链的话,就说明这个对象是要被回收的。
如图,Object5、Object6、Object7虽然他们互相关联,但是他们与根节点之间没有任务一条引用链,所以都是不可用对象!!
目前java中可作为GC Root的对象有:
1、 虚拟机栈中引用的对象(本地变量表)
2、 方法区中静态属性引用的对象
3、 方法区中常量引用的对象
4、 本地方法栈中引用的对象(Native对象)
2.枚举根节点
现在的很多应用都是十分巨大的,每次从根节点开始往下搜索引用,都是耗时十分庞大的工作!另外可达性分析对执行时间的敏感还体现在GC停顿上,每次要GC时,都会进行一次Stop The World,因为这样可以防止在GC Root进行搜索时有些引用还存在变化而导致的数据有误差。
在STW后,我们就要开始进行根节点枚举了,但是我们真的需要去一个不漏的检查完所有的执行上下文和全局的引用位置吗,不,并不要!就跟我们翻字典一样,我们找一个单词,我们需要从字典第一页开始翻到最后一页??那还不得累死啊,虚拟机也是这样认为的,太累了,所以虚拟机希望可以通过一种记录表,在枚举时可以快速查找到被引用的对象,这样工作效率会大大提升。于是OopMap的数据结构出现了,在类加载完后,对象内什么偏移量上是什么类型的数据都会记录在OopMap里。这样的话在根节点枚举时就可以得到这些信息了。
3.安全点
在OopMap的帮助下,虚拟机可以快速的完成根节点的枚举。从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停暂停所以活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。
4.安全区域
2.垃圾收集器
1.serial收集器
在经过虚拟机的可达性分析之后,垃圾回收就开始了(GC算法请看:https://www.cnblogs.com/Booker808-java/p/9063677.html),如果说回收算法是内存回收的方法理论,那么垃圾收集就是内存回收的具体实现。,不同版本的虚拟机提供的收集器都会有所差别,这里我们以jdk1.7之后的HotSpot虚拟机为例。
不过在讲解回收器之前,我们要明白的一点是,垃圾回收器没有完美的,只有适合不适合的!
首先是Serial收集器,它是发展历史比较久的一个,该收集器是个单线程收集器,这里的单线程并不是指系统只会分配一个线程去给他进行回收,而是指它在进行垃圾回收时,会停下其他所有工作线程,“Stop The World”。但是不好的地方就是,如果该回收器是服务于用户可能用着用着一个和用户交互性较强的系统,那么因为STW,用户可能会用着用着就突然死机几分钟,这样肯定是对用户不友好的!
如图,serial收集器工作流程图:
其实,虽然上面的描述使得serial收集器看起来是个不怎么好的收集器,但是如果是在一个单线程的环境下,这个收集器还是挺高效的,因为serial收集器没有线程交互的开销,相比于其他并发收集器在单线程环境下要更好。在用户的桌面应用场景中,因为给虚拟机分配的内存不会很大,而新生代的空间一般又要比老年代小一些,而Serial收集器又属于新生代收集器,所以在垃圾收集时尽管需要STW,但是因为新生代空间少,所以停顿一下也才几十毫秒或者一百多毫秒,也是可以接受的。
2.ParNew收集器
ParNew收集器就是serial收集器的一个多线程版本,也是属于新生代收集器,可以使用多线程来进行垃圾收集,其余行为例如:控制参数、收集算法、STW、对象分配原则、回收策略都与serial一模一样:
ParNew收集器是运行在server模式下的虚拟机中的首选新生代收集器,并且他还有个特点,就是除了serial之外,他是唯一一个可以CMS搭配的新生代收集器(CMS之后介绍),ParNew虽然是款多线程收集器,但是如果在单线程环境下(尽管现在几乎不可能存在单线程的系统),还可能不如单线程的serial收集器好使,因为存在线程交互之间的开销。但是如果在多线程的环境下,对于GC时系统资源的有效利用还是很不错的。
3.Parallel Scavenge收集器
Parallel Scavenge收集器是一款新生代的收集器,也是采用的复制算法的收集器,也是并行的多线程收集器,流程工作图和上面的ParNew收集器一样,但是此收集器的关注点和别的收集器可不一样,有点个性!
其他收集器的关注点是尽可能的缩短垃圾收集时间或者用户线程的停顿时间,但是Parallel Scavenge收集器的关注点是如何提高吞吐量,吞吐量就是指:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
停顿时间越短,说明越适合与用户进行交互,而吞吐量越高说明可以高效率的运用CPU时间,尽快的完成后端服务器的任务,所以该收集器适合应用在后台,并且该收集器又几个很重要的参数,这也是为什么这个收集器有个性的原因。重要的参数有三个,其中两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的 -XX:GCTimeRatio参数。另外一个是UseAdaptiveSizePolicy开关参数。MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
自适应性:Parallel Scavenge收集器能够配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
4.Serial Old收集器
这个收集器可以从名字里看出,这是属于老年代的收集器,他是个单线程的收集器,使用的标记整理算法。
前面的新生代能看得出是单线程的非并发的收集器Serial,而后面的就是我们的Serial Old收集器,它的意义也是给处于Client模式下的虚拟机使用的。
5.Parallel Old收集器
Parallel Old是Parallel Scavenge的老年版本,属于并行的多线程的老年代收集器,因为该收集器的出现,Parallel Scavenge收集器才有了一个绝佳的搭档:
该图是Parallel Scavenge和Parallel Old的搭配示意图,两个并发多线程的新生代和老年代的组合还是挺不错的哈!
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款以获取最短停顿时间为目标的收集器。目前很多大型的互联网项目都很重视服务响应速度,希望系统停顿时间越短越好,。而CMS收集器就很适合。
从名字可以看出CMS是基于标记清除算法的一款收集器,运作流程相对较复杂一点,总体可以分为四个步骤:
(1)初始标记(initial mark)(需要STW)
(2)并发标记(concurrent mark)
(3)重新标记(remark)(需要STW)
(4)并发清除(concurrent sweep)
初始标记仅仅是标记一下能和GC Roots直接关联的对象,速度是很快的,并发标记就是进行根节点的枚举,此时用户线程并不需要停下来。而重新标记就是修正并发标记阶段因用户程序的继续运作而导致标记产生变动那一部分对象的标记记录,这个阶段要比初始标记的时间长一些,但是比并发标记的时间要短。
因为并发标记和并发清除的时间在整个周期里算时间比较长的,但是他们都可以与用户线程一起工作,所以从总体来看,该收集器的回收过程和用户进程还是属于一起进行的,如图:
他的优点是很明显的,并发收集,低停顿,但是CMS也有几个缺点:
(1)CMS收集器对CPU资源很敏感,其实面向并发的程序都对CPU资源比较敏感。虽然不会导致用户线程停顿,但是因为占用了CPU资源,而导致吞吐量下降,CMS默认启动的回收线程数是(CPU数+3)/4,当CPU在4个以上,
(2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”,失败而导致另一次Full GC。因为并发清除过程,用户线程还在运作,伴随着程序的清理,一定还会有新的垃圾不断产生,这一部分因为是出现在并发标记之后的,所以收集器在当前的收集中是无法处理的,只能在下一次GC时来重新清理,这一部分的称为“浮动垃圾”。因为浮动垃圾的原因,收集阶段时还需要预留一部分空间给用户线程使用,所以我们的收集过程是不能等到老年代全部满了以后再去清理的,因为此时的用户线程还在执行,产生的对象就会无处可放。所以CMS收集器提供了参数用来设置空间百分比,如果CMS在收集期间发现预留的空间不够,就会导致“Concurrent Mode Failure”。
(3)CMS是采用的标记清除算法,,因为该算法,垃圾回收完之后会产生大量的内存碎片。这样的会产生一个很麻烦的问题,空间碎片过多,将会给大对象的分配带来很大的麻烦。往往老年代的空间剩余明明很大,但是因为没有一块足够大的连续内存,而导致要提前进行一次Full GC(Minor GC是指新生代,Full GC是指老年代)。所以设计者提供了一个方案,在老年代顶不住要进行Full GC时,开启碎片整理过程,但是碎片整理过程是没有办法并发的,所以这时还是会出现短暂的停顿,停顿时间延长。
7.G1收集器
最后的当然是属于重头戏了,该收集器的出现的原意就是取代CMS收集器(不过这是要在以后了),并且G1收集器是一个可以掌控全局的收集器,不仅负责新生代,还负责老年代,与其他GC收集器相比,G1收集器有如下优点:
(1)并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间,部分其它收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
(2)分代收集:与其它收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象已获得更好的收集效果。
(3)空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
(4)可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
当然,更细致的G1收集器讲解,可以参阅:https://www.cnblogs.com/ASPNET2008/p/6496481.html。
使用G1收集器时,Java堆得内存布局就与其它收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
一个对象分配在某个Region中,它并非只能被本Region中的其它对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在G1收集器中,Region之间的对象引用以及其它收集器中的新生代与老生代之间的对象引用,虚拟机都是使用Remembered Set来避免全栈扫描的,G1中每个Region都有一个与之对应Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属RegionRemembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为:
1)初始标记(Initial Marking)(需要STW)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
2)并发标记(Concurrent Marking)
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
3)最终标记(Final Marking)(需要STW)
最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,每个Region都有一个与之对应RememberedSet,虚拟机发现程序在堆Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,然后检查引用的对象是否存在不同的Region中,如果是,就把相关的引用记录记录到对象所属的Region的RememberedSet中,这样的话在GC根节点枚举时,不需要扫描全堆也可以做到信息不会遗漏(类似OopMap?)。而最终标记时,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
4)筛选标记(Live Data Counting and Evacuation)
最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1收集器的运行示意图: