码迷,mamicode.com
首页 > 编程语言 > 详细

Java GC机制

时间:2016-08-05 21:23:32      阅读:369      评论:0      收藏:0      [点我收藏+]

标签:

    Java中GC机制(garbage collection)是垃圾回收机制,更确切的说是内存回收机制

    

     在一个对象不再被程序引用时,它所占用的堆空间就可以回收,以便分配给新对象使用。而且除了释放不再被引用的对象外,垃

圾收集器还要处理堆碎块(堆碎块是在正常的程序运行时产生的),因为新的对象分配了空间,不再被引用的对象被释放,所以堆内存的空闲位置介于活对的对象之间,而请求分配新对象时可能不得不增大堆空间,因为虽然总的空闲空间是够的,但堆中没有连续的空闲空间放得下新对象。

 

   上面是垃圾收集的作用,其好处在于:1)提高了工作效率,在一个没有垃圾收集机制的语言下编程,程序员还要花时间来解决难以捉摸的内存问题;

                                                2)帮助程序保持完整性,因为程序员不可能因为失误错误地释放内存而导致jvm崩溃;

 

2.垃圾收集器怎么知道对象不再被引用?

  

    垃圾检测通常是通过建立一个根对象的集合并且检查从这些根对象开始的可触及性来实现。可触及就是正在执行的程序可以访问到的根对象和某个对象之间存在引用路径。

    区分活动对象和垃圾的两个基本方法是引用计数跟踪。

 

3.引用计数收集器是怎么判定对象为垃圾的?

  

   引用计数的原理是:堆中第一个对象都有一个引用计数,当有一个对象被创建时,这个对象的引用计数被置为1。当有其他变量被赋值为这个对象的引用时,计数加1;当这个对象的引用超过了生存期或被设置为新的值,计数减1;当计数为0时,对象被认为是垃圾,将被回收;

   这种方法的早已过时,主要缺陷是:他无法检测出循环引用(即两个或多个对象互相引用),例如:父对象有一个对子对象的引用,子对象又反过来引用父对象,这时计数永远不可能为0。

 

4.跟踪收集器又是怎么一回事?

 

   跟踪就是对对象设置标记,通过一个从根结点开始的对象引用图追踪对象,在这个过程中对遇到的对象打上标记,当追踪结束时没有被打上标记的就是无法触及的,从而可以被回收。

5.怎么处理堆碎块?

 

   一般处理堆碎块有以下几种收集器:

  

   1.压缩收集器:把活动的对象都推到堆的一端,从而使另一端出瑞一个大的连续空闲区,所有被移动的对象的引用也被更新,指向新的位置;为了简便,更新被移动的对象引用时通过一个间接引用层,因为不直接引用堆中的对象而对象的引用实际上指向一个对象句柄表的话不用改变引用指向的位置,但损失了部分性能。

 

   2.拷贝收集器:把所有的活动的对象移动到一个新的区域,在这个过程中,活动对象被紧挨着布置从而消除原本它们在旧区域的空隙。一般的拷贝收集算法是——停止并拷贝,在这个方法中堆被分为两个区域,任何时候都只使用其的一个区域。对象在同一个区域分配,直到这个区域满了为止;这时程序中止,堆被遍历,遍历到的活动对象被拷贝到另外一个区域,完成后程序继续执行,如此循环;这种方法的代价是:对于指定大小的堆来说需两倍大小的内存。

下图是“停止并拷贝”算法的垃圾收集堆的图形描述:第一张图中,堆的下半部分没有被使用,上半部分被对象零散的填充;2图是上半部分逐渐被填充,3图中上半部分被填满,这时程序中止,垃圾收集器开始工作,从根结点开始追踪活动对象图,当遇到活动对象时就拷贝到堆的下半部分,每一个都紧挨着,如4图;如此打循环

技术分享

 

 3.按代收集的收集器:

     简单的停止拷贝收集器的缺点在于:第一次收集,所有的活动对象都要被拷贝,而程序中一般都有以下特点:

     1)大多数程序创建的大部分对象都具有很短的生命期;

     2)大多数程序都创建一些具有非常长生命周期的对象;

     简单的停止拷贝收集器每次都把那些生命周期长的对象来回拷,浪费了时间;按代收集器通过把对象按照寿命来分组,更多的收集那些年幼的对象,当一个年幼对象经过几次收集后仍然存活,那就将它提升为寿命更高的一代,从而对这个对象的收集频率就减少了,帮命越长的收集频率就越小;

 

  4.自适应收集器:看这个收集器的名字,顾名思义:根据不同的情况选择不同的垃圾收集技术;

 

6.什么是火车算法

 

    由于垃圾收集时一般会停止程序,如果停止的时间长得让用户注意到而不满意,那就说明这种算法存在破坏性,为了减少这种破坏性而采用渐进式收集算法(每次回收一部分),通常渐进式收集器都是按代收集器,而火车算法就是为了在成熟空间空间(最高寿的那个年龄层)提供限定时间的渐进收集。

   火车算法把成熟对象空间划分为固定长度的内存块,算法每次在一个块中单独执行,每一个块属于一个集合,块就是“车厢”,集合就是“火车”,成熟对象空间就是“火车站”;就如车厢一样,块在一个集合中有序的;集合也被排序,就像火车站中的火车按轨道排列一样,如下图:

技术分享

对象从更年轻的年龄层的子堆中进入成熟对象空间都会被附加到任何已存在的火车中或者为他们专门创建一列或多列火车。

     每一次执行火车算法的时候,它不是收集最小数字的火车中的最小数字车厢,就是收集整列最小数字的火车,算法首先检查指向最小数字火车中任何车厢的引用,如果不存在任何来自最小数字火车以外的引用指向它内部包含的对象,那么整个火车都是垃圾,回收整个火车;如果不都是垃圾,那么将注意力放在火车中最小数字车厢上,首先把所有被最小数字车厢外部的车厢引用的对象转移到其他车厢去,之后任何保留在车厢内的对象都是没有引用的,可以被回收;

而保证整列火车中没有循环的数据结构的关键是算法如何移动对象:

1)如果对象被成熟对象空间的其他火车引用,对象不被转移到引用它的那列火车中去;

2)然后转移过后的对象被扫描,查找对原车厢的引用,发现的任何被外用的对象都被转移到引用它的火车中去;

3)新被转移的对象也被扫描,这个过程不断重复,直到没有任何来自其他火车的引用指向正被收集的车厢;

4)如果接收对象的火车没有空间了,那么算法会创建新的车厢,并附加到那列火车的尾部;

 

7.终结

   java中一个对象可以拥有终结方法:这个方法是垃圾收集器在释放对象前必须运行的。这个方法使垃圾收集器要做的工作更加复杂,因为垃圾收集器必须检查它所发现的不再被引用的对象是否存在终结方法——finalize();还得记住一点:是垃圾收集器运行对象的终结方法。

 

8.GC中对象的六种可触及状态

   1.强可触及:对象可以从根结点不通过任何引用对象搜索到

   2.软可触及:对象不是强可触及的,但是可以从根结点开始通过一个或多个(未被清除的)软引用对象触及

   3.弱可触及:对象既不是强可触及也不是软可触及的,但是从根结点开始可以通过一个或多个弱引用对象触及

   4.可复活的:对象既不是强可触及、软可触及,也不是弱可触及,但是仍然可能通过执行某些终结方法复活到这几种状态之一

   5.影子可触及:不上以上任何可触及状态,也不能通过终结方法复活,并且它可以从根结点开始通过一个或多个影子引用对象触及(影子引用不会被垃圾收集器清除,由程序明确地清除)

   6不可触及:就是已经准备回收的状态

常用垃圾收集算法

1. 标记-清除算法

这种垃圾收集算法思路非常简单,主要是首先标记出所有需要回收的对象,然后回收所有需要回收的对象。

但是有一个明显的缺点,采用这种算法之后会发现内存块回收之后就不连续了,这就导致了在下一次想分配一个大内存块的时候无法分配。

技术分享

 

2. 标记-清除-压缩

这种垃圾收集算法主要是对上面的算法进行了优化,内存回收了对内存进行了一次优化压缩。这样回收后内存块的连续性又比较强了。

但是这种算法会涉及到不停的内存间的拷贝和复制,性能会非常差。

技术分享

 

3.标记-清除-复制

这种算法会将内存空间分配成两块相同的区域A和B。当内存回收的时候,将A中的内存块拷贝到B中,然后一次性清空A。

但是这种算法会对内存要求比较大一些,并且长期复制拷贝性能上也会受影响。

技术分享

 

Java分代收集算法

Java主要采用了分代收集算法。分代收集算法主要将对象存活期的长短将内存进行划分。

Java主要将内存划分为两部分:新生代老生代

Java的新生代中,对象的存活率低,存活期期会相对会比较短一些,所以可以选用复制算法来进行内存回收。

Java的老生代中,对象的存活率比较高,并且相对存活期比较长一些,可以采用标记-清除-压缩的算法来进行内存回收。

可以看图:

技术分享

通常新生代分为Eden和两个Survivor,其中可以通过-XX:SurvivorRatio=1来设置(这里要考虑两个Survivor,意味着二个S的大小是整个新生代的2/3)

前面已经说了,Java的内存分配和内存回收主要在Java的堆上进行的。而Java的方法区间和常量池我们一般称为永久代。永久代可以通过-XX:PermSize=512M -XX:MaxPermSize=512M设置

Java堆内存设置参数:-Xmx20m -Xms20m

Java堆新生代内存分配设置:-Xmn10m 新生代分配了10M的内存,那么剩下的10M就是老生代上面分配了。也可以设置:-XX:NewRatio=4

通过设置参数,我们就可以在控制台中看到Java虚拟机在执行GC时候的日志:-XX:+PrintGCDetails  

也可以指定日志的位置:-Xloggc:gc.log   

永久代一般是指方法区和常量池,一般情况下永久代在虚拟机运行时就能确定大小的,但是一些框架可能动态生成一些类信息就会导致永久代越来越大。

 

 

Java内存分配策略

使用的ParNew+Serial Old收集器组合

1. 优先在Eden上分配。

 

Java的对象优先会在新生代的Eden上分配。

我们可以看一个例子:

我设置了这些参数:-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m,堆内存分配20M,新生代10M,老生代10M,默认情况下Survivor区为8:1,所以Eden区域为8M

我运行这段代码:

 

[java] view plain copy
 
 print?技术分享技术分享
  1. public class JavaTest {  
  2.   
  3.     static int m = 1024 * 1024;  
  4.   
  5.     public static void main(String[] args) {  
  6.         //分配2兆  
  7.         byte[] a1 = new byte[2 * m];  
  8.         System.out.println("a1 ok");  
  9.         //分配2兆  
  10.         byte[] a2 = new byte[2 * m];  
  11.         System.out.println("a2 ok");  
  12.     }  
  13. }  


控制台日志:

 

 

[html] view plain copy
 
 print?技术分享技术分享
  1. a1 ok  
  2. a2 ok  
  3. Heap  
  4.  def new generation   total 9216K, used 4603K [0x331d0000, 0x33bd0000, 0x33bd0000)  
  5.   eden space 8192K,  56% used [0x331d0000, 0x3364ef50, 0x339d0000)  
  6.   from space 1024K,   0% used [0x339d0000, 0x339d0000, 0x33ad0000)  
  7.   to   space 1024K,   0% used [0x33ad0000, 0x33ad0000, 0x33bd0000)  
  8.  tenured generation   total 10240K, used 0K [0x33bd0000, 0x345d0000, 0x345d0000)  
  9.    the space 10240K,   0% used [0x33bd0000, 0x33bd0000, 0x33bd0200, 0x345d0000)  
  10.  compacting perm gen  total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)  
  11.    the space 12288K,   3% used [0x345d0000, 0x3462f4d0, 0x3462f600, 0x351d0000)  
  12.     ro space 10240K,  55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)  
  13.     rw space 12288K,  55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)  

 

日志中非常清晰的可以看到,我们分配了一个4M内存大小,直接是分配在了eden space里面。

 

 

2. 大对象直接进入老生代。

 

参数:-XX:PretenureSizeThreshold(该设置只对Serial和ParNew收集器生效) 可以设置进入老生代的大小限制,我们设置为3M,则大于3M的大对象就直接进入老生代

测试代码:

 

[java] view plain copy
 
 print?技术分享技术分享
  1. public class JavaTest {  
  2.   
  3.     static int m = 1024 * 1024;  
  4.   
  5.     public static void main(String[] args) {  
  6.         //分配2兆  
  7.         byte[] a1 = new byte[2 * m];  
  8.         System.out.println("a1 ok");  
  9.         byte[] a3 = new byte[4 * m];  
  10.         System.out.println("a2 ok");  
  11.     }  
  12. }  


控制台日志:

 

 

[html] view plain copy
 
 print?技术分享技术分享
  1. a1 ok  
  2. a2 ok  
  3. Heap  
  4.  def new generation   total 9216K, used 2555K [0x331d0000, 0x33bd0000, 0x33bd0000)  
  5.   eden space 8192K,  31% used [0x331d0000, 0x3344ef40, 0x339d0000)  
  6.   from space 1024K,   0% used [0x339d0000, 0x339d0000, 0x33ad0000)  
  7.   to   space 1024K,   0% used [0x33ad0000, 0x33ad0000, 0x33bd0000)  
  8.  tenured generation   total 10240K, used 4096K [0x33bd0000, 0x345d0000, 0x345d0000)  
  9.    the space 10240K,  40% used [0x33bd0000, 0x33fd0010, 0x33fd0200, 0x345d0000)  
  10.  compacting perm gen  total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)  
  11.    the space 12288K,   3% used [0x345d0000, 0x3462f4d0, 0x3462f600, 0x351d0000)  
  12.     ro space 10240K,  55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)  
  13.     rw space 12288K,  55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)  


上面的日志中,可以清洗看到第一次分配的2M留存在了eden space中,而4M超过了大对象设置的值3M,所以直接进入了老生代tenured generation

 

 

3. 长期存活的对象进入老年代

为了演示方便,我们设置-XX:MaxTenuringThreshold=1(默认15),当在新生代中年龄为1的对象进入老年代。

测试代码:

 

[java] view plain copy
 
 print?技术分享技术分享
  1. public class JavaTest {  
  2.   
  3.     static int m = 1024 * 1024;  
  4.   
  5.     public static void main(String[] args) {  
  6.         //分配2兆  
  7.         byte[] a1 = new byte[1 * m / 4];  
  8.         System.out.println("a1 ok");  
  9.         byte[] a2 = new byte[7 * m];  
  10.         System.out.println("a2 ok");  
  11.         byte[] a3 = new byte[3 * m]; //GC  
  12.         System.out.println("a3 ok");  
  13.     }  
  14. }  


控制台日志:

 

 

[html] view plain copy
 
 print?技术分享技术分享
  1. a1 ok  
  2. a2 ok  
  3. [GC [DefNew: 7767K->403K(9216K), 0.0062209 secs] 7767K->7571K(19456K), 0.0062482 secs]   
  4. [Times: user=0.00 sys=0.00, real=0.01 secs]   
  5. a3 ok  
  6. Heap  
  7.  def new generation   total 9216K, used 3639K [0x331d0000, 0x33bd0000, 0x33bd0000)  
  8.   eden space 8192K,  39% used [0x331d0000, 0x334f9040, 0x339d0000)  
  9.   from space 1024K,  39% used [0x33ad0000, 0x33b34de8, 0x33bd0000)  
  10.   to   space 1024K,   0% used [0x339d0000, 0x339d0000, 0x33ad0000)  
  11.  tenured generation   total 10240K, used 7168K [0x33bd0000, 0x345d0000, 0x345d0000)  
  12.    the space 10240K,  70% used [0x33bd0000, 0x342d0010, 0x342d0200, 0x345d0000)  
  13.  compacting perm gen  total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)  
  14.    the space 12288K,   3% used [0x345d0000, 0x3462f548, 0x3462f600, 0x351d0000)  
  15.     ro space 10240K,  55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)  
  16.     rw space 12288K,  55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)  


我们可以看到在A3处有一次GC,并且a2的7M已经满足-XX:MaxTenuringThreshold=1的要求,所以a2进入老年代,而空出来的空间a3就进入新生代

 

 

4. 动态对象年龄判定

 

为了使内存分配更加灵活,虚拟机并不要求对象年龄达到MaxTenuringThreshold才晋升老年代

如果Survivor区中相同年龄所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象在Minor GC时将复制至老年代

 

5. 空间分配担保

新生代使用复制算法,当Minor GC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC。

在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败,如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC。

说白了,新生代放不下就会借用老年代的空间来进行GC

 

 

Java垃圾收集器:

 

首先我们可以看一张图,下面这张图中列出来新生代和老生代可以用到的垃圾收集器。

技术分享

 

1. Serial 收集器 串行

单线程的串行收集器。它在垃圾收集的时候会暂停其它所有工作线程。直到收集结束。一般在客户端模式下使用。

 

2. ParNew收集器 并行

ParNew收集器是Serial的多线程版本。一般运行在Server模式下首先的新生代收集器。如果老年代使用CMS收集器,基本也只能和它进行合作。参数:-XX:+UseConcMarkSweepGC,比较适合web服务的收集器。

一般ParNew和CMS组合

 

3. Parallel Scavenge收集器 并行

 

它使用复制算法的收集器,并且是多线程的。该收集器主要目的就是达到一个可控制的吞吐量,说白了就是CPU的利用率。于是该收集器比较适合后端运算比较多的服务。

-XX:MaxGCPauseMillis每次年轻代垃圾回收的最长时间(最大暂停时间),收集器尽量保证内存回收时间不大于这个值,应该设置一个合理的值。

-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比

-XX:+UseAdaptiveSizePolicy 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.

 

4.Serial Old收集器 串行

 

单线程串行的老生代收集器。

 

5. Parallel Old 收集器 并行

使用“标记-整理”的算法。该收集器比较适合和Parallel Scavenge收集器进行组合。-XX:+UseParallelOldGC

 

6. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前大部分的B/S系统都使用CMS的收集器。一般CMS是老生代收集器,新生代就和PerNew进行组合。

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之后,跟着来一次碎片整理过程。

Java GC机制

标签:

原文地址:http://www.cnblogs.com/2714585551summer/p/5742257.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!