JVM对象内存回收

2018年03月28日 08:29 | 2593次浏览 作者原创 版权保护

JVM中自动的对象内存回收机制称为:GC(Garbage Collection),GC的基本原理为将内存中不再被使用的对象进行回收,GC中用于回收内存中不被使用的对象的方法称为收集器,由于GC需要消耗一些资源和时间的,Java在对对象的生命周期特征进行分析后,在V 1.2以上的版本采用了分代的方式来进行对象的收集,即按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停,对新生代的对象的收集称为minor GC,对旧生代的对象的收集称为Full GC,程序中主动调用System.gc()强制执行的GC为Full GC,在需要进行对象回收的语言(例如还有LISP)中常用的有引用计数收集器和跟踪收集器。

1、 引用计数收集器

引用计数是标识Heap中对象状态最明显的一种方法,引用计数的方法简单来说就是对每一个对象都提供一个关联的引用计数,以此来标识该对象是否被使用,当这个计数为零时,说明这个对象已经不再被使用了。

引用计数的好处是可以不用暂停应用,当计数变为零时,即可将此对象的内存空间回收,但它需要给每个对象附加一个关联引用计数,这就要求JVM在分配对象时必须增加赋值操作,并且引用计数无法解决循环引用的问题,因此JVM并没有采用引用计数。

2、 跟踪收集器

跟踪收集器的方法为停止应用的工作,然后开始跟踪对象,跟踪时从对象根开始沿着引用跟踪,直到检查完所有的对象。

JVM的根对象集合根据实现不同而不同,但总会包含局部变量中的对象引用和栈帧的操作数栈(以及变量中的对象引用),根对象的来源主要有三种。

根对象的来源之一是被加载的类的常量池中的对象引用,例如字符串、被加载的类的常量池可能指向保存在堆中的字符串,例如类名字,超类名字,超接口名字,字段名,字段特征签名,方法名或者方法特征签名。

来源之二是传到本地方法中,没有被本地方法“释放”的对象引用。

来源之三是虚拟机运行时数据区中从垃圾收集器的堆中分配的部分。

跟踪收集器采用的均为扫描的方法,但JVM将Heap分为了新生代和旧生代,在进行minor GC时需要扫描是否有旧生代引用了新生代中的对象,但又不可能每次minor GC都扫描整个旧生代中的对象,因此JVM采用了一种称为卡片标记(Card Marking)的算法来避免这种现象。

卡片标记的算法为将旧生代以某个大小(例如512字节)进行划分,划分出来的每个区域称为卡片,JVM采用卡表维护卡的状态,每张卡片在卡表中占用一个字节的标识(有些JVM实现可能会不同),当Java代码执行过程中发现旧生代的对象引用或释放了对于新生代对象的引用时,就相应的修改卡表中卡的状态,每次Minor GC只需扫描卡表中标识为脏状态的卡中的对象即可,图示如下:

JVM对象内存回收

跟踪收集器在扫描时最重要的是要根据这些对象是否被引用来标识其状态,JVM中将对象的引用分为了四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:

强引用

默认情况下,对象采用的均为强引用,例如:

A a=null;
public void execute(){
a=new A();,
// 其他代码
}

只有当execute所在的这个对象的实例没有其他对象引用,GC时才会被回收。

软引用(SoftReference)

软引用是Java中提供的一种比较适合于缓存场景的应用,采用软引用修改之上的代码如下:

SoftReference aRef=null;
A a=null;
public void execute(){
if((aRef==null)||(aRef.get()==null)){
        a=new A();
        aRef=new SoftReference(a);
}
else{
	a=aRef.get();
}
// 执行代码
a=null;
}

代码中不同于强引用中的为在execute方法的最后将a设置为了null,当execute方法执行完毕后,a对象只有在内存不够用的情况下才会被GC,这对于合理的使用缓存而言无疑非常有作用,既可以保证不至于大量使用缓存出现OutOfMemory,又可以在内存够用的情况下提升性能。

弱引用(WeakReference)

采用弱引用修改之上的代码如下:

WeakReference aRef=null;
A a=null;
public void execute(){
if((aRef==null)||(aRef.get()==null)){
        a=new A();
        aRef=new WeakReference(a);
}
else{
a=aRef.get();
}
// 执行代码
a=null;
}

对于a这个引用,在GC时a一定会被GC回收,这种引用有助于GC更快的回收对象,尤其是位于集合中的对象,同时也有助于在GC未回收之前仍然调用此对象来执行一些动作。

虚引用(PhantomReference)

采用虚引用修改之上的代码如下:

ReferenceQueue aRefQueue=new ReferenceQueue();
PhantomReference aRef=null;
A a=null;
public void execute(){
a=new A();
aRef=new PhantomReference(a,aRefQueue);
// 执行代码
a=null;
}

在SoftReference和WeakReference中也可以放入ReferenceQueue,这个Queue是用于对象在被GC后用于保存Reference对象实例的,由于虚引用只是用来得知对象是否被GC,通过PhantomReference.get返回的永远是null,因此它要求必须有ReferenceQueue,当上面代码中的a对象被GC后,通过aRefQueue.poll可以获取到aRef对象实例,从而可以做一些需要的动作。

在掌握了java中的对于根对象、分代扫描的方式以及对象的引用类型后,来具体的看看跟踪收集器,常用的有如下三种:

标记—清除(Mark-Sweep)

从根对象开始访问每一个活跃的节点,并标记访问到的每一个节点,当遍历完成后,就对堆空间进行清除,即清除那些没打上标记的对象,过程图示如下:

这种方法的好处是便于实现,但由于要扫描整个堆,因此要求应用暂停的时间会较长,并且会产生较多的内存碎片。

JVM并没有实现这种需要长时间停止应用的标记—清除收集器,而是在此基础上提供了并发的标记—清除(Concurrent Mark Sweep,缩写为CMS)收集器,使得在整个收集的过程中只是很短的暂停应用的执行,可通过在JVM参数中设置-XX:UseConcMarkSweepGC来使用此收集器,不过此收集器仅用于旧生代和持久代的对象收集,并发的标记—清除较之Stop-The-World的标记—清除复杂了很多,来看看:

并发标记—清除做到的是在标记访问每一个节点时以及清除不活跃的对象时采用和应用并发的方式,仅需在初始化标记节点状态以及最终标记节点状态时需要暂停整个应用,因此其造成的应用的暂停的时间会比较的短。

并发标记—清除为了保证尽量短的造成应用的暂停,首先从分配内存上做了改动,CMS提供了两个free lists,一个用于存放小对象,另外一个则用于存放大对象,当JVM需要给对象分配内存时,则通过free list来找到可用的堆地址,并进行内存的分配以及将此地址从free list删除,当CMS回收对象内存后,则将其相应的地址重新放入此free list中,这样的好处是在回收对象的时候不需要做对象的移动等,因此可以让回收过程并发的进行。

接着来看看并发标记—清除的执行步骤:

1. Initial Marking

此步需要暂停整个应用,JVM扫描整个old generation中根对象可直接访问到的对象,并对这些对象进行标记,对于标记的对象CMS采用一个外部的bit数组来进行记录,。

2. Concurrent Marking

在初始化标记完毕后,CMS恢复所有应用的线程,同时开始并发的对之前标记过的对象进行轮循,以标记这些对象可访问的对象。

CMS为了确保能够扫描到所有的对象,避免在Initial Marking中还有未标识到的对象,采用的方法为找到标记了的对象,并将这些对象放入Stack中,扫描时寻找此对象依赖的对象,如果依赖的对象的地址在其之前,则将此对象进行标记,并同时放入Stack中,如依赖的对象地址在其之后,则仅标记该对象。

在进行Concurrent Marking时minor GC也可能会同时进行,这个时候很容易造成旧生代对象引用关系改变,CMS为了应对这样的并发现象,提供了一个Mod Union Table来进行记录,在这个Mod Union Table中记录每次minor GC后修改了的Card的信息。

在进行Concurrent Marking时还有可能会出现的一个并发现象是应用修改了旧生代中的对象的引用关系,CMS中仍然采用Card Table的方式来进行记录,在Card中将某对象标识为dirty状态,但即使是这样仍然可能会出现一种现象导致不再被引用的对象仍然是marked的状态:

例如当Concurrent Marking已经扫描到了a所引用的对象b、c、e,如果在此后应用将b引用的对象由c改为了d,同时g不再引用d,此时会将b、g对象的状态在card中标识为dirty但c的状态并不会因此而改变。

3. Final Marking

此步需要暂停整个应用,由于在Concurrent Marking时应用可能会修改对象的引用关系或创建新的对象,因此需要把这些改变或新创建的对象也进行扫描,CMS递归扫描Mod Union Table以及Card Table中dirty的对象,并进行标记。

4. Concurrent Sweeping

在完成了Final Marking后,恢复所有应用的线程,就进入到这步了,这步需要负责的是将没有标记的对象进行回收。

回收过程是并发进行的,而JVM分配对象内存(尽管CMS仅用于old generation,但有些时候会由于应用创建的对象过大导致直接分配到old generation的现象,另外一种现象就是young generation经过回收后需要转入old generation的对象)和CMS释放内存又都是操作free list,会产生free list竞争的现象,因此CMS在此增加了Mutual exclusion locks,以JVM分配优先。

CMS为了避免每次回收后回收到的大小都比之前分配出去的内存小,在进行sweeping的时候,还会尽量的将相邻的块重新组装为一个块,sweeping为了避免和JVM分配对象内存产生冲突,采用的方法为首先从free list中删除块,组装完毕后再重新放入块中,为了能够从free list中删除指定的块,CMS将free list设计为了双向链表。


CMS中的耗时的过程都是和应用并发进行的,这也是CMS最突出的优点,使得其造成的应用的暂停时间比Mark-Sweeping的方式短了很多,但同时也意味着CMS会和应用线程争抢CPU资源,CMS回收内存的方式也使得其很容易产生内存碎片,降低了空间的利用率,

另外就是CMS在回收时容易产生一些应该回收但需要等到下次CMS才能被回收掉的对象,例如上图中的C对象,称为“浮动垃圾“,这也就要求了采用CMS的情况下需要提供更多的可用的旧生代空间,总体来说CMS很适用于对响应时间要求很高、CPU资源竞争不是很激烈以及内存空间相对更充足的系统。

CMS为了降低和应用争抢CPU资源的现象发生,还提供了一种增量的模式,称为i-CMS,在这种模式下,CMS仅启动一个处理器线程来并发的扫描标记和清除,并且该线程在执行一小段时间后就会先将CPU使用权让出来,分多次多段的方式来完成整个扫描标记和清除的过程,这样降低了对于CPU资源的消耗,但同时也降低了CMS的性能,因此仅适用于CPU少的应用。

CMS为了减少产生的内存碎片,提高jvm空间的利用率,提供了一个整理碎片的功能,可通过在jvm中指定-XX:+ UseCMSCompactAtFullCollection来启动此功能,在启动了此功能后默认为每次Full GC的时候都会进行整理,也可以通过-XX:CMSFullGCsBeforeCompaction=来指定多少次Full GC后才执行整理,不过要注意的是,整理这个步骤是需要暂停整个应用的。

复制(Copying)

同样从根开始访问每一个活跃的节点,但其不做标记,而是将这些活动的对象复制到另外的一个空间去,在遍历完毕后,只需把原空间清空就可以了,过程图示如下:

这种方法的好处是只访问活跃的对象,不用扫描整个堆中的所有对象,因此其扫描的速度仅取决于活跃的对象的数量,并且不会产生内存碎片,但其不足的地方是需要一个同样大小的空间,增加了内存的消耗,并且复制对象也是需要消耗时间的。

JVM中提供了此收集器的实现,但仅用于新生代中对象的收集,并提供了串行和并行的两种执行方式,串行即为单线程运行此收集器,可通过 -XX:+UseSerialGC来指定使用串行方

式的复制收集器;并行则为多线程运行此收集器,可通过-XX:+UseParallelGC(指定新生代、旧生代以及持久代都采用并行的方式进行收集,旧生代并行运行收集器仅在JDK 5 Update 6后才支持)或-XX:+UseParNewGC(指定新生代采用并行的方式进行收集)来指定使用并行方式的复制收集器,其中并行的线程数默认为CPU个数,可通过-XX:ParallelGCThreads来指定并行运行收集器时的线程数。

复制时将Eden Space中的活跃对象和一块Survior Space中尚不够资格(又称为From Space,小于-XX:MaxTenuringThreshold(默认为31次)次Minor GC)进入Old Generation的活跃对象复制到另外一块Survior Space(又称为To Space)中,对于From Space中经历过-XX:MaxTenuringThreshold次仍然存活的对象则复制到Old Generation中,大对象也直接复制到Old Generation,如To Space中已满的话,则将对象直接复制到Old Generation中(这点非常值得注意,在实际的产品中要尽量避免对象直接到Old Generation),可通过-XX:SurvivorRatio来调整Survior Space所占的大小,然后清除Eden Space和From Space,过程图示如下:

标记—整理(Mark-Compact)

标记—整理吸收了标记—清除和复制的优点,第一阶段从根节点遍历标记所有活跃的对象,第二阶段遍历整个堆,清除未标记的对象,并把存活的对象“压缩“到堆中的一块,按顺序排放,这样就避免了内存碎片的产生,同时也不像复制算法需要两倍的内存空间,过程图示如下:

但由于标记—整理仍然是需要遍历整个堆的,因此其仍然要求应用暂停较长的时间。

JVM中提供了此收集器的实现,但仅用于旧生代中对象的收集,同时也是旧生代默认采用的收集器,从JDK 5 Update 6后支持并行运行,以加快标记—整理的执行时间,JVM中标记—整理收集器的执行过程图示如下:

JVM内存回收机制

Java为了降低GC对应用产生的影响,一直都在不断的发展着GC,并提供了多种不同的收集器,以便JAVA开发人员能够根据硬件环境以及应用的需求来选择相应的收集器。

并发标记—清除收集器

并发标记—清除收集器的特征是能够让GC过程暂停应用的时间缩短,但需要消耗更多

的CPU资源以及JVM内存空间,并容易产生内存碎片,对于响应时间要求非常灵敏的系统而言(如GUI系统),是无法忍受GC一次带来的几秒的暂停的,在这种情况下可以优先采用这种收集器。

并发标记—清除收集器仅对旧生代和持久代的收集有效,可通过在JVM参数中加入-XX:UseConcMarkSweepGC来采用此收集器。

串行复制收集器

此收集器仅适用于新生代的收集,其特征为适用于快速的完成活跃对象不多的空间的收集,不产生内存碎片,但需要双倍的内存空间,执行过程中应用需要完全暂停,可通过在JVM参数中加入-XX:+UseSerialGC来采用此收集器。

并行复制收集器

此收集器和串行复制收集器唯一不同的地方在于采用了多线程进行收集,在超过2个CPU的环境上,其速度比串行复制收集器快很多,因此在超过2个CPU的环境上应采用此收集器来完成新生代对象的收集,可通过在JVM参数中加入-XX:+UseParallelGC或-XX:+UseParNewGC指定使用此收集器。

串行标记—整理收集器

此收集器仅适用于旧生代的对象收集,是JDK 5 Update 6之前的版本中默认的旧生代收集器,其特征为适用于收集存活时间较长的对象,不产生内存碎片,但收集造成的应用暂停的时间会比较长。

并行标记—整理收集器

此收集器和串行方式不同之处仅在于多线程执行,因此造成的应用的暂停时间能有一定的缩短,仅在JDK 5 Update 6之后的版本可使用,可通过-XX:+UseParallelGC或-XX:+UseParOldGC来指定,但不可与并发标记—整理收集器同时使用。

在JDK 5以前的版本中还有一个收集器是增量收集器,此增量收集器可通过-Xincgc来启用,但在JDK 5以及以上的版本中废弃了此增量收集器,-Xincgc会自动的转为采用并行收集器去进行垃圾回收,原因是其性能低于并行收集器,因此在本书中就不介绍此收集器了,增量收集器中采用的火车算法比较有意思,如果有兴趣的话可以去看看。

JVM为了避免JAVA开发人员需要头疼这么多种收集器的选择,还提供了两种简单的方式来控制GC的策略:

1、 吞吐量优先

吞吐量是指GC所耗费的时间占应用运行总时间的百分比,例如应用总共运行了100分钟,其中GC执行占用了1分钟,那么吞吐量就是99%了,JVM默认的指标是99%。

吞吐量优先的策略即为以吞吐量为指标,由JVM自行选择相应的GC策略以及控制New Generation、Old Generation内存的大小,可通过在JVM参数中指定-XX:GCTimeRatio=n来使用此策略。

2、 暂停时间优先

暂停时间是指每次GC造成的应用的停顿时间,默认不启用这个策略。

暂停时间优先的策略即为以暂停时间为指标,由JVM自行选择相应的GC策略以及控制New Generation、Old Generation内存的大小,来尽量的保证每次GC造成的应用停顿时间都在指定的数值范围内完成,可通过在JVM参数中指定-XX:MaxGCPauseMillis=n来使用此策略。

当以上两参数都指定的情况下,首先满足暂停时间优先策略,再满足吞吐量优先策略。

大多数情况下使用默认的JVM配置或者使用以上两个参数就可以让GC符合应用的要求运行了,只有当默认的或使用了以上两个参数还达不到需求时,才值得自行来调整这些和内存分配和回收相关的JVM参数。

在Java中除了能够通过调用System.gc()来强制JVM进行GC操作外,就只能由JVM来自行决定什么时候执行GC了,由于年轻代中多数为新创建的对象,并且大多数都已不再活跃,因此Java采用复制收集器来回收年轻代中的对象,当Eden Space空间满的时候,会触发minor GC的执行,Eden Space空间满的原因是新创建的对象的大小超过了Eden Space的大小,例如如下的一段代码,当新生代的大小设置为10M(-Xmn10M),整个jvm堆设置为64M时(-Xms64M –Xmx64M),下面的代码在执行过程中会经历一次minor GC:

Map<String, byte[]> bytes=new HashMap<String, byte[]>();
for (int i = 0; i < 8*1024; i++) {
bytes.put(String.valueOf(i),new byte[1024]);
}

由于新生代的大小为10M,那么按照默认的Survivor Ratio为8的分配方法:Eden Space为8M,两个Survivor Space均为1M,因此只要新创建的对象超过了8M,就会执行minor GC,上面的代码中保证了bytes属性中的value的大小在8M,因此可以保证在执行的过程中会经历一次minor GC,按照复制收集器中的讲解,下面的程序运行状况则会有所不同:

byte[] bytes=new byte[8*1024*1024];

这个对象会直接被分配到old generation中,并不会触发minor GC,这也是为什么之前的一段程序中不直接分配大对象的原因。

年老代中的对象则多数为长期活跃的对象,因此Java采用标记—整理收集器或并发的标记—清除收集器来回收年老代中的对象。

触发JVM执行Full GC的情况有如下两种:

1、 Old Generation空间满或接近某个比例

Old Generation空间满的原因是从新生代提升到旧生代的对象大小+当前旧生代的对象的大小已经接近Old Generation的空间大小,标记—整理收集器的触发条件为Old Generation空间满,CMS的触发条件为Old Generation接近某个比例。

按照之前对于复制收集器的描述,对象从新生代提升到旧生代的原因有如下三种:

新分配的对象的大小超过了Eden Space的大小;

对象在新生代中经过了-XX:MaxTenuringThreshold次仍然存活;

Minor GC时放不进To Space中的对象;

CMS可通过-XX:CMSInitiatingOccupancyFraction来指定旧生代中空间使用比率占到多少时,开始执行CMS,默认值为68%。

当Full GC后空间仍然不足以放入对象时,JVM会抛出OutOfMemory的错误信息,例如下面的代码:

byte[] toBytes=new byte[1024*1024];

byte[] maxBytes=new byte[8*1024*1024];

当jvm的启动参数设置为-Xmn10M –Xms18M –Xmx18M时,上面的代码运行会直接报出如下错误:

java.lang.OutOfMemoryError: Java heap space

当看到这个错误时,说明jvm的空间不足或是系统有内存泄露,例如该释放的引用没释放等,但出现这个错误时jvm不一定会crash,伴随着的是Full GC的频繁执行,会严重影响应用的响应速度。

2、 Permanet Generation空间满

Permanet Generation中存放的为一些class的信息等,当系统中需要加载的类、反射的类和调用的方法较多的时候,Permanet Generation可能会被占满,占满时如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

java.lang.OutOfMemoryError: PermGen space

当看到这个错误时,说明Perm空间分配的不足,通常的解决方案为通过增大Perm空间来解决,配置的参数为:-XX:PermSize以及-XX:MaxPermSize。

GC仍然在继续的发展,除了这些已有的GC外,JDK 7中增加了一种新的Garbage First的收集器,同时Java为了能够满足实时系统的要求,还提供了一个RealTime版的JDK,在这个JDK中允许开发人员更加灵活的控制对象的生命周期,例如可以在某个方法中执行完毕后就自动回收对象的内存,而不是等到minor GC或Full GC,这两个变化对于编写高性能的JAVA应用而言都会产生不小的影响,因此在本章节中也对其进行介绍。

Garbage First

Garbage First简称G1,它的目标是要做到尽量减少GC所导致的应用暂停的时间,让应用达到准实时的效果,同时保持JVM堆空间的利用率,其最大的特色在于允许指定在某个时间段内GC所导致的应用暂停的时间最大为多少,例如在100秒内最多允许GC导致的应用暂停时间为1秒,这个特性对于准实时响应的系统而言非常的吸引人,这样就再也不用担心系统突然会暂停个两三秒了。

G1要做到这样的效果,也是有前提的,一方面是硬件环境的要求,必须是多核的CPU以及较大的内存(从规范来看,512M以上就满足条件了),另外一方面是需要接受吞吐量的稍微降低,对于实时性要求高的系统而言,这点应该是可以接受的。

为了能够达到这样的效果,G1在原有的各种GC策略上进行了吸收和改进,在G1中可以看到增量收集器和CMS的影子,但它不仅仅是吸收原有GC策略的优点,并在此基础上做出了很多的改进,简单来说,G1吸收了增量GC以及CMS的精髓,将整个jvm Heap划分为多个固定大小的region,扫描时采用Snapshot-at-the-beginning的并发marking算法(具体在后面内容详细解释)对整个heap中的region进行mark,回收时根据region中活跃对象的bytes进行排序,首先回收活跃对象bytes小以及回收耗时短(预估出来的时间)的region,回收的方法为将此region中的活跃对象复制到另外的region中,根据指定的GC所能占用的时间来估算能回收多少region,这点和以前版本的Full GC时得处理整个heap非常不同,这样就做到了能够尽量短时间的暂停应用,又能回收内存,由于这种策略在回收时首先回收的是垃圾对象所占空间最多的region,因此称为Garbage First。

看完上面对于G1策略的简短描述,并不能清楚的掌握G1,在继续详细看G1的步骤之前,必须先明白G1对于JVM Heap的改造,这些对于习惯了划分为new generation、old generation的大家来说都有不少的新意。

G1将Heap划分为多个固定大小的region,这也是G1能够实现控制GC导致的应用暂停时间的前提,region之间的对象引用通过remembered set来维护,每个region都有一个remembered set,remembered set中包含了引用当前region中对象的region的对象的pointer,由于同时应用也会造成这些region中对象的引用关系不断的发生改变,G1采用了Card Table来用于应用通知region修改remembered sets,Card Table由多个512字节的Card构成,这些Card在Card Table中以1个字节来标识,每个应用的线程都有一个关联的remembered set log,用于缓存和顺序化线程运行时造成的对于card的修改,另外,还有一个全局的filled RS buffers,当应用线程执行时修改了card后,如果造成的改变仅为同一region中的对象之间的关联,则不记录remembered set log,如造成的改变为跨region中的对象的关联,则记录到线程的remembered set log,如线程的remembered set log满了,则放入全局的filled RS buffers中,线程自身则重新创建一个新的remembered set log,remembered set本身也是一个由一堆cards构成的哈希表。

尽管G1Heap划分为了多个region,但其默认采用的仍然是分代的方式,只是仅简单的划分为了年轻代(young)和非年轻代,这也是由于G1仍然坚信大多数新创建的对象都是不需要长的生命周期的,对于应用新创建的对象,G1将其放入标识为young的region中,对于这些region,并不记录remembered set logs,扫描时只需扫描活跃的对象,G1在分代的方式上还可更细的划分为:fully young或partially young,fully young方式暂停的时候仅处理young regions,partially同样处理所有的young regions,但它还会根据允许的GC的暂停时间来决定是否要加入其他的非young regions,G1是运行到fully-young方式还是partially young方式,外部是不能决定的,在启动时,G1采用的为fully-young方式,当G1完成一次Concurrent Marking后,则切换为partially young方式,随后G1跟踪每次回收的效率,如果回收fully-young中的regions已经可以满足内存需要的话,那么就切换回fully young方式,但当heap size的大小接近满的情况下,G1会切换到partially young方式,以保证能提供足够的内存空间给应用使用。

除了分代方式的划分外,G1还支持另外一种pure G1的方式,也就是不进行代的划分,pure方式和分代方式的具体不同在下面的具体执行步骤中进行描述。

掌握了这些概念后,继续来看G1的具体执行步骤:

1. Initial Marking

G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。

开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。

触发这个步骤执行的条件为:

G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;

在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。

2. Concurrent Marking

按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。

3. Final Marking Pause

当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。

4. Live Data Counting and Cleanup

值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:

G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;

对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young 

regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。

这一步中GC线程并行的扫描所有region,计算每个region中低于next TAMS值中marked data的大小,然后根据应用所期望的GC的短延时以及G1对于region回收所需的耗时的预估,排序region,将其中活跃的对象复制到其他region中。


G1为了能够尽量的做到准实时的响应,例如估算暂停时间的算法、对于经常被引用的对象的特殊处理等,G1为了能够让GC既能够充分的回收内存,又能够尽量少的导致应用的暂停,可谓费尽心思,从G1的论文中的性能评测来看效果也是不错的,不过如果G1能允许开发人员在编写代码时指定哪些对象是不用mark的就更完美了,这对于有巨大缓存的应用而言,会有很大的帮助,G1随JDK 6 Update 14已经beta发布,在Java 7中估计会正式的作为替代CMS的GC策略,由于在本书的编写阶段中G1尚处于beta阶段,不过还是尝尝鲜,来看看G1的实际表现吧。

等到7、8月份再看看G1是不是有新的发布,再来写这个部分。


Real-Time版的JDK

为了满足实时领域系统使用Java的需求,也为了让Java能够进入更多的高端领域,Java推出了Real-Time版的规范(JSR-001,更新的版本为JSR-282),并且各大厂商也都积极响应,相应的推出了Real-Time实现的JDK,Real-Time版的JDK对java做出了很多的改进,例如强大的线程调度机制、异步的事件处理机制、更为精准的时间刻度等,在此最为关心的是其在java内存管理方面的加强。

GC无疑是java进入实时领域的一个很大的障碍,毕竟无论GC怎么改进,它肯定是会造成应用暂停的现象的,而且是在运行时突然的就会造成暂停,这对于实时系统来说是不可接受的,因此Real-Time版的JDK在此方面做出了多方面的改进,由于没试用过,在此也只能是按照规范纸上谈兵了。

新的内存管理机制

提供了两种内存区域:Immortal内存区域和Scoped内存区域。

Immortal内存区域用于保留永久的对象,这些对象仅在应用结束运行时才会释放内存,这个最典型的需求场景莫过于缓存了。

Scoped内存区域用于保留临时的对象,位于scope中的对象在scope退出时,这些对象所占用的内存会被直接回收。

Immortal内存区域和Scoped内存区域均不受GC管理,因此基于这两个内存区域来编写的应用完全不用担心GC会造成暂停的现象。

允许Java应用直接访问物理内存

在保证安全的情况下,Real-Time JDK允许Java应用直接访问物理内存,而非像以前的java程序,需要通过native code才能访问,能够访问物理内存,也就意味着可以直接将对象放入物理内存,而非jvm heap中。



小说《我是全球混乱的源头》
此文章本站原创,地址 https://www.vxzsk.com/724.html   转载请注明出处!谢谢!

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程