JVM笔记整理(第3章)

来源:互联网 发布:轩辕网络上市 编辑:程序博客网 时间:2024/05/22 03:30

资料来源:《深入理解java虚拟机》


本章主要讲解了三部分内容:1、如何判定一个java对象已经死亡(为垃圾收集做准备,因为对象死亡才能进行回收其对应的内存)。2、java对象死亡后,如何去回收内存。即为垃圾收集算法思想的讲解。同时讲解了算法对应的实现:即垃圾收集器,包括其对应实现的原理及其特征和适用场景。3、了解了1和2后,接下来要知道的就是:生成了一个java对象后,内存是怎么给其分配空间的。发现自己越来越喜欢这本书,逻辑性特别好,讲解也很清楚。之前开始读这本书的原因是技术学习的需要,现在的原因是我开始喜欢它了>_<

 

 

关于回收区,我们要知道的是:java虚拟机栈、本地方法栈、程序计数器,这三部分内存都是线程私有的,故随线程而灭。因此这3个内存的分配和回收具有确定性,故不需要过多考虑回收问题,因为方法结束或者线程结束时,内存自然回收了。所以我们需要考虑回收的只有2个线程公有区域:java堆和方法区。

 

 

1、对象已死吗?

有2个算法可以判定一个java对象是否已死。可达性分析算法是主流商用程序语言的主流实现中真正使用的;引用计数法因其自身明显的缺点导致并不是主流java虚拟机实现内存管理的算法,但这并不是说没人使用它,其实它也有一些比较著名的使用案例。

 

1.1、可达性分析算法

★算法原理:首先找到一系列被称为“GC Roots”的点作为起点,然后从这些节点开始沿着“引用链”向下搜索。当一个对象到GC Roots没有任何引用链时,则说明这个对象是不可用的。

注1:引用链定义:上述搜索过程从一个object到另一个object走过的路径。

注2:可以作为“GC Roots”的点:有4类,如下:

◆  虚拟机栈(栈帧中本地变量表)中引用的对象。

◆  方法区中类静态属性引用的对象。

◆  方法区中常量引用的对象。

◆  本地方法栈中JNI(一般说的Native方法)引用的对象。

注3:方法中,不可达对象并不是“非死不可”的。要宣告一个对象真正死亡,至少要经历2次标记过程。第一次标记并筛选,如果此次筛选中,对象成功和GC Roots连接上,则在第二次标记中,对象会被移出“即将回收”集合,否则才会被回收。try-finally可以实现帅选的功能。

 

1.2、引用计数法

★算法原理:给java对象添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器就-1。(这也是很多面试者中常见的说法,不好啊)

★算法缺点:很难解决对象直接互相引用的问题。例子如下:

 objA.instance=objB.instance;

 objB.instance=objA.instance;因为它们互相引用,所以它们的引用计数器永远不会为0,导致无法通知GC收集器回收它们。

 

1.3、补充:引用

这两个算法中都提到了引用。那在JDK1.2之后有4类引用。强度依次减弱。

◆  强引用:只要强引用存在,则对应的java对象就不会被回收。

所谓强引用,就是类似于“Object a=newObject()”,a这样的引用。

◆  软引用:用来描述一些有用但非必需的对象。在系统将要发生溢出之前,会把这些若引用的对象列进回收范围内进行第二次回收。如果此次回收后内存还是不够用,则抛出内存溢出异常。

◆  弱引用:用来描述非必需对象。被若引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器开始工作时,无论内存是否够用,都会回收被若引用关联的对象。

◆  虚引用:不对对象的生存时间构成影响。存在的唯一意义是:在对象被回收时,会收到一个系统通知。别名:幽灵引用or 幻影引用。

 

1.4、补充:方法区回收对象

注1:方法区回收性价比要比java堆低很多。通常,java堆中新生代的回收可以释放70%-95%空间。

 

★方法区回收对象有2类:废弃常量、无用的类。

★如何判定是废弃常量

  非常简单:没有任何String对象引用常量池中的该对象,也没有其他地方引用了这个字面量。

★如何判定是无用的类

必须同时满足一下3个条件:

  a、该类所有的实例均已经被回收。

  b、加载该类的ClassLoader已经被回收。

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

注2:满足上述3个条件的类是可以被虚拟机回收,但不是一定会被回收。

 

 

2、垃圾收集算法

注:共有4个算法,第1个是最原始的算法,空间利用率和效率都比较低;第2个和第3个都是对第1个的改进,所以第2个效率有所提升,但是空间利用率不高;第3个在空间和效率上都有所提升。第4个是是第2个和第3个针对不同的使用对象,做出的综合方案。

 

2.1、标记-清除算法

★工作原理:算法分标记和清除两步。首先,标记出所有需要回收的对象。然后,全部标记完成后,统一回收。

★不足:

   a效率低:标记和清除过程效率都很低。

 b空间问题:标记清除后,产生大量碎片,这会导致后面再给对象分配内存时,可能没有足够大的一块内存可以容纳新对象,从而必须提前出发新一次的GC。

 

2.2、复制算法

★工作原理:将可用内存划分为大小相等的两块。每次只使用其中一块。一块用完了,将存活的对象复制到另一块,然后清理掉刚刚使用的内存块。这样每次只能使用半个内存空间。

★优势:运行高效,实现简单。

★缺点:内存缩小为原来的一半。

★适用对象:新生代。(很重要)

 注1:java中98%对象是“朝生夕死”,故不按照1:1分配内存。实际分配是:内存划分为1个Eden区、2个Survivor区。每次使用1个Eden区、1个Survivor区。当回收时,将Eden和Survivor中存活的对象复制到另一个未使用的Survivor中。HotSpot默认Eden:Survivor=8:1。

 

2.3、标记-整理算法

★工作原理:前面标记-清除算法一样,但后续步骤不一样:不是直接对可回收对象进行清理,而是让所有存活的对象都移动到一端,然后直接清理掉边界以外的内存。

★适用对象:老年代

 

2.4、分代收集算法

★工作原理:根据对象存活周期的不同将内存划分为几块。一般将java堆分为新生代和老年代。然后根据各个年代的特点采用最实用的方法。

★定位:当前商业虚拟机的垃圾收集都采用“分代收集”算法。(很重要)

 

2.5、HotSpot算法实现

★如何查找根节点(GCRoots)

   ◆停止所有的java执行线程(“stop the world”)

   ◆准确式GC:当执行系统停下来时,不需要一个不漏的检查完所有执行上下文和全局的引用位置。在特定位置上(注意:非所有指令位置),虚拟机通过OopMap数据结构在类加载时,将对象内什么偏移量上是什么类型的数据计算出来,并存储到其中,来达到这个目的。

★如何保证根节点获取的全部正确,没有遗漏

   ◆上述准确式GC中的特定位置就是:安全点。可想而知,不是程序的什么位置都能停下来进行GC,只能在程序的安全点停下来。

   ◆安全点选定原则:以程序“是否具有让程序长时间执行的特征”为标准。

   ◆一个程序如何在安全点停下来进行GC:2个策略。

       a抢占式中断 b主动式中断

注1:因为目前没有虚拟机实现a,所以只说主动式思想:当GC需要中断线程时,不直接对线程操作,仅仅简单设置一个标志,各个线程执行时主动轮询这个标志,发现为真就自己挂起。

   ◆因为安全点的检测只能是在运行的线程,对于处于Sleep和Blocked状态的线程则不适用,故这样的线程需要安全区域来实现。

◆安全区工作原理:a、当线程进入SafeRegion时,首先标识字节进入了Safe Region。这样JVM发起GC时,不用管标识自己进入Safe Region的线程了。b、当线程离开Safe Region时,它需要检查系统是否完成了根节点当枚举,如果完成了,那线程就继续执行,否则它必须等到直到收到可以安全离开Safe Region的信号为止。

 

 

2.6、7个垃圾收集器

注:掌握内容有:特点、适用对象,其次终点掌握CMS和G1。

 

★Serial收集器

◆工作原理:单线程 or单CPU,去完成垃圾收集工作

◆适用对象:Client模式下,新生代的默认收集器

 

★ParNew收集器

◆工作原理:Serial的多线程版本

◆适用对象:Server模式下,新生代的默认收集器

◆说明:除了Serial收集器,它是唯一一个能和CMS配合工作的。

 

★ParalellScavenge收集器

◆工作原理:目标:达到一个可控制的吞吐量。

  注:吞吐量=运行用户代码t/(运行用户代码t+垃圾收集t)

◆适用对象:新生代

◆吞吐量控制的2个参数:a控制最大垃圾收集停顿时间。b、吞吐量大小。

◆GC停顿时间缩短代价:吞吐量下降、新生代调小

◆GC自适应调节:打开“开关参数”字段,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间or最大吞吐量。

 

 

★Paralell Old收集器

◆工作原理:多线程、标记-整理算法

◆适用对象:注重吞吐量和CPU资源敏感的场合,可以优先考虑Paralell Scavenge+Paralell Old

◆JDK1.6开始提供此收集器

 

 

★CMS(ConcurrentMark Sweep)收集器

◆工作原理:基于“标记-清除”算法。

◆目标:获取最短回收停顿时间

◆适用对象:注重服务的响应速度,希望系统停顿时间最短,以给用户带来较好的用户体验。CMS非常适用这类应用的需求。

◆标记分4个步骤:

   a、初始标记:仅仅标识一下GC Roots能关联的点。

   b、并发标记:就是进行GC Roots Tracing的过程。

   c、重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

   d、清除标记

         注1:其中a和c依旧需要“stop the world”。

         注2:“重新标记”用时比“初始标记”稍长,但远比“并发标记”短。

         注3:耗时最长的两个过程:并发标记、并发清除。

         注4:可以与用户进程并发的是:并发标记、并发清除。

                不可以与用户进程并发的是:初始标记、重新标记。

◆3个明显缺点

 a、对CPU资源非常敏感。当CPU在4个以上时,并发回收时垃圾收集线程占用25%以上的CPU资源。当不足4个时,大概占用50%的CPU资源,严重影响用户进程的执行速度。

b、无法处理浮动垃圾。

注1:浮动垃圾是指:并发清理阶段,因为与用户进程是并行的,此时用户进程还会产生新的垃圾。这一部分垃圾称为“浮动垃圾”。同时,这一点也说明了:垃圾收集阶段,用户进程还要工作,所以必须要给用户线程预留足够的内存空间,因此CMS收集器不能和其他收集器一样,等到老年代都要填满了才进行收集操作。如果CMS预留内存不够用,就会出现“Concurrent Mode Failure”失败,这时虚拟机将启动预备方案:临时启用SerialOld收集器来重新进行老年代对回收。

c、有大量空间碎片产生。因为其是基于“标记-清除”算法的。

 

 

★G1(Garbage-First)收集器

◆工作原理:多线程、标记-整理算法

◆适用对象:面向服务端

◆历史地位:当今收集器技术发展的最前沿成果之一

◆4个特点:

  a、能充分利用多CPU、多核环境下的硬件优势。部分其他收集器需要停顿java线程的操作,G1收集器可以通过并发方式让java程序继续执行。

  b、可以独立管理整个GC堆。同时根据不同对象使用不同处理方式。

  c、整体看,基于“标记-整理”算法;局部看,基于“复制”算法。

  d、可预测停顿、低停顿。可预测停顿是对CMS的一巨大优势。

  注1:可预测实质:有计划的避免在整个java堆进行全区域的垃圾收集。

  注2:可预测原理:后台维护一个优先列表,列表里面存放了G1跟踪各个Region里面的垃圾堆积的价值的大小。每次回收,根据优先列表,优先回收价值最大的Region。这保证了G1在有限时间内尽可能高的收集效率。

◆内存布局:采用G1管理内存时,内存被分为很多个大小相等的独立区域(Region)。新生代、老年代不再是物理隔离,它们是一部分Region的集合。

 注1: Remembered Set用于解决问题:Region之间的对象引用;其他收集器的新生代和老生代之间的对象引用。

 注2:Remembered Set工作机制:G1中每个Region都维护一个RememberedSet,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中。如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行垃圾收集时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

◆和CMS对比:都立足于低停顿。G1虽然不错,但距离成熟发布版本是时间比较短,经历的实际应用的考验比较少。所以在现有的收集器没有出现什么问题的情况下,没有理由选择G1。

 

2.7、GC日志格式

注:每种收集器的日志格式都有它们自身去实现,即可以不一样。但为方便用户阅读,它们都维持了一定的共性。下面对一个典型的例子说明一下各个部分的含义。

33.125: [GC [DefNew:3324K->152K(3712K),0.0025925 secs]2234K->152K(11904K), 0.0031680 secs]

 

下划线共有6处,依次为:

a、代表了GC发生的时间,这个数字的含义是从JVM启动以来经过的秒数。

b、如果有Full,即为FullGC时,表示这次GC是发生了“Stop the World”的。

c、表示GC发生的区域。新生代或者老年代。

d、表示“GC在该内存区域内已使用容量->GC后该内存区域已使用容量”。

e、表示该内存区域GC所占用的时间,单位是秒。

f、表示GC前java堆已使用容量->GC后java堆已使用容量。

 

 

3、内存分配和回收策略

注1:对象的内存分配,主要就是在java堆上分配(也可能经过JIT编译器编译后,被拆分为标量类型,从而分配在栈上)。

注2:对象主要分配在Eden区,如果启动了本地线程分配缓冲,则优先在TLAB分配。少数情况直接在老年代分配。

 

3.1、对象优先在Eden分配

★对象优先分配在Eden,如果Eden内存不足,将触发一次Minor GC。

★区分新生代GC(MinorGC) && 老年代GC(Major GC/Full GC)

  ◆Minor GC:指发生在新生代的GC。因为java对象基本都是朝生夕灭,故Minor GC非常频繁,速度也比较快。

  ◆Major GC:值发生在老年代的GC。出现Major GC,通常伴随一次Minor GC,但非绝对。速度比Minor GC慢10倍以上。

 

3.2、大对象直接进入老年代

★大对象定义:需要大量连续内存的java对象。最典型的就是很长的字符串或者数组。

★这样做的原因:经常出现大对象容易导致内存还有不少空间就提前触发GC以获取更大的连续空间来存储新对象。

★这样做的目的:避免在Eden区及两个Suivivor区之间发生大量的内存复制。

 

3.3、长期存活的对象将进入老年代

★JVM为每个对象都设置一个对象年龄计数器。

★年龄增长机制:如果对象生存在Eden区并经过第一次GC后仍存活,并且能被Survivor接受,将被移动到Survivor空间,并且年龄设置为1。以后在该区域每经历过一次Minor GC,年龄就增长1岁,到15岁后(默认),将会被晋升为老年代。

 

 

3.4、动态对象年龄判定

★目的:为更好的适应不同程序的内存情况,JVM并不是用于要求对象从1岁逐岁增长到15才能进入老年代。

★实现原理:如果在Survivor区中,相同年龄的所有对象大小总和大于其空间大小的一半。则年龄大于或等于该年龄的对象可以直接进入老年代。

 

 

3.5、空间分配担保

★工作机制:在发生一次MinorGC之前,JVM会检查老年代最大连续可以空间是否大于新生代对象总空间:

如果大于,则此次GC是安全的;

如果小于,JVM会查看HandlePromotionFailure设置值是否允许失败:

    如果允许担保失败:

        JVM会继续检查老年代最大连续内存大小是否大于历次晋升到老年代对象的平均大小:

如果大于,就进行一次Minor GC,虽然这样做是有风险的。

如果小于,就进行一次Major GC。

★冒险

  ◆实质:老年代接纳Survivor容纳不下的复制过来的存活下来的新生对象。

  ◆为什么说是冒险:因为新生代存活下来,进行Minor GC时,是将其从Eden区复制到Survivor区。考虑极端情况,当Eden区所有对象都存活下来了,那么就需要将所有的对象复制到未被使用的那个Survivor区。显然此时Survivor是装不下这么多对象的。此时装不下的对象就要被放到老年代当中。也就是老年代为其做了担保。
原创粉丝点击