在面试中通常会问到关于Java虚拟机的内容,重点在类机载机制、运行时数据区、GC垃圾回收,JVM性能调优
等;
有关于运行时数据区的整理部分可阅读
Java虚拟机之内存结构
本文主要是对GC垃圾回收相关知识点进行的整理。
哪些对象可以被回收
-
引用计数器法
堆内每个对象实例都有一个引用计数器;每当一个地方引用它,计数器就加1;当引用失效,计数器就减1;
任何时候计数器为0的对象就是不可能再被使用的
,但是目前主流的虚拟机并没有选择这个算法来管理内存,原因是不能解决对象之间相互循环引用的问题
相互循环引用的对方,它们的引用计数器都不为0,,也就不会被回收。 -
可达性分析法
通过一系列的称为“GC ROOT”的对象为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC ROOT没有任何引用链相连的话,则证明此对象是不可用的;
可以用作GC ROOT的对象包括: a) 虚拟机栈中引用的对象(栈帧中的本地变量表); b) 方法区中类静态属性引用的对象; c) 方法区中常量引用的对象; d) 本地方法栈中JNI(Native方法)引用的对象。
无论是引用计数器还是可达性分析,判断对象是否存活都与“引用”有关系且与强引用
有关系。在Java中,将引用可分为强引用、软引用、弱引用、虚引用。
- 强引用:如果一个对象具有强引用,
垃圾回收器绝不会回收它
。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError,使程序异常终止; - 软引用:
如果内存空间够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存
,只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存。 - 弱引用:比软引用更弱一些,
弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
。 - 虚引用:
在任何时候都可能被垃圾回收器回收,主要用于跟踪对象被垃圾回收器回收的活动
软
引用、弱
引用都在引用对象被垃圾回收之后
,JVM才把引用加入到与之关联的引用队列中;而虚
引用对象在垃圾回收之前
,JVM把引用加入到阈值关联的引用队列中。虚引用必须和引用队列联合使用
,当垃圾回收器准备回收一个对象时,若发现它还有虚引用
,就会在回收对象的内存之前
,把这个虚引用加入到与之关联的引用队列中。这样就可以通过判断引用队列中是否已经加入了虚引用来了解被引用对象是否将要被垃圾回收器回收
对象回收前的最后挣扎
即使在可达性分析算法中不可达的对象,也并非是“非死不可”
,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程
。
-
第一次标记且会进行一次筛选
对于
可达性分析中不可达的对象
将会进行第一次标记且会进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法,
在finalize方法中没有重新与引用链关联关系的,就会被回收。如果此对象对应的类中没有覆盖finalize
方法,或finalize方法已经被虚拟机调用过[finalize方法系统只会调用一次]
,则认为该对象没有必要执行finalize方法,对象直接被回收; -
第二次标记
对于有必要执行finalize方法的对象,会被放置在一个F-Queue的队列中,并在稍后由一条由虚拟机自动建立的,低调度优先级的Finalizer线程去执行finalize方法,除非这个对象在执行finalize方法时与引用链建立关联[成功拯救自己],否则第二次标记成功的对象将被回收。
垃圾收集算法
-
分代收集
为什么要进行分代:
大多数对象都是朝生夕灭的;越是熬过越多次垃圾收集过程的对象就越难以消亡
,这样将那些朝生夕灭的对象放在一起,在回收时只需要关注如何保留少量存活而不去标记大量将要回收的对象,就能以较低代价回收到大量的空间;对于熬过多次垃圾收集的对象,将它们集中在一起,虚拟机便可以以较低频率来回收这个区域,这样就兼顾了垃圾回收的时间开销和内存的空间利用
。在Java堆中划分不同的区域之后,垃圾收集器才能每次只回收其中一个或者某些部分的区域——从而有了“ Minor GC[ 对新生代进行回收,不会影响到年老代 ]”、“ Major GC”、“Full GC[ 对整个堆进行回收,包括新生代和老年代 ]”这样的回收类型,从而也有了标记-清除
、标记-复制
、标记-整理
等针对性的垃圾收集算法。目前虚拟机使用的回收算法
, -
标记-清除
首先
标记出所有需要回收的对象
,在标记完成之后统一回收所有被标记对象
,该算法在存活对象较多的情况下极为高效,但是会产生大量碎片
; -
标记-复制(回收新生代-新生代中有大量的对象需要回收)
针对
整理-清除
算法的缺点,复制算法将内存分为大小相同的两块,每次使用其中的一块。当这块内存使用完后,就将还存活的对象复制到另一半去,然后再把使用的空间一次清理掉。但是这样做成本较高(若存活的对象很多,复制的成本就很大,其次就是可用内存缩小为原来的一半,空间浪费大)。所以一般不会1:1划分边界,可以分成一块较大的Eden空间和两块较小的Survivor空间
,每次使用Eden空间和1块Survivor空间,当会收时,就将Eden空间和Survivor空间中还存活的对象复制到另一块Survivor上
,清理Eden空间和Survivor空间即可,一般的比例是8:1:1
,每次浪费10%的空间,若存活的对象大于10%,就采取分配担保策略,多出来的对象直接进入老年代
。 -
标记-整理
标记过程与
标记-清除
一致,不直接对可回收对象进行回收,而是让所有存活的对象移动到一端,然后直接清除掉端边界之外的内存即可;
GC 优化
- 将进入老年代的对象数量降到最低
- 减少Full GC的执行时间
- 优化JVM参数——堆和栈的大小,设置垃圾收集器的模式
常见的面试题可能还会问常见的垃圾收集器有哪些?需要进一步进行整理和记忆