《深入理解java虚拟机》的重点记录,方便复习
对象生命周期
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
实现简单,判断效率也很高,但是很难解决对象之间相互循环引用的问题。
可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用的。
4种GC Roots对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
引用的类型
在JDK 1.2之后,java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种。
强引用
值在程序代码之中普遍存在的,类似Object obi = new Object()
这类的应用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
用来描述一些还有用但并非必需的对象。对于软引用的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用
也是用来描述非必需的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存时候足够,都会回收掉只被弱引用关联的对象。
虚引用
也称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在对象被收集器回收时收到一个系统通知。
生存还是死亡
即使在可达性分析中不可达的对象,也不是非死不可的。如果对象要真正死亡,要经历两次标记。如果对象不可达,进行第一个标记并且筛选。筛选的标准是“对象是否有必要执行finalize()方法”。如果对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过(任何一个对象的finalize方法都只会被系统自动调用一次),则视为不需要执行finalize方法。
如果对象被判定为需要执行,则把它放入到一个F-Queue的队列中,稍后由一个虚拟机自己建立的Finalizer()线程执行。 finalize()方法是对象逃离死亡的最后一次机会,稍后GC将会对F-Queue队列中的对象第二次标记,如果对象在finalize()方法中拯救自己——对象与引用链上的任何一个对象关联起来,比如对象将自己赋值给类变量或者给一个对象的成员变量;那么此对象将会被移除队列不会被回收。
建议尽量避免使用finalize()方法,因为它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。“关闭外部资源”之类的工作,使用try-finally或者其他方式都可以做得更好。更及时。
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
回收废弃常量和回收堆中的对象非常类似。
无用的类需要满足以下3个条件:
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类
无用的类并不是一定要被回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。
垃圾收集算法
标记清除算法
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
2个缺点:
- 效率问题,标记和清除的效率都不高
- 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前另一次垃圾收集动作。
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。
优点:不会出现内存碎片,按顺序分配内存即可,实现简单,运行高效。
缺点:有一半的内存不可用
回收新生代
采用复制算法回收新生代
新生代中的对象98%是“朝生夕死”的,并不需要按照1:1的比例来划分内存空间,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清洗掉Eden和刚才用过的Survivor空间。当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
标记整理算法
首先标记出所有需要回收的对象,之后所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会出现内存碎片,也不会浪费内存,适用于存活率较高的对象,一般用在回收老年代
分代收集算法
把java堆分为新生代和老年代,回收新生代采用复制算法,回收老年代使用标记整理算法。
垃圾收集器
Serial 收集器
- 最基本、发展历史最悠久的新生代收集器,使用复制算法。
- 单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
- 虚拟机运行在Client模式下的默认新生代收集器。
ParNew 收集器
- 其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集,其余行为与Serial收集器完全一样。
- 许多运行在Server模式下的虚拟机中首选的新生代收集器。除了Serial 收集器外,目前只有ParNew 收集器能与CMS 收集器配合工作。
- ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
Parallel Scavenge 收集器
- 并行的多线程新生代收集器,使用复制算法。
- Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old 收集器
- Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。
- 主要给Client模式下的虚拟机使用
- 在Server模式,主要有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge 收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old 收集器
- Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。
- 从JDK 1.6中才开始提供,在这之前,Parallel Scavenge只能搭配Serial Old 收集器使用(效果不好)。
CMS 收集器
- CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务器端上,这类应用尤其重视服务的相应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
- HotSpot 虚拟机中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 基于“标记-清除”算法,整个过程分为4个步骤:初始标记、并发标记、重新标记和并发清除。
- 初始标记、重新标记仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 由于整个过程汇总耗时最长的标并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
- 3个明显的缺点:
- 对CPU资源非常敏感。
- 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
- 由于基于“整理-清除”算法实现,会产生大量空间碎片,给大对象分配带来很大的麻烦。
并发与并行
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
G1 收集器
- 当今收集器技术发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器。
- 特点:并行与并发、分代收集、空间整合、可预测的停顿。
- G1 收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
内存分配与回收策略
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在 Eden 区以及两个 Survivor 区之间发生大量的内存复制。
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被Survivor容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间时候大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
下面解释一下“冒险”是冒了什么风险,新生代使用复制收集算法,但为了内存利用率,只使用其中一个 Survivor 空间来作为轮换备份,因此当出现大量对象在 Minor GC 后仍然存活的情况,就需要老年代进行分配担保 ,把 Survivor 无法容纳的对象直接进入老年代,但是分配担保的前提是,老年代本身还有容纳这些对象的剩余空间。而一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多的空间。