第一次看这本书,以前写程序的时候一直没怎么接触过JVM虚拟机,只有一些比较笼统的概念,总觉得虚拟机什么的挺高大上的,也挺好奇的,本着见见世面的心思,打算花几天功夫来探一探庐山真面目
有些看不懂的就直接略过了,先理一下脉络吧,不求一次能理解,希望读了这本书起码以后被别人问到不会眼前一抹黑吧
我也只能算上一个小白,这篇文章也只能算是把我能理解的尽量写的明白,算是个人笔记.有些理解不到位的地方希望大佬能够指正
第3章 垃圾收集器与内存分配策略
本章引文:Java和C++之间有一堵由内存分配和垃圾收集技术所围成的”高墙”,墙外的人向进去,墙里面的人想出来
概述
说起垃圾收集,主要就是思考GC需要完成的3件事情:
1. Which:哪些资源要回收
2. When:什么时候回收?
3. How:如何回收?
引用计数法
这种算法基本思路为:给对象添加引用计数器,当被一个地方引用,计数值+1,失效则-1,当计数值为0时则代表对象不可能再被使用
这种方法实现简单,效率高,但是主流Java虚拟机没有选用,主要就是因为它很难解决对象之间相互引用的问题,它们互相引用,导致计数算法无法让GC回收它们
可达性分析算法
这种算法基本思路为:以称为”GC Roots”的对象作为起始点,往下搜索,走过的路径为引用链,当一个对象没有被任何的引用链相连,则证明对象不可用
作为GC Roots的对象包含下面几种:
1. 虚拟机栈中的引用对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI(Native方法)引用的对象
再谈引用(引用的定义和细分)
1. 以上两种方法判定对象存活都与引用有关,而引用在Java中的定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用
但这种定义过于狭隘,对于”鸡肋”这种的对象就显得无能为力,我们希望能描述这样一类对象:内存够时留着,内存在垃圾收集后还是非常紧张则抛弃
2. Java在1.2以后堆引用的概念进行扩充,分为:
1) 强引用:类似”Object obj = new Object()”这类,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
2) 软引用:还有用非必须的对象,在系统将要发生内存溢出异常钱,会把这类对象列入回收范围进行二次回收
3) 弱引用:非必须且强度比软引用低,这类对象只能生存到下次垃圾回收发生前,无论内存是否充足,都会回收弱引用关联对象
4) 虚引用:它是最弱的引用关系
生存还是死亡(二次标记)
1. 引用可达算法中不可达对象不是非死不可,要死亡则需要两个被标记的过程
进行可达分析发现没有相连引用链,则第一次被标记
2. 被标记后进行筛选,条件是是否有必要执行finalize方法,如果被判定有必要,则被放入F-Queue队列,稍后由虚拟机自建的低优先级Finalizer线程执行,稍后GC将对F-Queue队列中对象进行第二次小规模标记,如果对象重新与引用链上的任何对象关联,则获救,如果没有则基本就会被回收
回收方法区(回收的内容)
很多人认为方法区没有或不需要垃圾收集,因为在方法区回收垃圾性价比极低
方法区(永久代)主要回收2类:
1. 废弃常量:例如字符串”abc”已经进入常量池,但没有一个String对象引用这个”abc”,也没有其他地方引用了,这时发生内存回收且必要的话,则”abc”会被清理出去
2. 无用的类有以下条件:
1) 该类所有实例被回收,Java堆不存在该类实例
2) 加载该类的ClassLoader已被回收
3) 该类对应的对象没有被引用也无法通过通过反射访问该类
但和对象不同,不是不使用了就一定回收
垃圾收集算法
本节只是介绍几种算法的思想和发展过程
标记-清除算法
该算法分为”标记”和”清除”两部分,是最基础的方法,后续的收集算法都算是基于这种算法的改进,标记过程见生存还是死亡(对象标记的判定)小节
该算法有两点不足:
1) 效率问题:清楚和标记的效率都不高
2) 空间问题:标记清楚之后会产生大量不连续内存碎片,导致分配大对象时无法找到连续内存提前出发另一次的垃圾收集动作
复制算法
为了解决效率问题,复制算法出现了.将可用内存按容量分成大小相等两块,每次只用一块,比如先用左半块,左半块用完了,就把存活的对象复制到右半块,然后将左半块使用过的内存空间一次清除
优点:内存分配不用考虑内存碎片等复杂情况,实现简单,运行效率高
确定:将内存缩小为原来的一半,代价太高
现在商业虚拟机都采用这种收集算法回收新生代,IBM研究表明,新生代对象98%都是朝生夕死的,所以不用1:1划分内存,而是分成一块较大的Eden和两块较小Suvivor空间
标记-整理算法
标记过程和”标记-清除”算法一样,但后续步骤不是直接堆课回收对象进行清理,而是让所有存活对象往一段移动,然后直接清理端边界以外的内存
分代收集算法
当前商业虚拟机都采用”分代收集”算法,只是根据对象的存活周期将内存划分几块,一般分为新生和老年代,然后根据各个年代的特点采用适当算法:
1) 新生代采用复制算法,只要少量存活对象的复制成本即可完成收集
2) 老年代采用”标记-清理”或”标记-整理”算法回收
HotSpot的算法实现
枚举根节点
l 现在很多应用仅方法区就有数百兆,如果使用引用可达法则要消耗很多时间
l 可达性分析工作必须在一个确保一致性的快照进行(可以理解为时间冻结),导致GC进行时必须停顿所有Java执行线程
l 当执行系统停顿下来后,并不是一个不漏检查所有引用位置,在HotSpot虚拟机使用一组OopMap来得知哪些地方存着引用
安全点
HotSpot在OopMap的协助下可以快速完成GC Roots枚举,但如果每一条指令都生成OopMap,则空间成本变得很高
实际HotSpot没有为每条指令生成OopMap,而是在特定”安全点”(Safepoint)记录这些信息
对于安全点,需要在GC发生时让所有线程跑到最近的安全点停下,有两种方案:
1) 抢先式中断:GC发生时终端所有线程,如果有线程没到安全点就中断,则恢复让其跑到安全点为止,几乎没有虚拟机采用这种方式
2) 主动式中断:GC需要中断线程时,仅设置一个标志,线程执行时轮询标志,发现为真时就把自己中断挂起,轮询标志的地方和安全点是重合的
垃圾收集器
Serial收集器
最基本的收集器,单线程收集,这里的单线程指的是使用一个CPU或一条线程区收集,且暂停其他所有工作线程,直到收集结束,相当于你的妈妈打扫房间时你不能动,直到打扫结束
依然是Client模式下的默认新生代收集器,简单高效,在桌面应用中,分配给虚拟机的内存不会很大,停顿时间可以控制很短
ParNew收集器
Serial的多线程版,Server模式下的首选新生代收集器,可以和CMS收集器配合工作
Parallel Scavenge收集器
新生代收集器,采用复制算法,并行多线程
专注达到一个可控制的吞吐量,吞吐量=CPU用于运行用户代码的时间:CPU总消耗时间
Serial Old收集器
Serial老年代版本,单线程,使用”标记-整理”
Parallel Old收集器
Parallel Scavenge老年代版本,采用”标记-整理”
CMS收集器
获取最短回收停顿时间的收集器,应用于互联网站或B/S系统服务端,重视响应速度
优点:并发收集,低停顿
缺点:1.占用一部分分线程导致程序变慢 2.无法处理浮动垃圾 3.因为基于”标记-清除”所以结束时会有空间碎片
G1收集器
技术发展最前沿成果
面向服务端应用的垃圾收集器
特点:1.并行域并发 2.分代收集 3.空间整合 4.可预测的停顿