《深入理解Java虚拟机》第二版 第三章笔记
目录
三.垃圾收集器与内存分配策略
1.1 概述
1.2 对象已死吗?
1.2.1 引用计数法(RC Reference Counting)
1.2.2 可达性分析算法
1.2.3 引用
1.2.4 生存还是死亡
1.2.5 回收方法区
1.3 垃圾收集算法
1.3.1 标记 - 清除算法
1.3.2 复制算法
1.3.3 标记 - 整理算法
1.4 HotSpot的算法实现
1.4.1 枚举根节点
1.4.2 安全点
1.4.2 安全区域
1.5 垃圾收集器
1.5.1 Serial收集器
1.5.2 ParNew收集器
1.5.3 Parallel Scavenge 收集器
三.垃圾收集器与内存分配策略
1.1 概述
抛出问题
- 哪些内存需要回收
- 什么时候回收
- 如何回收
1.2 对象已死吗?
如何判断对象已经死亡:
1.2.1 引用计数法(RC Reference Counting)
原理:每个对象都有一个引用计数器,每当其他地方引用它时,计数器加一。引用失效时,计数器减一。
当计数器减到0,表示对象不再被引用。
缺点:当两个对象互相引用时,那么他们永远不会被GC回收。
优点:实现简单,判定效率高,但是主流的JVM中没有选用RC的算法
1.2.2 可达性分析算法
原理:一系列被称为GC Roots的对象作为起始点,当一个对象没有任何引用链连接GC Roots时,证明此对象不可用。
可以当做GC Roots对象的有:
- 虚拟机栈(栈帧的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般指Native方法)引用的对象。
-—————— 最后结束修改于2019年01月 15日 22:18 待续… ——————-
1.2.3 引用
定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称为这块内存代表一个引用。
JDK1.2扩充:
将引用分为了 强引用,软引用,弱引用,虚引用。强度依次减弱。
强引用:
Object obj = new Object();
类似这种引用,只要强引用一直存在,垃圾收集器永远不会回收被引用的对象。
软引用:
软引用被用来描述还有用但是又非必须的对象,在系统将要发生内存溢出时,将会把这些对象列入回收范围。
弱引用:
被软引用指向的对象,不论下一次垃圾回收器收集时,内存足够与否,都会被回收。
虚引用:
最弱的一种引用,此引用不会对对象何时被垃圾回收影响,唯一的作用是被此引用指向的对象只会在被垃圾回收的时候返回一个通知。
1.2.4 生存还是死亡
即使可达性分析算法中不可达的对象,也并非一定会回收,真正宣告一个对象死亡至少需要经过两次标记。
第一次标记:
- 可达性分析不可达后,会被标记一次,并且经过一次塞选。
塞选条件是:是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法,或finalize()已经被虚拟机调用过时,没有必要执行finalize()方法
- 如果对象被判定需要执行finalize方法,对象就会被放入一个F-Queue的队列中。
- 稍后会有由虚拟机自动创建的,低优先级的Finalizer线程去执行它。
- 执行意味着这个线程会去触发finalize方法,但线程并不保证等待finalize()方法运行结束,为了避免执行对象的finalize()方法时出现运行缓慢或死循环,导致队列中其他的对象处于永久等待,导致回收系统崩溃。
第二次标记:
- finalize方法是对象逃脱被清理的最后一次机会。
- 如果在第二次标记之前,队列中的对象没有与引用链发生关系,或没有与任何一个对象发送关联。
- 那么第二次标记之时,它将被移出队列,并被回收。
补充:任何对象的finalize方法只会被系统自动调用一次。
1.2.5 回收方法区
方法区,也被HotSpot虚拟机中的GC机制形容为永久代。我认为应该是垃圾回收效率常常很低,因此叫永久代。
方法区主要回收两部分内容:
废弃常量和无用的类
废弃常量:
比如当没有任何一个String对象引用指向常量池的字面量时,此字面量在必要的情况下会被回收。
常量池中的其他类,接口,方法,字段的符号引用同理。
无用的类:
当必须同时满足一下三条时,可以算是无用的类。
类的 所有实例都被回收,堆中没有此类的实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
补充:
在大量使用反射,动态代理,CGLib···频繁自定义ClassLoader的场景都要求虚拟机具备类卸载的功能,以保证永久代不会溢出。
1.3 垃圾收集算法
先想象一个网格,网格中随机出现需要收集的垃圾对象。
1.3.1 标记 - 清除算法
原理:首先标记出所有需要回收的对象,然后统一清楚被标记的对象。
优点:最基础的垃圾回收算法,有啥子优点?
缺点:标记与清除两种操作效率都不高,回收后产生大量空间碎片。当分配大对象时,没有足够的内存,会触发另一次的垃圾回收。
1.3.2 复制算法
原理:通过将内存分为两块,每次只使用一块,当这一块内存使用完之后,会把还存活的对象搬到另一块内存,然后一次清空之前使用的那块内存。
优点:不用再考虑空间碎片的情况。另一半分配内存时,只需顺序分配内存,实现简单,运行高效。
缺点:只使用了一半的内存。
1.3.3 标记 - 整理算法
原理:标记过程,与标记-清除算法一样,但是之后做的是,把所有存活的对象都向一端移动,之后清楚掉边界以外的内存。意思就是把有用的对象放在一堆,然后直接清楚掉堆外的内存。
优点:没有了空间碎片 也没有浪费内存(人类总是这么喜欢追求极致不是?)
-—————— 最后结束修改于2019年01月 23日 16:41 待续… ——————-
1.4 HotSpot的算法实现
1.4.1 枚举根节点
什么叫枚举根节点呢?顾名思义,把根节点枚举出来。这样可以减少对根节点的判定!
在目前主流的Java虚拟机中,例如HotSpot中虚拟机采用OopMap这种数据结构来知道哪些地方存放这对象的引用,在类加载完成之后,Hotspot把对象内什么偏移量上是什么数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样在GC扫描时可以直接遍历map得知哪些引用还有效。完成GC Roots的枚举。
PS:枚举出根节点干嘛呢?因为GC Roots判断对象已死?需要用到根节点,然而这些根节点的选用,就需要被枚举出来。而不是每次读取判断哪些是roots(可以复习一下可达性分析算法)
1.4.2 安全点
安全点为的是保证在进行GC时,线程需要运行到一个安全点才能进行GC,这里我的理解是,随时停止一个线程的执行是有风险的,因此在停止一个或暂停一个线程时,需要设置一个安全点。
因此GC时需要所有的线程都运行到安全点。
如何实现呢?
一种是 抢先式中断:
GC时,首先中断所有线程,当有线程未到安全点时,唤醒响应线程,使之运行到安全点。
一种是 主动式中断:
不对线程做操作,仅设置一个标志,让各个线程执行时主动去轮询这个标志,标志为真时,自己就挂起,轮询标志的地方与安全点重合。
轮询标志实现原理:
虚拟机会把线程中执行的某个指令的内存页设置为不可读,当线程执行到此指令时,会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样实现一条汇编指令完成安全点轮询和触发线程中断。
1.4.2 安全区域
安全点有一个问题,当线程未被分配cpu时间片时**,**此线程无法响应JVM的中断请求,运行到安全点去中断挂起。
此时需要安全区域来解决此问题。
安全区域指的是,在某代码片段中,引用关系不会发生变化,因此在任何地方开始GC都是安全的。
实现原理:
当线程执行到Safe Region中的代码时,首先标识自己已经进入安全区域,因此JVM要GC时,就不需要考虑这些标识自己进入安全区域的线程了,当线程需要离开安全区域时,它需要检查系统是否已经完成了根节点的枚举或者GC的整个过程,如果完成,那么线程继续执行,否者继续等待收到可以安全离开安全区的信号为止。
1.5 垃圾收集器
PS:这里讨论JDK1.7之后HotSpot虚拟机的垃圾收集器
如图所示:
新生代使用的垃圾收集器有:Serial 收集器,ParNew 收集器,Parallel Scavenge 收集器,G1收集器
老年代使用的垃圾收集器有:CMS收集器,Serial Old 收集器,Parallel old 收集器,G1 收集器
互相连线的表示可以搭配使用。具体情况和场景有不同的搭配。
1.5.1 Serial收集器
特点:
- 最基本,发展最悠久的收集器。
- 采用 复制算法 的 单线程 新生代 收集器。
- JDK1.3.1之前是新生代唯一的选择。
- 单线程收集器。不仅说明只使用一个cpu或一条收集线程进行垃圾回收时,会暂停其他所有的工作线程。Stop The World
- 目前依旧是Client模式下,默认的新生代收集器。
- 简单高效,对于限定单个cpu的环境来说,Serial收集器无线程开销。专心做垃圾收集可以获得最高的单线程收集效率。
1.5.2 ParNew收集器
特点:
- 运行在Server模式下首选的新生代收集器。
- Serial的多线程版本。
- 在控制参数,对象分配规则,回收策略等都与Serial收集器完全一样。
- 目前只有它能与CMS收集器配合工作(CMS,并发的标记清除收集器,后续有介绍,一款真正做到垃圾收集线程与用户线程同时工作的收集器)。
1.5.3 Parallel Scavenge 收集器
特点:
- 采用 复制算法 的 并行 新生代 收集器。
- 收集器的关注点在于达到一个可控制的吞吐量。(Throughput)
吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾回收时间)
停顿时间越短,越适合做交互类的程序,提高响应速度,提升用户体验,高吞吐量可以提高CPU利用时间,主要适合低交互的任务
- -XX:MaxGCPauseMilis 控制最大垃圾收集停顿时间 (一个大于0的毫秒数)时间设小后,系统是根据缩小新生代内存空间,来达到加快收集速度减少停顿时间的!
- -XX:GCTimeRatio 控制吞吐量大小(一个0到小于100的整数)当设置为5时,表示GC时间占总时间的1/(1+5),就是六分一的时间
- 也常被称为“吞吐量优先”收集器。
- -XX:+UseAdaptiveSizePolicy 当此参数打开时,就不需要手工指定新生代的大小,以及Eden与Survivor的比例(-XX:SurvivorRatio),晋升老年代年龄(-XX:PretenureSizeThreshold)等参数了,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间以及吞吐量。这种方式称为GC的自适应调节策略
- 自适应调节策略也是它与ParNew收集器重要的区别
1.5.4 Serial Old 收集器
特点:
- 它是Serial收集器的老年代版本。
- 使用多线程的标记整理算法。
- 在Client模式下的虚拟机使用。
- 在Server模式下时,它还有两大用途,一是在JDK1.5及之前与Parallel Scavenge收集器搭配使用,另一种作为CMS收集器的后备预案(在出现Concurrent Mode Failure时使用,就是在并发模式失败下启用单线程模式,Serial Old收集器启用)
1.5.5 Parallel Old 收集器
特点:
- 它是Parallel Scavenge收集器的老年代版本。
- 使用多线程标记整理算法。
- JDK1.6后提供,在JDK1.6之前,新生代使用Parallel Scavenge后,老年代必须使用Serial Old。因此单线程的老年代回收在服务器的多核心的cpu资源上出现了浪费,这种组合不如ParNew 与 CMS组合给力。
- 在注重吞吐量与CPU资源敏感的场景,优先考虑Parallel Scavenge与Parallel Old 组合。
1.5.6 CMS收集器(Concurrent Mark Sweep)
特点:
- 采用并发的标记清除算法。
- 一款以最短回收停顿时间的为目标的收集器,高响应的B/S系统宜采用。
- 标记清除分为四步:
初始标记:需要Stop The World,标记GC roots 能关联到的对象。速度很快。
并发标记:需要Stop The World,进行GC Roots Tracing。一个根节点遍历的过程。
重新标记:为了修正并发阶段用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。此过程花费时间比初始标记阶段稍长,远比并发标记时间短。
并发清除:垃圾清理线程开始清除无用对象。
- 有三个明显的缺点:
1.对cpu资源非常的敏感。
- CMS默认启动的回收线程数为(CPU数量+3)/ 4。意味着并发回收时,垃圾收集线程不少于25%的CPU资源。CPU数越多,占比越少,但当CPU数不足4个时,CMS对用户程序的影响可能变得很大。垃圾回收的线程占比越大,对用户线程的影响越大。
- 因此JVM提供了一种称为“增量式并发收集器” i-CMS CMS的变种。
原理:
与单CPU年代PC机操作系统使用的抢占式来模拟多任务机制的思想一样,就是在并发标记,清理的时候让GC线程,用户线程交替执行,减少GC线程独占系统资源的时间,整个垃圾收集过程会被拉长,但是减小对用户线程的影响,有点像当下分期购物一样。降低大出血的购物对当下的生活水平的影响。
实践证明 i-CMS 效果很一般。分期购物并不能减少还款的金额对吧。
2.CMS无法处理浮动的垃圾(Floating Garbage)。
- 因为用户线程与垃圾回收线程并行执行,因此用户线程必然会在标记之后产生垃圾,这些垃圾只能等待下一次GC时再清理,这些垃圾被称为浮动垃圾。
- 也因为有用户线程在执行,因此需要预留一些内存空间给用户线程使用,因此CMS不能等到老年代几乎被填满再进行垃圾回收,填满了用户线程就没有内存使用了。
- 在JDK1.5的时候,老年代使用了68%的空间就会被激活,这是一个偏保守的设置。
- 当老年代增长不是很快的时候,可以适当调高触发百分比:-XX:CMSInitiatingOccupancyFraction
- JDK1.6时,阈值被提升到92%。
如果运行期间预留的内存无法满足用户线程的需要,那么就会出现Concurrent Mode Failure,此时采用Serial Old收集器来进行老年代的垃圾收集工作。这样停顿时间就会很长了。因此 -XX:CMSInitiatingOccupancyFraction 参数并不是越高越好,越高会导致大量的 Concurrent Mode Failure
3.既然是标记-清楚算法,那么就会有大量的碎片,导致分配大对象时如果没有足够的连续空间。会导致一次FullGC。
为了解决此问题,CMS提供了-XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于将要进行FullGC之前,进行一次内存碎片整理工作。此过程无法并发,因此停顿时间会变长。
- 虚拟机还提供了另外一个参数:-XX:CMSFullGCsBeforeCompaction 。用于设置执行多少不压缩的FullGC后,运行一次带压缩的FullGC:FullGC之前,进行一次内存碎片整理工作。
1.5.7 G1 收集器(Garbage-First)
特点:
- 它是当前收集器技术发展最前沿的成果之一
- JDK1.7立项
- 一款面向服务端应用的垃圾收集器,目标为替换掉JDK1.5中的CMS
- 与其它收集器相比G1具备如下特点:
并行与并发:
- 充分利用多cpu,多核环境,来缩短STW的停顿时间。
- 其它收集器需要停顿的,G1依旧可以并发的使Java程序继续执行。
分代收集:
- G1可以独立的管理整个GC堆,但它能采用不同的方式处理新创建的对象和已经经过多次GC的旧对象。以获得更好的收集效果。
空间整合:
- G1与CMS的标记-清除不同,G1更像是标记-整理。从局部上(两个region)看又像是复制算法。
- 因此这两种算法都不会在G1运行期间产生内存空间碎片。收集之后都有规整的可用内存已被大对象使用
可预测的停顿:
- 这是与CMS相比更大的优势,降低停顿时间是G1与CMS共同的目标。
- G1还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时JAVA的垃圾收集器的特征。
- G1不在将 新生代和老年代物理隔离,而是把他们都划分为一个个的不需要连续大小相等的Region集合。
- 避免在java堆中进行全区域的垃圾收集,而是跟踪各个region,看各自region垃圾回收的价值。(回收所得空间,和所花费的时间)在后台维护一个优先列表。每次根据回收价值更大的region有限回收。这种方式极大的提高了在有限的时间内,获取尽可能高的收集效率。
- 一个对象被分配在一个region中时,它并非只能被本region的其他对象引用,它能被Java堆的任意对象发生引用关系,因此做可达性分析的时候,有可能会扫描整个堆。因此G1采用Remembered Set 来避免全堆扫描。
Remember Set:G1中,每个Region都有一个与之对应的Remember Set,虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象)
然后通过CardTable把相关引用信息记录到被引用对象所属的Region的Remember Set之中。
当进行内存回收时,做可达性分析时,在GC根节点的枚举范围中加入Remember Set即可保证不对全堆扫描也不会有遗漏。
PS:我对于他的理解就像数据库建立索引一般,把索引放入Set中,枚举的时候在枚举中加入Set中的引用链,从而避免全堆扫描。
- G1收集器的运作大致可划分为以下几个步骤:(可以对比CMS收集器)
初始标记:
标记一下GC roots能直接关联到的对象。需要停顿线程,耗时短。
并发标记:
开始从GC root中对堆中的对象进行可达性分析。可与用户线程并发执行,耗时长。
最终标记:
为了修正 在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。最后把Logs里的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。
筛选回收:
最后对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
还没有评论,来说两句吧...