深入理解JVM垃圾回收机制剖析_第1页
深入理解JVM垃圾回收机制剖析_第2页
深入理解JVM垃圾回收机制剖析_第3页
深入理解JVM垃圾回收机制剖析_第4页
深入理解JVM垃圾回收机制剖析_第5页
已阅读5页,还剩27页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、Java语言出来之前,大家都在拼命的写 C或者C+的程序,而此时存在一个很大的矛盾, C+等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构 造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的析构。于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?I960年基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Java的专利,GC的历史远远大于 Java 的历史!那究竟GC为我们做了什么操作呢?1、哪些内存需要回收?2

2、、什么时候回收?3、如何回收?这时候有人就会疑惑了, 既然GC已经为我们解决了这个矛盾, 我们还需要学习 GC么? 当然当然是肯定的,那究竟什么时候我们还需要用到的呢?1、排查内存溢出2、排查内存泄漏3、性能调优,排查并发瓶颈我们知道,GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?1、对象没有引用2、作用域发生未捕获异常3、程序在作用域正常执行完毕4、程序执行了 System.exit()5、程序发生意外终止(被杀进程等)其实,我们最容易想到的就是当对象没有引用的时候会将这个对象标记为可回收对象,那么现在就有一个问题,是不是这个对象被赋值为null以后就一定被标记为可回

3、收对象了呢?我们来看一个例子:package com.yhj.jvm.gc.objEscape.fi nalizeEscape;import com.yhj.jvm.gc.objEscape.pojo.Fi nalizedEscapeTestCase;* Described :逃逸分析测试* author YHJ create at 2011- 12- 24 下午 05:08:09* FileNmae com.yhj.jvm.gc.fi nalizeEscape. Fin alizedEscape.java */public class Fin alizedEscape public stat

4、ic void main( Stri ngargs) throws In terruptedExcepti on System. out .println(FinalizedEscapeTestCase.caseForEscape);FinalizedEscapeTestCase.caseForEscape = new FinalizedEscapeTestCase();System.out .println(FinalizedEscapeTestCase.caseForEscapehFin alizedEscapeTestCase.caseForEscape= n ullJSystem.gc

5、 ();Thread.sleep (100);System.);out .println(FinalizedEscapeTestCase.caseForEscapepackage com.yhj.jvm.gc.objEscape.pojo;* Described:逃逸分析测试用例* author YHJ create at 2011- 12- 24 下午 05:07:05* FileNmae com.yhj.jvm.gc.pojo.TestCaseForEscape.java*/public class Fin alizedEscapeTestCase caseForEscape = n ul

6、lpublic staticFin alizedEscapeTestCase Overrideprotected void fin alize()throws Throwable super .finalize();System.out .println(哈哈,我已逃逸!);caseForEscape = this程序的运行结果回事什么样子的呢?我们来看这段代码1、System.out .println(FinalizedEscapeTestCase.caseForEscape);2、Fin alizedEscapeTestCase.caseForEscape=new FinalizedEsc

7、apeTestCase();3、System.out .println(FinalizedEscapeTestCase.caseForEscape);4、Fin alizedEscapeTestCase.caseForEscape=n ull ;5、System.gc ();6、Thread.sleep (100);7、System.out .println(FinalizedEscapeTestCase.caseForEscape);1、当程序执行第一行是,因为这个对象没有值,结果肯定是n ull2、程序第二行给该对象赋值为新开辟的一个对象3、第三行打印的时候,肯定是第二行对象的hash代码

8、4、第四行将该对象重新置为null5、第五行触发GC6、为了保证GC能够顺利执行完毕,第六行等待100毫秒7、第七行打印对应的值,回事null么? 一定会是null么?我们来看一下对应的运行结果本例中打印了GC的日志,让我们看的更清晰一点,我们很清晰的看出,最后一句打印的不是null,并且子啊之前,还出现了逃逸的字样。说明这个对象逃逸了,在垃圾回收之前逃逸了,我们再来看这个pojo的写法,就会发现,我们重写了方法finalize,而这个方法就相当于 C+中的析构方法,在GC回收之前,会先调用一次这个方法,而这个方法又将this指针指向他自己,因此得以成功逃逸!可见,并不是这个对象被赋值为nul

9、l之后就一定被标记为可回收,有可能会发生逃逸!下面我们来看一下几种垃圾收集算法1、在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会 产生方法区,堆栈、 程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间 中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题当我们的代码出现下面的情形时,该算法将无法适应a) ObjA.

10、obj = ObjBb) ObjB.obj - ObjA这样的代码会产生如下引用情形objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。2、根搜索算法根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, 从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点 的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点, 即无用的节点。目前java中可作为GC Root的

11、对象有1、虚拟机栈中引用的对象(本地变量表)2、方法区中静态属性引用的对象3、方法区中常量引用的对象4、本地方法栈中引用的对象(Native对象)说了这么多,其实我们可以看到,所有的垃圾回收机制都是和引用相关的,那我们来具体的来看一下引用的分类,到底有哪些类型的引用?每种引用都是做什么的呢?Java中存在四种引用,每种引用如下:1、强引用只要引用存在,垃圾回收器永远不会回收Object obj = new Object。;/可直接通过 obj取得对应的对象 女口 obj.equels(new Object。);2、软引用而这样obj对象对后面new Object的一个强引用,只有当obj这个引

12、用被释放 之后,对象才会被释放掉,这也是我们经常所用到的编码形式。非必须引用,内存溢出之前进行回收,可以通过以下代码实现Object obj = new Object();SoftRefere nce sf = new SoftRefere nce(obj);obj = n ull;sf.get();有时候会返回null这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null ;软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓

13、存数据,从真正的来源查询这些数据。3、弱引用第二次垃圾回收时回收,可以通过如下代码实现Object obj = new Object();WeakRefere nce Objectwf = new WeakRefere nce(obj); obj = n ull;wf.get();有时候会返回nullwf.isEnQueued();返回是否被垃圾回收器标记为即将回收的垃圾弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回 n ull。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返

14、回对象是否被垃圾回收器4、虚引用(幽灵/幻影引用)垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现Object obj = new Object();Pha ntomRefere nce pf = new Pha ntomRefere nce(obj); obj=n ull;pf.get(); 永远返回 nullpf.isEnQueued();返回从内存中已经删除虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据

15、的回收比例是不同的,根据IBM的统计,数据如下图所示:我们知道,方法区主要存放类与类之间关系的数据,而这部分数据被加载到内存之后, 基本上是不会发生变更的,Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈, 因此回收率可认为是 100%;而程序计数器我们前面也已经提到,主要用户记录线程执行的 行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是 性价

16、比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在 现在高性能分布式 J2EE的系统中,我们大量用到了反射、动态代理、CGLIB JSP和OSGI等, 这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久带不会溢出, 他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!1、所有实例被回收2、加载该类的 ClassLoader被回收3、Class对象无法通过任何途径访问(包括反射)好了,我们现在切入正题,Java1.2之前主要通过引用计

17、数器来标记是否需要垃圾回收,而1.2之后都使用根搜索算法来收集垃圾,而收集后的垃圾是通过什么算法来回收的呢?1、标记-清除算法2、复制算法3、标记-整理算法我们来逐一过一下1、标记-清除算法标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再 扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活 对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!2、复制算法复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,

18、极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。也就是我们前面提到的s0 si等空间。3、标记-整理算法标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。 标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决 了内存碎片的问题。我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,下图展示了minor GC的执行过程。对于新生代和旧生代,JVM可使用很多种垃圾回收器进行垃

19、圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运 用于不同的场景。如下图所示F面我们来逐一介绍一下每个垃圾回收器。1、Serial收集器看名字我们都可以看的出来,这个属于串行收集器。其运行示意图如下Serial收集器是历史最悠久的一个回收器,JDK1.3之前广泛使用这个收集器,目前也是Clie ntVM下ServerVM 4核4GB以下机器的默认垃圾回收器。串行收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结

20、束为止,因此又号称Stop The World 的垃圾回收器。注意,JVM中文名称为java虚拟机,因此它就像一台虚拟的电脑一样在工作,而其中的每一个线程就被认为是JVM的一个处理器,因此大家看到图中的 CPU0 CPU1实际为用户的线程,而不是真正机器的CPU,大家不要误解哦。串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。Seria I收集器默认新旧生代的回收器搭配为Serial+ SerialOld2、ParNew收集器ParNew收集器其实就是多线程版本的SeriaI收集器,其运行示意图如下同样有Stop T

21、he World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收 效率远远低于SeriaI收集器,所以一定要注意场景哦),也是 Server模式下的默认收集器。3、ParallelScave ngeParallelScave nge又被称为是吞吐量优先的收集器,器运行示意图如下ParallelScave nge所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了 100 分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是 99%。在当今网络告诉发达的今天,良 好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用C

22、PU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回 收器。4、ParallelOldParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先 的一个收集器。这个收集器是JDK1.6之后刚引入的一款收集器,我们看之前那个图之间的关联关系可以看到,早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下5、SerialOldSerialOld是旧生代Client模式下的默认收集器,单线程执行;在 JDK1.6之前也是

23、 ParallelScve nge回收新生代模式下旧生代的默认收集器,同时也是并发收集器 CMS回收失败后的备用收集器。其运行示意图如下6、CMSCMS又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法,CMS对CPU是非常敏感的,它的回收线程数 =(CPU+3) /4,因此当CPU是2核的实惠,回收线程将占用的CPU资源的50%,而当CPU核心数为4时仅占用25%。他的运行示意图如下CMS模式主要分为4个过程 在初始标记的时候,需要中断所有用户线程,在并发标记阶段,用户线程和标记 线程并发执行,而在这个过程中,随着内存引用关系的变化, 可能会发生原来标记的对象

24、被释放, 进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。CMS为了确保能够扫描到所有的对象,避免在Initial Marking中还有未标识到的对象,采用的方法为找到标记了的对象,并将这些对象放入Stack中,扫描时寻找此对象依赖的对象,如果依赖的对象的地址在其之前,则将此对象进行标记,并同时放入Stack中,如依赖的对象地址在其之后,则仅标记该对象。在进行Concurrent Marking时minor GC也可能会同时进行,这个时候很容易造成旧 生代对象引用关系改变,CMS为了应对这样的并发现象,提供了一个Mod UnionTable来进行记录,在这个 Mod Union

25、Table中记录每次 minor GC后修改了的Card的信息。这也是 ParallelScavenge不能和 CMS一起使用的原因。CMS产生浮动垃圾的情况请见如下示意图在运行回收过后,c就变成了浮动垃圾。由于CMS会产生浮动垃圾,当回收过后,浮动垃圾如果产生过多,同时因为使用 标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代 移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次 FULL GC,而这时候则采用 SerialOld进行二次回收。同时CMS因为可能产生浮动垃圾,而CMS在执行回收的同时新生代也有可能在进行回收操作,为了保证旧生代能够存放

26、新生代转移过来的数据,CMS在旧生代内存到达全部容量的 68%就触发了 CMS的回收!7、 GarbageFirst(G1 )我们再来看垃圾回收器的总图,刚才我们可以看到, 我在图上标记了一个?,其实这是一个新的垃圾回收器,既可以回收新生代也可以回收旧生代,Su nH otSpot1.6u14以上EarlyAccess版本加入了这个回收器,sun公司预期SunHotSpot1.7发布正式版,他是商用高性能垃圾回收器,通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间,但是杯具的是被 oracle收购之后这个收集器属于商用 收费收集器,因此目前基本上没有人使用,我们在这里也就不多介绍

27、,更多信息可以参考oracle新版本JDK说明。下面我们再来看下 JVM的一些内存分配与回收策略1、 优先在Edon上分配对象代码示例package com.yhj.jvm.gc.ede nFi rst;/* Described : Edon优先划分对象测试*VM params :- Xms20M - Xmx20M - Xmn 10M - XX:+PrintGCDetails- verbose:gc* Edon s0 si old* 8 1 1 10* author YHJ create at 2012-1-3 下午 04:44:43* FileNmae com.yhj.jvm.gc.ede

28、nFi rst.Ed onF irst.java*/public classEdo nF irst private finalstaticint ONE_MB = 1024*1024;* param args* Author YHJ create at 2012-1-3 下午 04:44:38*/public static void main( Stri ng args) SuppressWarnings( unused )byte testCase1,testCase2,testCase3,testCase4;testCase1 =newbyte2*ONE_MB;testCase2 =new

29、byte2*ONE_MB;testCase3 =newbyte2*ONE_MB;/testCase1 = n ull;/testCase2 = n ull;/testCase3 = n ull;testCase4 =newbyte2*ONE_MB;运行结果结果分析从运行结果我们可以很清晰的看到, eden有8MB的存储控件(通过参数配置),前 6MB 的数据优先分配到 eden区域,当下一个2MB存放时,因空间已满,触发一次 GC,但是这部分数据因为没有回收(引用还在,当赋值为 null后则不会转移),数据会被复制到sO区域,但是sO区域不够存储,因此直接放入老生代区域,新的2MB数据存放在e

30、den区域2、 大对象直接进入老生代代码示例package com.yhj.jvm.gc.bigObj In toOld;/* Described :大对象直接进入老生代测试*VM params :- Xms20M - Xmx20M - Xmn 10M - XX:+PrintGCDetails- verbose:gc* Edon sO si old* 8 1 1 10* author YHJ create at 2012-1-3 下午 05:28:47* FileNmae com.yhj.jvm.gc.bigObjl ntoOld.BigObjl ntoOld.java*/public cla

31、ssBigObjl ntoOld private final static int ONE_MB = 1024*1024;/* param args* Author YHJ create at 2012-1-3 下午 04:44:38*/public static void main( Stri ng args) SuppressWarnings( unused )byte testCase1,testCase2,testCase3,testCase4;testCase1 =new byte 8* ONE_MB;/ testCase2 = new byte2*ONE_MB;/ testCase

32、3 = new byte2*ONE_MB;/ testCase1 = null;/ testCase2 = null;/ testCase3 = null;/ testCase4 = new byte2*ONE_MB;我们看到,没有触发 GC日志,而数据是直接进入老生代的3、年长者(长期存活对象)进入老生代代码示例:package com.yhj.jvm.gc.lo ngLifeTimel ntoOld;/*/public static void main( Stri ng args) SuppressWarnings( unused )byte testCase1,testCase2,tes

33、tCase3,testCase4;* Described:当年龄大于一定值的时候进入老生代默认值15岁*VM params :- Xms20M - Xmx20M - Xmn10M - XX:MaxTenuringThreshold=1 - XX:+PrintGCDetails- verbose:gc* Edon s0 si old age* 8 1 1 10 1* author YHJ create at 2012-1-3 下午 05:39:16* FileNmae com.yhj.jvm.gc.longLifeTimelntoOld丄ongLifeTimel ntoOld.java*/pub

34、lic class Lon gLifeTime In toOld private final static int ONE_MB = 1024param args* Author YHJ create at 2012-1-3 下午 04:44:381024;从代码中我们可以看到,当testCasel 划分为0.25MB数据,进行多次大对象创建之后,testCasel 应该在GC执行之后被复制到 s0区域(s0足以容纳testCasel ),但是我们设 置了对象的年龄为1,即超过1岁便进入老生代,因此GC执行2次后testCasel 直接被复制到了老生代,而默认进入老生代的年龄为15。我们通过p

35、rofilter的监控工具可以很清楚的看到对象的年龄,如图所示右侧的年代数目就是对象的年龄4、群体效应(大批中年对象进入老生代)代码示例package com.yhj.jvm.gc.d yn amicMoreAVGn toOld;* Described : s0占用空间到达50%直接进入老生代VM params :- Xms20M - Xmx20M - Xmn10M - XX:MaxTenuringThreshold=15 - XX:+PrintGCDetails- verbose:gc* Edon s0 si old age* 8 1 1 10 15* 0.5 0 0 7.5* 7.5 0.

36、5 0 7.5* 7.5 0 0 8* author YHJ create at 2012-1-3 下午 05:50:40* FileNmae com.yhj.jvm.gc.dy namicMoreAVGn toOld.MoreAVGn toOld.java*/public class MoreAVG_i ntoOld private final static int ONE_MB = 1024*1024;/* param args* Author YHJ create at 2012-1-3 下午 04:44:38*/public static void main( Stri ng args

37、) SuppressWarnings( unused )byte testCase1,testCase2,testCase3,testCase4;testCase1 =newbyte7*ONE_MB+ONE_MB/2;testCase2 =newbyteONE_M臼2;testCase3 =newbyte7*ONE_MB+ONE_MB/2;testCase3 =nullJtestCase4 =newbyte7*ONE_MB+ONE_MB/2;/ testCase1 = new byte7*ONE_MB+3*ONE_MB/4;/ testCase2 = new byteONE_MB/4;/ testCase3 = new byte7*ONE_MB+3*ONE_MB/4;运行结果结果分析我们看到,当创建后 testCase3 , testCase2被移动到S0区域,当被释放后,继续创建testCase3 ,按理说testCase2应该移动到si区域,但是因为超过了 si区域的1/2,因此直接进入老生代5、担保G

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论