您的位置:首页 > 其它

深入理解JVM—第三章:垃圾收集器与内存分配策略

2018-04-08 20:41 519 查看

概述

对于Java内存运行时区域的各位部分,其中程序计数器、虚拟机栈、本地方法栈这三个区域都是随线程而生,随线程而灭。并且栈帧中分配的内存也是在编译后就已知的。因此这几个区域的内存分配和回收都具备确定性,所以我们在这几个区域就不必过多地考虑回收问题。

而Java堆和方法区中的内存分配是动态的,要在运行期才知道内存的实际分配情况,所以垃圾收集器所关注的是Java堆和方法区中的内存。

哪些内存需要被回收?答:Java堆和方法区中的垃圾内存

1,JVM的垃圾判断方法:

①引用计数算法:给每个对象加一个引用计数器,有地方引用了这个对象,则该引用计数器加1,当引用失效了,引用计数器减1,最后当引用计数器为0的时候,那就说明该对象不被引用了。

该方法实现简单、判断效率高,但很难解决循环引用的问题。所以不被主流的Java虚拟机所使用。

②可达性分析算法:从GC Roots出发的引用链到某对象不可达,则该对象可被回收。

GC Roots的对象包括下面几种:
a,虚拟机栈(局部变量表)中引用的对象
b,方法区中类静态属性引用的对象
c,方法区中常量引用的对象
d,本地方法栈JNI(即Native方法)引用的对象

对于可达性分析算法中不可达的对象,也并非“非死不可”,他们只是处于“缓刑”状态,要宣告一个对象的死亡,至少需要经历两次标记过程:若对象在可达性分析算法中发现没有与GC Roots相连接的引用链,那它将会第一次标记并且进行一次筛选,筛选的条件为此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,对象会被直接回收。若对象可以被虚拟机执行finalize()方法,则看它调用之后,能够“自救”,“自救”则复活,不“自救”则被回收。

2,引用的扩充
①强引用:只要强引用还在,对象就永远不会被回收
②软引用:描述有用但并非必须的对象,这些对象在内存将要溢出前,才会对其进行回收
③弱引用:描述非必须的对象,无论内存是否足够,对象都一定会被回收
④虚引用:其唯一目的就是在对象被回收的时候,可以收到一个系统通知

3,方法区的垃圾回收

方法区的回收效率低,主要是回收废弃常量和无用的类

回收废弃常量是指回收那些被没有其他地方引用的字面量或许符号引用

回收无用的类比较麻烦,无用的类要符合以下三个条件:
①该类的所有的实例都已经被回收,即java堆中不存在该类的任何实例
②加载该类的ClassLoader(类加载器)已经被回收
③该类对应的java.lang.Class(字节码)对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法

垃圾内存如何回收?答:有相应的垃圾回收算法对不同的内存区域进行回收。

1,标记-清除算法:用于回收老年代(经历好几次回收仍存活或特别大的对象)。先标记要被回收的对象,标记完成后,统一回收所有被标记的对象

2,复制算法:用于回收新生代(很快被回收或不是特别大的对象)。将可用的内存分成两半,每次仅用一半,当这一半用完,将存活的对象复制到另一半上面去,最后对使用完的空间进行清理。

新生代中的内存实际分成一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。当回收的时候,将Eden和Survivor中存活的对象一次性复制到另外一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间。HotSpot虚拟机中默认Eden和survivor的大小比例为8:1。也就是每次新生代可用内存空间为90%。由于无法保证每次回收的时候存活对象不多于10%,所以当Survivor空间不够的时候,需要通过分配担保机制将这些新生代中所有存活对象都存放到老年代中。

3,标记-整理算法:用于回收老年代。对所有可回收对象进行标记,然后让所有存活对象均往一端移动,清楚端边界以外的内存。

4,分代收集算法:

根据对象存活周期不同,将Java堆中的内存分为新生代和老年代。

新生代:采用复制算法。因每次垃圾收集都有大量的对象死去,只有少量存活,仅需付出少量的存活对象复制成本即可完成垃圾收集。

老年代:采用“标记-清理”或“标记-整理”算法。因对象的存活率高,对象所占的内存空间较大,且没有额外的空间给它做分配担保。

谁来执行回收动作?答:垃圾收集器

1,HotSpot的垃圾收集器

2,垃圾收集器分类:

直到现在还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,所以我们选择的只是对具体应用最合适的收集器。

2.1 Serial收集器

作用于新生代,单线程效率高。

到目前为止,Serial收集器依然是Client模式下的默认的新生代垃圾收集器。

2.2 ParNew收集器

Serial收集器的多线程版本。作用于新生代、并行多线程、能与CMS配合使用。

ParNew收集器是许多运行在Server模式下的默认新生代垃圾收集器,为什么不选用Serial作为新生代的收集器呢?主要在于除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。

2.3 Parallel Scavenge收集器

作用于新生代,并行的多线程,关注吞吐量,有GC自适应调节策略。

Parallel Scavenge收集器更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。如虚拟机共运行100分钟,垃圾收集花了1分钟,吞吐量为99%。

虚拟机可根据当前系统的运行情况收集性能监控信息,动态调整三个优化参数以提供最合适的停顿时间或最大吞吐量。该调节方式称为GC自适应的调节策略。

2.4 Serial Old收集器

Serial的老年代版本,单线程,使用“标记-整理”算法。作为CMS收集器的后背预案。

2.5 Parallel Old收集器

用于老年代、并行的多线程、注重吞吐量和CPU资源利用率。
使用“标记-整理”算法,其通常与Parallel Scavenge收集器配合使用,

2.6 CMS收集器

老年代版本,并发低停顿,应用于服务端

基于“标记-清除”算法。

整个执行过程分为以下4个步骤:

初始标记
并发标记
重新标记
并发清除

初始标记和重新标记这两个步骤仍然需要暂停Java执行线程,初始标记只是标记GC Roots能够关联到的对象,并发标记就是执行GC Roots Tracing的过程,而重新标记就是为了修正并发标记期间因用户程序执行而导致标记发生变动使得标记错误的记录。其执行过程如下:

CMS收集器的缺点:
①CMS收集器对CPU资源非常敏感。当CPU资源不足的时候,CMS收集器占CPU的比率大,导致应用程序变慢
②CMS收集器无法处理浮动垃圾。由于该浮动垃圾在标志之后,本次清理无法包括它们,所以要等到下一次CC。
③收集后会产生大量的内存空间碎片,当无法给大对象分配空间,将开启碎片整合,此时无法并发执行,停顿时间更长。

2.7 G1收集器

应用于服务端、可预测、可控的停顿,多线程

G1具备以下特点:
①并行与并发
②分代收集
③空间整合:不会产生内存碎片
④可预测停顿:将整个Java堆划分为多个大小相等的region,每个region有新生代和老年代,后台维护一个优先列表,根据允许的收集时间,优先回收价值最大的region,保证在规定时间内可以获取尽可能高的收集效率。

G1的工作过程如下:

初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)

初始标记阶段仅仅只是标记一下GC Roots能够直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建对象,这个阶段需要暂停线程。并发标记阶段从GC Roots进行可达性分析,找出存活的对象,这个阶段食欲用户线程并发执行的。最终标记阶段则是修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,仍然需要暂停用户线程。最后在筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。整个执行过成功如下:

3,垃圾收集器常用参数总结

在HotSpot中如何实现这些垃圾判定以及回收算法

1,枚举根节点

枚举根节点就是进行可达性分析的过程,从GC Roots节点找引用链。GC Roots的节点主要在全局性的引用(常量或类静态属性),与执行上下文(栈帧中的局部变量表)中。

在进行GC的时候,为了保证对象的引用关系不发生变化,必须停顿所有的Java执行线程。简称Stop the world。因此在枚举根节点的时候必须停顿。

目前的Java虚拟机使用的都是准确式GC,知道内存中什么位置是什么类型的数据。当执行系统停顿下来,并不需要一个不漏地检查完所有的执行上下文跟全局的引用。在HotSpot虚拟机中使用OopMap这种数据结构来保存存活的对象的引用信息。GC在扫描时就可以很快地知道这些信息。

2,安全点

对于序列复用的指令因“长时间执行”,例如方法调用、循环跳转、异常跳转等,具有这些功能的指令才会产生安全点。

程序并非在任何地方都可以停下来开始GC,只有到达安全点(safepoint)才能暂停。也只有在安全点的位置才会产生OopMap。从而使得HotSpot在OopMap协助下,快速完成枚举根节点。

让线程在安全点才停下来的两种方式:
①抢先式中断:先让所有线程全部中断,如果发现有线程中断的位置不在安全点上,就恢复线程,让它“跑”到安全点上。

②主动式中断:当GC需要中断时,仅仅修改中断标志,各个线程在执行时主动去轮询这个标志,发现中断标志为真,则线程自己中断挂起。轮询标记的地方是与安全点重合的,另外创建对象需要分配内存的地方也是与安全点重合的。

3,安全区域

在一段代码片段中,引用关系不发生变化,此区域任何地方都可以开始GC。该区域称为安全区域(safe region)。例如处于sleep状态的线程.

当线程执行到安全区域的代码时,先标记自己进入安全区域,在线程需要离开安全区域的时候,它要检查系统是否已经完成了根节点枚举,从而确认哪些线程占用的内存会被回收(或者整个GC过程)。如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开安全区域的信号为止。

4,上述三部分内容流程为:
安全点(安全区域)—>让所有的线程暂停—>保证对象的引用关系不再发生变化—>产生OopMap—>记录存活对象的引用—>枚举根节点—>开始GC回收动作。

内存分配与回收策略

1,对象优先在Eden分配

大多数情况下,对象在新生代Eden分配空间。当Eden区没有足够的空间进行分配时,虚拟机将发动一次Minor GC(新生代GC)。若存活对象无法进入Survivor,则通过分配担保机制转移到老年代中。

2,大对象直接进入老年代

大对象指需要大量连续内存空间的Java对象。可通过参数PretenureSizeThreshold设置大对象的大小。

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

虚拟机给每个对象定义了一个对象年龄计数器。如果对象出生并经过第一次Minor GC后仍存活,并且能被Survivor容纳,将被移到Survivor空间中,且其对象年龄设为1,对象在Survivor中每熬过一次Minor GC,年龄增加1岁,当它的年龄增加到一定的程度(默认为15岁),将会晋升到老年代中。

4,动态对象年龄判定

Survivor空间中相同年龄的所有对象大小总和,大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年代。

5,空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,若大于,则可以安全进行Minor GC。否则,看虚拟机是否允许担保失败,如果允许,则看老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次Minor GC。如果小于,则不允许冒险,改为进行一次Full GC.

参考《深入理解Java虚拟机》 周志明 著
阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: