Java虚拟机(五)-JVM垃圾回收策略

垃圾收集简史

       垃圾收集提供了内存管理的机制,使得应用程序不需要在关注内存如何释放,内存用完后,垃圾收集会进行收集,这样就减轻了因为人为的管理内存而造成的错误,比如在C++语言里,出现内存泄露时很常见的。

       Java语言是目前使用最多的依赖于垃圾收集器的语言,但是垃圾收集器策略从20世纪60年代就已经流行起来了,比如Smalltalk,Eiffel等编程语言也集成了垃圾收集器的机制。

垃圾收集算法的核心思想

       Java语言建立了垃圾收集机制,用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象。该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用。

       垃圾收集算法的核心思想是:对虚拟机可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,那么称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配。垃圾收集算法的选择和垃圾收集系统参数的合理调节直接影响着系统性能,因此需要开发人员做比较深入的了解。

在讲垃圾收集算法之前,需要了解堆的内存分配原则:

JVM堆一般又可以分为以下三部分:

一、Perm

       Perm代主要保存class,method,filed对象,这部分的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。

二、Tenured

       Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

三、Young

       Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

四、Total Heap

   -Xms  :指定了JVM初始启动以后初始化内存。

   -Xmx:指定JVM堆得最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存。

   -Xmx -Xms之差就是三个Virtual空间的大小

五、Young Generation

   -XX:NewRatio=8意味着tenured和 young的比值8:1,这样eden+2*survivor=1/9

堆内存

   -XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.

   -Xmn  参数设置了年轻代的大小

六、Perm Generation

   -XX:PermSize=16M -XX:MaxPermSize=64M

常用的垃圾回收算法

垃圾回收的核心问题有三个:(1)回收哪些内存 (2)何时回收 (3)如何回收。

       在Java中,需要回收的内存区域包括堆和方法区。方法区在Hotspot中又被称为“永生代”,主要收集这两方面的内容:废弃常量和无用的类。废弃常量比较容易理解,例如常量区存在“abc”的字符串常量,当系统中没有任何String指向“abc”时,则“abc”可以被回收。无用的类的判断要复杂一些,必须同时满足以下三个条件:

   1、Java堆中不存在该类的任何实例

   2、加载该类的classloader已经被回收

   3、该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该方法。

垃圾内存判断算法

1、引用计数法

   引用计数法是最为出名的一种垃圾内存判断算法,在很多教科书中都出现过。其原理是:给对象添加一个引用计数器,每当有其他地方引用它时,计数器就增1;当失去一个引用时,计数器值减1;计数器为0时则说明这个对象可以被回收。

   这种算法的优点是简单、高效,缺点是无法处理相互引用的情况。例如下面这段代码,对象A和B都有字段instance,令A.instance = B及B.instance=A,除此之外,这两个对象再无引用,实际上这两个对象都可以被回收,但因为他们相互引用,引用计数始终大于1,因此使用这种方式无法回收此类垃圾内存。

public Class RCGC{  
   public Object instance = null;
   private staic final int _1MB = 1024 * 1024;  
   public staic void test(){  
   RCGC A = new RCGC();  
   RCGC B = new RCGC();  
   A.instance = B;  
   B.instance = A;  
   A = null;  
   B = null  
   System.gc(); 
   }  
} 

2、根搜索算法

         目前主流的程序设计语言中,都是使用根搜索算法来判断对象是否存活。此算法的思路是从一系列“GC Roots”的对象作为起始点,开始向下搜索,搜索走过的路径成为“引用链”,当一个对象和GC Roots之间没有任何一条引用链相连时,则证明此对象不可到达。如下图中的object 5, 6, 7没有到GC Roots的引用链,则可以被回收。

3、finalize方法

        根搜索算法不可到达的对象也不会被马上回收,甚至还可能通过finalize方法“复活”。要宣布一个对象的“死刑”,至少要经过两次标记过程:如果对象中进行根搜索后发现没有与GC Roots相连,则被第一次标记并进行筛选。筛选的过程是检查对象是否需要执行finalize方法,若对象没有覆盖finalize方法或finalize方法已经执行过,则虚拟机将这两种情况视为“没有必要执行”finalize方法。

        如果这个对象被JVM判定为需要执行finalize方法,则它会被放入一个F-Quene队列中,并在稍后由一条JVM自动建立的、低优先级的Finalize线程去执行。这里的“执行”是指会触发这个方法,但并不承诺会等待它结束。这么做的原因是:若某个对象的finalize方法执行缓慢或陷入死循环,将可能导致F-Quene队列中对象永久处于等待状态,无法完成回收。而这将导致整个JVM内存回收系统崩溃。

垃圾收集算法

        判断出需要回收的垃圾内存后,下一步要做的是确定如何回收内存。下面介绍几种常用的垃圾收集算法的思想:

标记 -清除算法

        从名字可以看出,这种算法分标记和清除两个阶段执行:首先标记出要回收的对象,标记完成后统一回收对象。这种算法的一个明显缺陷是容易产生大量不连续的内存碎片,如下图所示:

复制收集算法

        复制算法使用两块内存,当一块内存空间不足时,将还存活的对象复制到另一块内存中,这样就可以避免碎片的问题。复制算法是目前商业虚拟机都在使用的一种方法。因为新生代中对象垃圾收集率很高,因此不需要按1:1的比例来分配内存,而是将内存分为一块儿比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor空间。回收时将Eden和那块Survivor中还活着的对象一次性拷贝到另一块Survivor中,最后清理掉Eden和那块Survivor。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,这样每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存空间被“浪费”。当占10%的Survivor空间不能存放GC后的内存怎么办?此时需要依赖其他内存(这里指老年代)进行空间分配担保(Handle Promotion)。

空间分配担保:

       在介绍下面的内容之前,首先介绍两个名词:Minor GC和Full GC。

       Minor GC:在新生代中进行的垃圾回收。

       Full GC:在老年代和新生代中都进行垃圾回收。

       一般情况下,Full GC花费的时间远超Minor GC。

       在理想的复制算法中,应该由两块相等大小的内存来进行相互复制和清理,但这样会带来巨大的空间浪费。在新生代,大部分对象的生命期很短,因此Eden和两个Survivor在大多数情况下足够了。为了应对新生代中大量对象Minor。

       老年代进行空间分配担保后,Survivor中无法存放的对象将被直接放到老年代中,因此进行担保的前提是老年代本身的剩余空间足以容纳这些对象。因为主担保时JVM无法获知下次GC时剩余对象的大小,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间比较,并决定是否进行Full。

       使用老年代进行空间分配担保并不能保证万无一失,若某次Minor GC后存活的对象突增,远高于平均值,依然会导致担保失败(Hanlde Promotion Failure)。若出现担保失败,则只好马上发起一次FullGC。

标记 -整理算法

       复制收集算法在对象存活率较高时就要执行更多的复制操作,导致效率降低,所以在老年代中一般不会直接使用复制整理算法。根据老年代对象存活率较高的状况,有人提出了标记 -清除算法的改进版本:标记 - 整理算法。其标记过程与标记 -清除算法一样,但后面的步骤不是对直接可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理边界以外的内存,如下图所示:

分代收集算法

      当前商业JVM的GC基本都采用“分代收集算法”,即将对象按其生命周期分别存放到不同的内存空间中。Java堆一般分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次收集时对象的存活率不高,适用于复制收集算法,因为只需付出少量的对象复制成本。老年代中因为对象存活率高、没有额外空间对它进行分配担保,使用“标记 - 清理”算法或“标记 -整理”算法进行回收比较合适。






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值