二 垃圾收集器与内存分配策略
1 JVM中哪些内存需要回收?
JVM垃圾回收主要关注的是Java堆和方法区这两个区域;而程序计数器、虚拟机栈、本地方法栈这3个区域随线程而生,随线程而灭,随着方法结束或者线程结束内存自然跟随着回收了,因此不需要过多考虑内存分配和回收的问题。
2 判断对象是否存活的算法
(1)引用计数算法
基本思路:给对象添加一个引用计数器,每当有一个地方引用它,计数器值加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率高。
缺点:很难解决对象之间循环引用的问题。
应用:Python语言等。
(2)可达性分析算法
基本思路:通过一系列的称为“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Java语言中,GC Roots的选取为:
· 虚拟机栈中引用的对象;
· 方法区中类静态属性引用的对象;
· 方法区中常量引用的对象;
· 本地方法中JNI引用的对象。
应用:Java、C#等。
3 JVM中如何判断一个对象已经死亡?
判定一个对象死亡要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(当该对象没有覆盖finalize()方法或者finalize()方法已经被调用过,虚拟机将这两种情况都视为”没有必要执行“)。若这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果该对象在执行finalize()后重新与引用链上的任何一个对象建立了关联,那么在第二次标记时它将被移除出”即将回收“的集合;否则,该对象就将被回收。
4 强引用、软引用、弱引用、虚引用的区别
强引用:程序代码中普遍存在,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:描述一些还有用但非必需的对象,若一个对象具有软引用,则内存空间足够时,垃圾收集器不会回收它,内存空间不足时,就会回收该对象。可用来实现内存敏感的高速缓存。
弱引用:强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用:也称为幽灵引用或者幻影引用,一个对象是否有弱引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是在这个对象被收集器回收时收到一个系统通知。
软引用、弱引用、虚引用都可以和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有软引用(或弱引用、虚引用),就会在回收对象的内存之前,把这个软引用(或弱引用、虚引用)加入到与之关联的引用队列中。
5 方法区的回收
回收内容:废弃常量和无用的类。
废弃常量的判定:没有任何引用指向该常量。
无用的类的判定:
· 该类的所有实例都已经被回收,也即Java堆中不存在该类的任何实例;
· 加载该类的ClassLoader已经被回收;
· 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
方法区回收的”性价比“一般比较低,虚拟机可以实现成不进行方法区的垃圾收集。
6 垃圾回收算法及应用场景
(1)标记-清除算法
过程:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象;
不足:
· 效率问题。标记和清除两个过程的效率都不高;
· 空间问题。标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
应用:CMS收集器。
(2)复制算法
过程:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面。然后再把已使用过的内存空间一次性清理掉。
优点:实现简单,运行高效,而且不用考虑内存碎片问题。
缺点:代价是将内存缩小为原来的一半。
改进:将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
改进版本缺点:每次回收时存活的对象可能比较多,导致Survivor空间不够用,需要依赖其他内存(老年代)进行分配担保。
应用:新生代垃圾收集器,如Serial、ParNew、Parallel Scavenge以及G1收集器的局部上看。
(3)标记-整理算法
过程:首先标记出所有需要回收的对象,然后让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会出现空间碎片问题。
应用:老年代垃圾收集器,如Serial Old、Parallel Old、G1收集器的整体上看。
(4)分代收集算法
过程:根据对象存活周期的不同将内存划分成几块,一般是将Java堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
使用:一般新生代使用复制算法,老年代使用标记-清除或标记-整理算法。
7 HotSpot的算法实现
(1)枚举根节点
使用一组称为OopMap的数据结构来达到使虚拟机知道哪些地方存放着对象引用的目的。
(2)安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
安全点的选择:以”是否具有让程序长时间执行的特征“为标准进行选定的。
”长时间执行“的特征:指令序列复用,如方法调用、循环跳转、异常跳转等。
如何在GC发生时让所有线程都跑到最近的安全点再停顿下来:
· 抢先式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。
· 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就将自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
(3)安全区域
安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的。
8 垃圾收集器
(1)新生代
1)Serial收集器
---单线程:不仅在于只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(“Stop The World”);
---使用复制算法;
---是虚拟机运行在Client模式下的默认新生代收集器;
---可以与CMS收集器配合工作;
---优点:简单而高效,对于限定单个CPU的环境下,Serial收集器由于没有线程交互的开销,故可获得最高的单线程收集效率。
2)ParNew收集器
---并行多线程:Serial收集器的多线程版本;
---采用复制算法;
---暂停所有的用户线程(“Stop The World”);
---运行在Server模式下的虚拟机中首选的新生代收集器;
---可以与CMS收集器配合工作。
3)Parallel Scavenge收集器
---并行多线程;
---使用复制算法;
---“吞吐量优先”收集器,目标是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值),其他收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间;
---具有自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大的吞吐量。
(2)老年代
1)Serial Old收集器
---单线程;
---使用标记-整理算法;
---主要用于给Client模式下的虚拟机使用;
---在Server模式下,主要有两大用途:一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
2)Parallel Old收集器
---多线程;
---标记-整理算法;
---与Parallel Scavenge收集器配合使用;
---用于注重吞吐量以及CPU资源敏感的场合。
3)CMS收集器
---基于标记-清除算法。
---目标:获取最短回收停顿时间。
---包括四个步骤:
一、初始标记:仅标记GC Roots能直接关联到的对象,速度快,单线程执行;
二、并发标记:进行GC Roots追踪的过程,速度慢,并发执行;
三、重新标记:修正并发标记期间因用户线程继续运作而导致产生变动的那一部分对象的标记记录,速度较快,并行执行;
四、并发清除:速度慢,并发执行;
其中初始标记与重新标记两步仍需要“Stop The World”。总体来说,其内存回收过程是和用户线程一起并发执行的。
---优点:并发收集、低停顿。
---缺点:
-
对CPU资源非常敏感:在并发阶段,因垃圾收集线程占用一部分CPU资源而导致用户程序变慢,总吞吐量会降低;
-
无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生:并发清理阶段,随着用户线程继续运行,还会有新的垃圾继续产生,CMS无法在当次收集中处理掉它们,如果运行期间预留的内存无法满足程序的需要,就会出现该失败,临时启用Serial Old收集器重新进行老年代的垃圾收集;
-
收集结束时会有大量空间碎片产生。
(3)G1收集器
---目标:获取最短回收停顿时间。
---面向服务端应用的垃圾收集器。
---特点:
· 并行和并发;
· 分代收集:可以采用不同的方式处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象;
· 空间整合:整体看基于标记-整理算法,局部看基于复制算法,不会产生内存空间碎片;
· 可预测的停顿:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
---使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将Java堆划分为多个大小相等的独立区域,新生代和老年代不再是物理隔离的,它们都是一部分Region的集合。
---G1收集器能建立可预测的停顿时间模型的原因:它避免在整个Java堆中进行全区域的垃圾收集,每次根据允许的收集时间,优先回收价值最大的Region。
---在G1收集器中Region之间的对象引用和其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。
---G1收集器的运行过程:
· 初始标记:标记GC Roots能直接关联到的对象,并且修改TAMS的值(以便下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象),单线程执行,需要“Stop The World",但速度快,耗时短;
· 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,并发执行,耗时较长;
· 最终标记:修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,并行执行,需要”Stop The World";
· 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,并行执行(也可以与用户线程并发执行,但是停顿用户线程将大幅提高收集效率)。
9 内存分配策略
· 对象优先在新生代的Eden分配;
· 大对象直接进入老年代。大对象:需要大量连续内存空间的Java对象,如数组、字符串;
· 长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄计数器;
· 动态对象年龄判定。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代;
· 空间分配担保。