深入理解java虚拟机精华总结:如何判断对象是否可回收、引用、finalize、方法区回收、垃圾收集算法、垃圾收集器、内存分配与回收策略
- 如何判断对象是否可回收
- 引用计数
- 可达性分析法
- 引用
- finalize
- 方法区回收
- 垃圾收集算法
- 标记-清除算法
- 标记-复制算法
- 标记-整理算法
- 垃圾收集器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- Garbage First收集器
- 内存分配与回收策略
往期内容:
- 深入理解java虚拟机精华总结:jvm内存模型(运行时数据区域)、对象、OOM异常
如何判断对象是否可回收
两大算法:引用计数法,可达性分析法。
引用计数
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
优点:原理简单,效率高。
缺点:无法解决对象间的循环引用的问题。
对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
可达性分析法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象
- 在方法区中类静态属性引用的对象
- 在方法区中常量引用的对象
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
- 被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
引用
四种引用类型:强引用、软引用、弱引用、虚引用。
- 强引用:普通引用,被引用对象不会被垃圾收集器回收
- 软引用:SoftReference类,内存充足时不会被回收,内存不足时会被回收
- 弱引用:WeakReference类,只能生存到下一次垃圾收集发生前,内存是否充足都会回收
- 虚引用:PhantomReference类,无法通过虚引用来取得一个对象实例,唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
finalize
要判断一个对象死亡,要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如果要执行finalize()方法,则会被放入一个F-Queue队列,随后由Finalizer线程执行finalize()方法。如果该对象重新被GC Roots引用链上的某个对象引用,则不会被回收,否则会被进行第二次标记,放入回收集合,然后被回收。
方法区回收
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
废弃的常量:常量池中没有任何引用指向的常量。
不再被使用的类,三个条件都必须满足:
- 该类所有的实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
垃圾收集算法
标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
- 标记和清除两个过程的执行效率都随对象数量增长而降低
- 内存空间的碎片化
标记-复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着
的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:空间浪费。
标记-整理算法
让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
垃圾收集器
Serial收集器
新生代垃圾收集器,单线程,基于标记-复制算法,进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW)。
ParNew收集器
是Serial收集器的多线程并行版本。
在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后的默认新生代收集器。
Parallel Scavenge收集器
新生代收集器,基于标记-复制算法,多线程收集器,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
Serial Old收集器
Serial收集器的老年代版本,单线程,使用标记-整理算法。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,多线程并发收集,基于标记-整理算法。
CMS收集器
以获取最短回收停顿时间为目标的收集器,基于标记-清除算法。
整个过程分为四个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
缺点:
- 无法处理“浮动垃圾”,有可能出现“并发失败”,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
- 基于“标记-清除”算法,会有大量空间碎片产生。
Garbage First收集器
面向局部收集,和基于Region的内存布局,JDK 9的默认垃圾收集器,支持指定停顿时间(消耗在垃圾收集上的时间不超过指定的停顿时间)。
连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。还有一类特殊的Humongous区域,专门用来存储大对象(超过了一个 Region容量一半)。Region是单次垃圾回收的最小单元。
G1收集器跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。
G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
内存分配与回收策略
堆内存划分:
一共五个内存分配原则:
- 对象首先在Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,会把Eden区和Survivor1区中的存活对象,挪到Survivor2区,然后Survivor1区和Survivor2区会互换。
- 大对象直接进入老年代,-XX:PretenureSizeThreshold参数指定大于该设置值的对象直接在老年代分配。
- 长期存活的对象将进入老年代,在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
- 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
- 空间分配担保机制:在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。如果出现了担保失败,重新发起一次Full GC。