☆技术问答集锦(17)JVM垃圾回收
1 判断对象是否可回收有几种方式?
- 引用计数算法
优点:实现简单,判定高效;
缺点:很难解决对象之间相互循环引用的问题;
- 可达性分析算法
通过一系列"GC Roots"对象作为起始点,开始向下搜索,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到这个对象不可达),则证明该对象是不可用的;
优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;
缺点:实现比较复杂;需要分析大量数据,消耗大量时间;分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题);
2 "GC Roots"对象都包含哪些
- 虚拟机栈 (栈帧中本地变量表)中引用的对象;
- 方法区中 类静态属性引用的对象 ;
- 方法区中 常量引用的对象;
- 本地方法栈 JNI(Native方法)中引用的对象;
3 Java四种引用类型分别是什么?及存活时间
- 强引用:程序代码普遍存在的,类似"Object obj=new Object()";只要强引用还存在,GC永远不会回收被引用的对象;
- 软引用:描述还有用但并非必需的对象;直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收;最常用于实现对内存敏感的缓存;SoftReference类实现;
- 弱引用:用来描述非必需对象;只能生存到下一次垃圾回收之前,无论内存是否足够;WeakReference类实现;
- 虚引用:完全不会对其生存时间构成影响;唯一目的就是能在这个对象被回收时收到一个系统通知;PhantomRenference类实现;
4 Java四种引用使用场景
- 强引用-FinalReference
地球人都知道,但是我讲不出来;
- 软引用-SoftReference
创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。PS:图片编辑器,视频编辑器之类的软件可以使用这种思路。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
- 弱引用-WeakReference
Java源码中的java.util.WeakHashMap中的key就是使用弱引用,一旦不需要某个引用,JVM会自动处理它,这样就不需要做其它操作。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
- 虚引用-PhantomReference
主要用来跟踪对象被垃圾回收器回收的活动。虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。
5 JVM如何进行对象标记
- 第一次标记:在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;没有必要执行的情况,则标记对象已死;有必要执行的情况,则对象被放入F-Queue队列中;
- 第二次标记:GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会;一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
6 为何不建议使用finalize()方法
因为其执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用;
如果有关键资源,必须显式的终止方法;一般情况下,应尽量避免使用它,甚至可以忘掉它;
7 什么是安全点,为什么需要
运行中,非常多的指令都会导致引用关系变化;如果为这些指令都生成对应的OopMap,需要的空间成本太高;
只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint);
8 如何选定安全点
不能太少,否则GC等待时间太长;也不能太多,否则GC过于频繁,增大运行时负荷;
所以,基本上是以程序"是否具有让程序长时间执行的特征"为标准选定,如:方法调用、循环跳转、循环的末尾、异常跳转等;
只有具有这些功能的指令才会产生Safepoint;
9 如何使Java线程在安全点上停顿
- 抢先式中断(Preemptive Suspension):在GC发生时,首先中断所有线程;如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;
- 主动式中断(Voluntary Suspension):在GC发生时,不直接操作线程中断,而是仅简单设置一个标志;让各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起;
- 而轮询标志的地方和Safepoint是重合的;
10 什么是安全区域,为什么需要安全区域
线程不执行时没有CPU时间(Sleep或Blocked状态),无法运行到Safepoint上再中断挂起;
安全区域:指一段代码片段中,引用关系不会发生变化;在这个区域中的任意地方开始GC都是安全的;
11 如何使用安全区域解决问题
- 线程执行进入Safe Region,首先标识自己已经进入Safe Region;
- 线程被唤醒离开Safe Region时,其需要检查系统是否已经完成根节点枚举(或整个GC);
- 如果已经完成,就继续执行;否则必须等待,直到收到可以安全离开Safe Region的信号通知,这样就不会影响标记结果;
12 GC算法:标记-清楚优缺点
优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;
13 GC算法:复制算法优缺点
优点:使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;
缺点:空间浪费;效率随对象存活率升高而变低;
14 GC算法:HotSpot虚拟机复制算法
- 将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
- 每次使用Eden和其中一块Survivor;
- 当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
- 而后清理掉Eden和使用过的Survivor空间;
- 后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
15 什么是分配担保
如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;
16 GC算法:标记-整理优缺点
优点:不会产生内存碎片;
缺点:增加了对存活对象需要整理的过程,效率更低;
17 分代收集算法
"分代收集"(Generational Collection)算法结合不同的收集算法处理不同区域。
新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;
老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;
优点:根据各个年代的特点采用最适当的收集算法;
缺点:仍然不能控制每次垃圾收集的时间;
18 G1垃圾收集算法
19 JVM有哪些收集器?分别用于哪些代?
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
20 Serial收集器
新生代、复制算法、单线程收集;
缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
Serial/Serial Old组合收集器运行示意图如下:
21 ParNew收集器
新生代、复制算法、多线程收集;
缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
ParNew/Serial Old组合收集器运行示意图如下:
22 Parallel Scavenge收集器
新生代、复制算法、多线程收集;
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
23 Serial Old收集器
老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、单线程收集;
24 Parallel Old收集器
老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、多线程收集;
25 CMS收集器
老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿
CMS收集器运行示意图如下:
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
CMS收集器3个明显的缺点:
- 对CPU资源非常敏感;
- 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败;
- 产生大量内存碎片;
26 G1收集器
27 JVM如何进行对象内存分配
在堆上分配(JIT编译优化后可能在栈上分配),主要在新生代的Eden区中分配;
如果启用了本地线程分配缓冲,将线程优先在TLAB上分配;
少数情况下,可能直接分配在老年代中;
分配的细节取决于当前使用哪种垃圾收集器组合,以及JVM中内存相关参数设置;
28 哪些情况下对象内存分配会直接进入老年代
- 当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代。
- 需要大量连续内存空间的Java大对象会直接进入老年代,容易提前触发老年代GC;
- 经过多次Minor GC,如果年龄达到一定程度,就晋升到老年代;
- 动态对象年龄判定:如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;
29 方法区中可回收哪些对象
- 废弃常量:与回收Java堆中对象非常类似;
- 无用的类:(1)该类所有实例都已经被回收(即Java椎中不存在该类的任何实例);(2)加载该类的ClassLoader已经被回收,也即通过引导程序加载器加载的类不能被回收;(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
30 JDK HotSpot虚拟机方法区调整
- 在JDK7中,使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;
- 在JDK8中,永久代已被删除,类元数据(Class Metadata)存储空间直接在本地内存中分配;