jvm垃圾回收

回答如下三个问题,即可掌握java虚拟机垃圾回收原理。

哪些内存可以回收?

         Java内存中的程序计数器、虚拟机栈、本地方法栈等区域随线程的产生而生,随线程的灭亡而消息。虚拟机中的栈帧随方法的进入和退出而有条不紊的执行入栈和出栈操作,每一个栈帧需要分配多少内存,在类结构明确的情况下就已经确定了。因此这几个区域的内存分配和回收都具有明确性,不需要考虑回收问题,因为方法和线程结束,内存自然就回收了。Java堆和方法区就不同了,因为只要在程序运行期才能知道要创建哪些对象,在栈中的引用类型指向这些对象的内存起始地址,当栈中引用类型随线程或方法结束而被回收后,这些对象就没有被引用时,我们就需要回收这些对象,以便其它对象需要分配相应的内存,这部分的内存分配和回收都是动态的。

正如上面讲,jvm垃圾收集器(garbage collection,gc)就是回收这些不存在任何引用的对象,即被称为已“死亡”的对象。那么,如何判断对象是“死亡”还是“存活”,主流的jvm采用可达性分析算法来判断对象是否存活。可达性分析算法就是:通过gc roots(垃圾回收根节点)向下寻找被其引用的对象,经历过的路径,称为引用链,当对象与gc roots不存在任何引用链时,即对象到gcroots不可达,说明这个对象已“死亡”,可以被回收。其中,哪些是gc roost呢?gc roots主要分为以下几类:

         虚拟机栈中的栈帧中的引用变量引用的对象;

         方法区中的静态变量引用的对象;

         方法区中常量引用的对象;

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

可以看出,判断对象是否可回收与对象引用有关,在jdk1.2之后,java对引用进行了扩充,包括强引用、软引用、弱引用和虚引用。

强引用,类似object o=new object(),只要引用存在,那么对象就不会被回收;

软引用,描述一些还有用但并非必要的对象,使用softreference修饰,当内存发生溢出时,这些软引用对象就会被回收;

弱引用,比软引用更弱的引用,描述非必要对象,使用weakreference修饰,弱引用对象只能存活到下一次垃圾收回之间;

虚引用,它是最弱的引用的关系,为对象这是虚引用的目的,是为了该对象被回收时收到一个系统通知,在jdk1.2之后,使用phantomreference类来使用虚引用。

         判断对象是“存活”还是“死亡”,可达性分析还是最终的过程,gc线程执行可达性分析后,对于那些与gc roots没有任何引用链的对象,至少要经过两次标记才能确定是否要回收。这些对象经过一次标记和过滤,过滤的条件是那些对象实现了finalize()方法,若对象没有实现或者finalize方法已经被虚拟机执行过了,那么这些对象就会被直接回收,对于那些实现finalize方法还没有执行的对象,会被放入一个f-queue队列中,虚拟机会启动一个低优先级的线程去逐个执行队列中的对象的finalize方法,这里的执行是触发这个方法,但并不是等待这个方法结束,否则若方式执行缓慢、或出现死循环,那么队列中的其它对象就会处在永久等待状态。Finalize方法是对象逃出被回收的最后唯一机会,在finalize方法中若对象被其它对象引用,那么对象就会不能被回收,这时对象会被第二次标志。举例说明上面的过程:

publicclassFinalizeEscapeGC {   

    publicstatic FinalizeEscapeGC SAVE_HOOK = null;

    publicvoid isAlive(){

         System.out.println("yes,i am still alive..");

    }

    @Override

    protectedvoid finalize() throws Throwable {

         // TODO Auto-generated method stub

         super.finalize();

         System.out.println("finalize method executed!");

         FinalizeEscapeGC.SAVE_HOOK = this;

    }

    publicstaticvoid main(String[] args)throws Throwable{

        SAVE_HOOK = newFinalizeEscapeGC();

         SAVE_HOOK= null;

         System.gc();

         Thread.sleep(500);

         if(SAVE_HOOK!=null){

             SAVE_HOOK.isAlive();

         }else {

             System.out.println("no i am dead..");

         }

         SAVE_HOOK = null;

         System.gc();

         Thread.sleep(500);

         if(SAVE_HOOK!=null){

             SAVE_HOOK.isAlive();

         }else {

             System.out.println("no i am dead..");

         }

    }

}

执行main方法,控制台提示信息:

finalize method executed!

yes,i am still alive..

noi am dead..

结果正如上文所讲,但是需要说明的是,不建议使用finalize方法,它并不像是c++中的析构函数,它的执行并不明确(上面提到,jvm不会等待执行结果),无法保证各个对象执行的顺序,因此不适合在此方法内做一些“关闭外部资源”的之类工作。

         上面讲的是java堆中的对象回收,那么在方法区是怎么执行gc呢?在方法区中回收的是废弃的常量和无用的类,常量的回收与堆中的对象回收类似,当没有任务对象引用时,gc就可以回收这个常量。类的回收比较复杂,因为判断类无用条件比较苛刻,类需要满一下三点才能算是无用的类,可被回收:

类的实例对象已回收;

加载该类的classloader已经被回收;

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

满足上面3个要求,才可以对类回收。在大量使用反射、动态代理框架、动态生成jsp中,类的及时回收是很有必要的,无用类及时回收,才能保证永久区不会溢出。

 

什么时候回收?

         Gc什么时候执行内存回收呢?gc在执行时要保证所有的其它的运行线程停止的时候,才能准确的执行内存回收,为什么要停止其它运行线程呢?线程停止才能保证对象引用关系不再发生变化,才能保证准确的执行gc,因为目前主流的java虚拟机都是采用准确式gc。举个例子:

publicclassJavaTest {

    publicstaticclassDemoObject {

       String val1;

    }

 

    /**

    * @param args

    */

    publicstaticvoidmain(String[] args) throws Exception {

        [1]DemoObject demoObject = new DemoObject();

        [2]//demoObject上挂一个字符串对象

        [3]demoObject.val1 = "this is a string object";

        [4]Thread.sleep(1000000);

    }

}

假如,gc线程与代码线程并行执行,gc线程通过扫描线程中的栈来获取被引用的对象,当代码线程执行到[1]时,gc线程发现demoOject对象,然后代码线程继续执行到[3],这时gc线程就会漏扫描对象“this is astring object”。另外,再往下一点,cpu执行运算的数据,需要将数据从内存中载入寄存器,运算完再从寄存器存入内存。对象的地址也要经过这个过程。将入一个java线程分配了一个对象A,该对象的地址存在某个寄存器中,该线程的cpu时间片到期被切换出去,这个时候gc线程开始扫描存活对象,发现没有到地址还在寄存器的这个对象的路径,那么这个对象就被当成垃圾回收,显然这是不合理的…,因此,gc执行时会出现stw(stop the world),即所有的线程都会终止,已保证对象引用关系处在一个被冻住的时间点上,不可以出现分析时对象引用关系还在不断变化的情况,否则分析结果准确性就无法得到保证。那么如何保证gc执行时代码线程停止呢?jvm采用的方式是代码线程主动中断,那么是如何主动中断呢?大家知道线程在运行时,不能够随意中断,否则会造成程序崩溃,下面会提到一个“安全点”的概念,gc执行要中断代码线程,那么执行时间就不能太长,否则就会造成项目出现卡顿的现象,影响使用效果。由上文可知,作为gc roots节点包括4种类型的应用对象,在现在的应用中往往方法区就能到达好几百兆,gc线程每次执行时都进行一次gc roots节点扫描,那么这个过程就会很耗时,不合理。因此,java采用的方式,在类加载完成时,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来,在jit编译过程中,会在特定的位置记录下内存和寄存器中哪些是引用类型,并将这些数据保存在Oopmap数据结构中,在gc线程在扫描时就可以直接从Oopmap中获取引用对象,极大提高了效率。上面提到的特定的位置就是“安全点”,当代码线程运行到安全点时就会主动中断线程,执行gc。安全点的选择不能太少以至于让gc等待时间太长,也不能太多增加运行时的负荷。因此,在实际中建议不要显示执行system.gc()操作。另外,对于安全点的延伸是安全区域,为什么需要安全区域,是因为若线程处于挂起状态,不执行时,就无法进入安全点,因此,安全区域就是在一段代码片段中,引用关系不会发生变化,在这个区域任意地方开始gc都是安全的。

如何回收?

查看具体的jvm参数,垃圾收集器使用的是哪个?

掌握Gc回收垃圾原理需要理解Java虚拟机收集垃圾采用什么方法?具体使用什么垃圾收集器?jvm垃圾回收采用的算法包括以下几种类型:

标记-清除算法

         标记-清除算法就是gc通过可达性分析后,标记需要回收的对象,然后清除这些对象。这种算法的缺点是:一个是效率不高,标记清除效率很低;另一个是空间问题,垃圾回收后,导致内存出现大量不连续碎片,当需要分配一个较大对象时就会可能无法找到足够的空间,而导致提前执行另一次垃圾收集动作。

复制算法

         复制算法是将内存分为两块区域,每次只使用其中的一块,gc只对其中一块区域执行垃圾回收,然后将存活的对象,复制到另一块区域中。这种算法解决了标记-清除算法带来的效率问题,但是一下子就把可用 的内存空间缩减到一半,使其另一半区域无法得到应用,造成空间利用率很低。

标记-整理算法

         标记-整理算法是在标记-清除算法基础上,将存活的对象移动到一端,然后直接清理掉端边界以外的内存,这种算法不会造成大量不连续碎片空间的问题。

分代收集算法

         就目前来说,主流的虚拟机根据对象存活的周期,将java堆划分为新生代和老年代,将方法区划分为永久代。新生代又划分为eden区和两个survivor区,eden区与survivor区的默认比例是8:1。主流的虚拟机都采用分代收集算法,根据ibm研究,大部分对象都是“早生夕灭”,因此,在新生代化为eden区、survivor1区和survivor2区,新生成的对象在eden区,每次只使用一个survivor区。新生代执行垃圾回收时将eden区和其中一个survivor区中存活的对象全部复制到另一个survivor区中,并清理掉eden区和刚才survivor区中使用的空间。可知,分代收集算法只能算是java虚拟机垃圾收集的总体方法,具体每代的收集算法也不相同,但是基本是上面讲的三个方法,比如此段讲的新生代使用的算法就是复制算法的原理。

         总之,上面讲的收集算法只是内存回收的方法论,jvm中内存回收具体实现是垃圾收集器,hotspot虚拟机针对不同版本的虚拟机、不同的代,包含多种垃圾收集器:

Serial收集器

         Serial收集器是最基本、历史最悠久的收集器,它是一个单线程收集器,线程执行时会中断其它所有的工作线程,目前这款收集器是虚拟机运行在client模式下的默认收集器,即c/s项目中。

Parnew收集器

         Parnew收集器是serial收集器的多线程版本,在jdk1.5版本中新增加的cms收集器用于老年代内存回收,除了serial收集器,只有parnew收集器能与cms收集器配合工作,用于新生代内存回收。

Parallel scavenge收集器

         Parallel scavenge收集器是用于新生代,采用复制算法,以吞吐量为目标的收集器。所谓吞吐量就是cpu运行工作代码的时间与总耗时比值,即吞吐量=cpu工作代码时间/(cpu工作代码时间+垃圾收集时间)。在jdk1.7和1.8中默认新生代默认使用此收集器。

Serial old收集器

         Serial old是serial收集器用于老年代,它同样是单线程收集器,使用标记-整理算法。Serial old收集器与ps marksweep收集器实现非常接近,很多时候它俩可以相互替换,由于cms收集器只能和parnew适配器配合使用,因此若老年代使用serial old(ps marksweep),那么新生代一般使用parallel scavenge收集器。

Parallel old收集器

         Parallel old是parallel scavenge用于老年代的收集器,使用多线程和“标记-整理”算法。

Cms收集器

         Cms是一个以缩短垃圾收集停顿时间为目标的收集器,采用“标记-清除”算法,用于老年代。Cms收集器执行垃圾回收需要4个过程:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记都需要中断其它工作线程,初始标记就是标记与gc roots相关联的对象,并行标记是gcroots tracing过程,重新标记是修正并发标记过程中产生的新的引用关系。并发清除就是清空非关联对象,并发标记和并发清除顾名思义同工作线程并发执行。

         Cms是一款优秀的收集器,非常适合应用在b/s系统的服务端,因为这类应用尤其注重服务端的响应速度,希望系统停顿时间最短。另外,cms采用“标记-清除”算法,这种算法会造成大量不连续的碎片,空间碎片过多,会对大对象的分配带来很大的麻烦。因此,cms收集器提供了一个参数-XX:+UseCMSCompactFullCollection开关参数(默认开启),表示进行fullgc时,cms收集器是否执行内存碎片整理。在碎片整理期间,工作线程是中断的,为了减少停顿时间,cms收集器还提供了一个参数-XX:CMSFullGCsBeforeCompaction,表示执行多少次不带压缩的fullgc,跟着来一次压缩的(默认为0,每次进入fullgc都进行碎片整理)。

G1收集器

         G1收集器是目前最技术最前沿的垃圾收集器,它的主要特点是:高并发和并行,利用多cpu来缩短停顿时间。分代回收,其它的收集器都是针对整个新生代或老年代,当使用g1收集器时,内存布局与其它收集器有很大不同,虽然还存在新生代和老年代的概念,但是新生代和老年代不再物理隔离,它们都是一部分region的集合,g1跟踪各个region里面的垃圾堆积的价值大小(所获的空间大小以及回收所需要的时间经验值),在后台维护一个优先列表,每次在允许的收集时间内,优先收集价值最大的region,从而获得更高的收集效率。空间整理,g1收集器从整体来看是采用“标记-整理”算法,从局部看是采用“复制”算法,即将对象从一个region复制到另一个region中,这就意味着g1收集器不存在碎片整理的问题,不需要进行fullgc,减少停顿。可预测性内存回收,g1收集器建立可预测的停顿时间,能让使用者指定在一个时间m毫秒内,用于垃圾回收的时间不超过n毫秒。目前g1收集器在jdk9中已是默认的垃圾收集器

         总之,本文讲了虚拟机内存分配和回收的原理以及各个不同垃圾收集器的原理。理解垃圾回收的根本目的是知道如何提高虚拟机的性能,因为垃圾回收造成的停顿往往成为高并发高性能的瓶颈。其实,根据本文可得知,提高虚拟机性能包括两个方面:降低停顿时间和提高吞吐量。像parallel scavenge收集器用于新生代,以提高吞吐量为目标,而cms、g1等收集器主要是降低停顿时间为目标。

         如何理解吞吐量与停顿时间呢?个人认为有个比喻非常形象,就是车辆通过一段路程与等待红绿灯的过程。车辆通过路程的吞吐量=车辆通过路程时间/车辆通过路径时间+等待红绿灯时间,假如车辆通过这段路程需要100分钟,等待红绿灯时间是1分钟,那么吞吐量就是99%,当路上的车辆很多,车辆可能要等待好几个红绿灯的变化才能通过,那么等待时间就会增加,意味着吞吐量降低,那么如何提高吞吐量呢?增加道路宽度,只需要一个红绿灯的变化就可通过,像parallel scavenge收集器就是利用这方式,通过多线程、多cpu达到高吞吐量,从而通过的车辆更多。但是,车辆等待时间即垃圾回收时间没有变短,cms、g1等收集器是通过降低垃圾收集时间(红绿灯等待时间)来达到通过更多的车辆,并且车辆驾驶人员的体会也会更好,因为提车等待的时间更短,大部分时间车辆都是在行驶中,没有人愿意停车等待,虚拟机也是同样的道理,执行代码的时间越长,停顿越短带来的用户体验越好。当我们请求应用时,没有人愿意看到应用出现卡顿的现象,系统越流畅效果越好。可知,吞吐量与低停顿是两个不同的目标,关注吞吐量一般是一些用于处理大量数据的应用,吞吐量越大,数据的处理越快;低停顿应用更多的是交互请求,停顿时间越低,请求交互实时效果越好。

         使用jconsole工具,查看jdk1.7(或者jdk1.8)版本中默认垃圾收集器使用状况如下:

可知,新生代使用了ps scavenge收集器,老年代使用了psmarksweep收集器。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值