Java虚拟机_内存管理_垃圾收集GC

来源:互联网 发布:森田玻尿酸乳液知乎 编辑:程序博客网 时间:2024/06/03 13:19

垃圾收集(Garbage Collection,GC)需要考虑三件事:

1:哪些内存需要回收

2:何时回收

3:如何回收

1:哪些内存需要回收

Java的运行时内存区域中,需要进行动态的内存分配和回收的是线程共享的内存区域:Java堆和方法区。

线程私有的内存区域随着线程的结束或者方法的退出会自动回收:虚拟机栈和本地方法栈;

堆中存放着Java世界中所有的对象实例,垃圾回收器对堆进行回收前,首先就是要先确定这些对象哪些还活着,哪些已经“死”了(不可能再有任何用途的对象)

有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收

1.1引用计数算法

引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加1,每当指向该对象的引用失效时计数器就减1。当该计数器的值降到0就认为对象死亡。每个计数器只记录了其对应对象的局部信息——被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况;存在一种一种情况是,两个对象相互引用除此之外都没有任何其他引用,导致两个对象都不能被回收;

主流JVM实现基本没有使用这个算法的;

1.2根搜索算法

Java使用根搜索算法回收垃圾,该算法的基本原理:定义一系列名为GC Roots的对象作为起点,从起点向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连,则说明该对象不可用,这时Java虚拟机可以对这些对象进行回收。

Java虚拟机将以下对象定义为 GC Roots :

Java虚拟机栈中引用的对象:比如方法里面定义这种局部变量 User user= new User();

静态属性引用的对象:比如 private static User user = new User();

常量引用的对象:比如 private static final  User user = new User();

本地方法栈中引用的对象:

在根搜索算法中,即便引用链不可达,也并不意味着该对象一定会被回收,因为回收要经历两次标记过程!第一次标记:对象进行根搜索之后,如果发现没有与GC Roots 相连接的引用链,就会被第一次标记并进行筛选所谓筛选,就是检查此对象是否有必要执行finalize方法,如果对象定义了该方法并且没有执行过。那么该对象就会被放入到一个队列F-Queue,随后会有一个低优先级的线程去执行这个队列里面对象的finalize方法第二次标记:JVM 将对F-Queue队列里面的对象进行第二次标记。如果对象不想被回收,那么就得在finalize方法里面拯救自己,否则,这些对象就真的会被回收

1.3引用

引用计数算法和根搜索算法,判断对象是否存活都与引用相关;
四类的引用类型:1:强引用Strong Reference 2:软引用 Soft Reference 3:弱引用 Weak Reference 4:虚引用 Phantom Reference
这篇文章介绍了java引用的类型:http://blog.csdn.net/mazhimazh/article/details/19752475

1.4方法区的回收

java虚拟机规范中没要求实现方法区的垃圾回收,而且方法区垃圾回收性价比不高,堆尤其是新生代中一次垃圾回收可以回收70%-95%的空间,但是方法区垃圾回收效率远低于此;

永久代的垃圾回收主要包括两部分内容:废弃常量和无用的类。

2:如何回收

2.1垃圾回收算法

以下内容来自http://my.oschina.net/u/1464779/blog/220632

2.1.1标记-清除算法

最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.1.2复制算法

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。复制算法的缺点显而易见,可使用的内存降为原来一半。

2.1.3标记-整理算法

标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

2.1.4分代收集算法

根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建;
现在的Java虚拟机就联合使用了分代复制、标记-清除和标记-整理算法,java虚拟机垃圾收集器关注的内存结构如下:


堆内存被分成新生代和年老代两个部分,整个堆内存使用分代复制垃圾收集算法。
(1).新生代:新生代使用复制和标记-清除垃圾收集算法,研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。使用java虚拟机-Xmn参数可以指定新生代内存大小。
(2).年老代:年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。
(3).永久代:java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。永久代也使用标记-整理算法进行垃圾回收,java虚拟机参数-XX:PermSize和-XX:MaxPermSize可以设置永久代的初始大小和最大容量。

2.2 垃圾回收器

如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现;垃圾收集器的实现JVM各不相同,下图是Sun HotSpot虚拟机1.6的垃圾收集器实现:
以下内容来自http://blog.csdn.net/java2000_wl/article/details/8030172

上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

2.2.1 Serial(串行GC)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

2.2.2 ParNew(并行GC)

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。包括Serial收集器可用的所用控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、停止其他线程、对象分配规则、回收策略等。
ParNew收集器在单个CPU环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器通过超线程技术实现的两个CPU环境中都不能百分之百超过Serial收集器。但是随着CPU数目的增加,它对于GC时系统资源利用还是有好处的,默认开启的线程数与CPU数目一样;可以通过-XX:ParallelGCThreads参数来设置线程数;

垃圾收集器中的并发与并行并行(Parallel):指多头垃圾收集器线程并行工作,但此时用户线程仍处于等待状态。
并发(Concurrent):指用户线程与回收器线程同时工作

2.2.3Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。看上去和ParNew收集器一样,有什么不同呢?
parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短越适合需要与用户交互的程序,吞吐量高则可以最高效的利用CPU时间尽快完成运算任务;

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,-XX:MaxGCcPauseMillis:控制最大垃圾收集停顿时间;-XX:GCTimeRatio:直接设置吞吐量大小

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器。

除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作原理不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

2.2.4 Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,也需要暂停所有用户线程,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

2.2.5Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,需要暂停用户线程;

2.2.6CMS(并发GC)收集器


CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的

整个收集过程大致分为4个步骤:①.初始标记(CMS initial mark)②.并发标记(CMS concurrenr mark)③.重新标记(CMS remark)④.并发清除(CMS concurrent sweep)     
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。
初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短.由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美.

主要有三个显著缺点:
1:CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
2:CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低.
3:CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程。

2.2.7G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。

G1(Garbage First)收集器是当前收集器技术发展的最前沿成果
它与CMS相比会有两个显著改进:1:采用“标记 - 整理”算法,避免产生碎片;2:可以精确地控制卡顿。这是通过让使用指定一个参数来控制在一个长度为M的时间片内垃圾回收的时间N。
G1之所以可以在基本不牺牲吞吐量的前提下完成垃圾回收,是因为它能够尽量避免全区域的垃圾回收。之前的收集器是进行收集的范围是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域的堆积成都,在后台维护一个优先列表,每次根据优先级从列表中挑选区域进行收集。

垃圾收集器参数总结

-XX:+<option> 启用选项
-XX:-<option> 不启用选项
-XX:<option>=<number> 
-XX:<option>=<string>

参数描述

-XX:+UseSerialGC

Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收-XX:+UseParNewGC打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收-XX:+UseConcMarkSweepGC使用ParNew + CMS +  Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。-XX:+UseParallelGCJvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge +  Serial Old的收集器组合进行回收
-XX:+UseParallelOldGC使用Parallel Scavenge +  Parallel Old的收集器组合进行回收
-XX:SurvivorRatio新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1-XX:PretenureSizeThreshold直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配-XX:MaxTenuringThreshold晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代-XX:UseAdaptiveSizePolicy动态调整java堆中各个区域的大小以及进入老年代的年龄-XX:+HandlePromotionFailure是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留-XX:ParallelGCThreads设置并行GC进行内存回收的线程数-XX:GCTimeRatioGC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效-XX:MaxGCPauseMillis设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效-XX:CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSCompactAtFullCollection
由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效-XX:+CMSFullGCBeforeCompaction
设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用-XX:+UseFastAccessorMethods
原始类型优化-XX:+DisableExplicitGC
是否关闭手动System.gc-XX:+CMSParallelRemarkEnabled
降低标记停顿-XX:LargePageSizeInBytes
内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m

Client、Server模式默认GC

 新生代GC方式老年代和持久GC方式

Client

Serial 串行GCSerial Old 串行GCServerParallel Scavenge  并行回收GCParallel Old 并行GC

Sun/oracle JDK GC组合方式

 新生代GC方式老年代和持久GC方式

-XX:+UseSerialGC

Serial 串行GCSerial Old 串行GC-XX:+UseParallelGCParallel Scavenge  并行回收GCSerial Old  并行GC-XX:+UseConcMarkSweepGCParNew 并行GCCMS 并发GC 
当出现“Concurrent Mode Failure”时
采用Serial Old 串行GC
-XX:+UseParNewGCParNew 并行GCSerial Old 串行GC-XX:+UseParallelOldGCParallel Scavenge  并行回收GCParallel Old 并行GC-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
Serial 串行GCCMS 并发GC 
当出现“Concurrent Mode Failure”时
采用Serial Old 串行GC

2.3Minor GC和Full GC

新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特性,所以MinorGC发生特别频繁,一般GC速度也很快
老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,出现了MajorGC一般伴随着至少一次MinorGC。发生不是很频繁,速度也比MinorGC慢;

3:总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一;虚拟机之所以提供多种不同的收集器以及调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定的收集器、参数组合,也没有最优的调试方法,虚拟机也没有必然的内存回收行为。

0 0
原创粉丝点击