垃圾收集器与内存分配策略
1.0 概述
垃圾收集(Garbage Collection,GC)要了解的三个问题:
哪些内存需要回收 ?
什么时候回收 ?
如何回收 ?
当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要进行必要的监控和调节。
程序计数器、虚拟机站、本地方法栈3个区域都是线程同步的,随线程而生,随线程而灭,因此,在这几个区域不需要考虑过多的回收问题。而Java堆和方法区不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。
2.0对象已死吗
2.1 引用计数法
引用计数法(Reference Counting)实现简单,判定效率高。但无法解决对象之间相互循环引用的问题。
2.2可达性分析算法
可达性分析(Reachability Analysis)判定对象是否存活。让“ GC Roots ”的对象作为起始点,从这些节点开始向下搜索,搜索的路径成为引用链(Reference Chain),当一个对象到GC Roots没有引用链相连时,则证明此对象是不可用的。
可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(一般说的Native方法)引用的对象。
2.3 强软弱虚引用
传统引用的定义:如果reference类型的数据中存储的数值是另一块内存的起始地址,就称这块内存代表着一个引用。但对一些“ 食之无味,弃之可惜 ”的对象显得无能为力。当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可抛弃这些对象。
4种引用:引用强度依次逐渐减弱。
- 强引用(Strong Reference)是在程序代码之中普遍存在的,类似“ Object obj = new Object(); ”这类引用,垃圾回收器永远不会回收掉被强引用引用着的对象。
- 软引用(Soft Reference)描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果回收还没有足够的内存,才会抛出内存溢出的异常。(SoftReference类来实现软引用)
- 弱引用(Weak Reference)是描述非必需对象的,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作,无论当前内存是否足够,都回会收掉只被弱引用关联的对象。(WeakReference类来实现弱引用)
- 虚引用(Phantom Reference)也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的:在这个对象被收集器回收时收到一个系统通知。(PhantomReference类来实现虚引用)
2.4 回收无效对象的过程
当JVM筛选出无用对象之后,并不是马上清楚,而是再给对象一次重生的机会,至少要经历两次标记过程。
(1)判断对象是否覆盖了Object类的finalize()方法,并且finalize()方法是否已经被JVM调用过
- 若未覆盖该方法,或者已经被JVM调用过该对象的finalize()方法,直接释放对象内存
- 若已覆盖方法,并且该对象的finalize()方法还没有被执行过,那么会将对象放到F-Queue队列中
(2)执行F-Queue队列中的finalize()方法,虚拟机会以较低的优先级执行这些finalize()方法,但不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清楚。
(3)对象重生还是死亡 如果在执行finalize()方法时,对象重新与引用链上的任何一个对象关联(将this赋给了某一个引用),那么对象会重生。若没有,那么垃圾收集器就会清楚。
注:强烈不建议使用finalize()函数进行任何操作!如果释放资源,使用try-finally。因为finalize()不确定性大,开销大,无法保证顺利执行。
2.5 方法区的回收
Java虚拟机规范中说过可以不要求虚拟机的方法区实现垃圾回收。在堆中,尤其是在新生代中,进行一次垃圾收集一般可以回收70% ~ 90%的空间,而永久代的垃圾收集效率远低于此。
方法区中存放声明周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。
方法区中主要清除两种垃圾:(1)废弃常量;(2)废弃的类。
如何判定废弃常量?
清楚废弃的常量和清除对象类似,只要常量池中的常量没有被任何变量或对象引用,那么这些常量就会被清除掉。
如何判定废弃的类?
1.该类的所有对象已被清楚;
2.加载该类的ClassLoader已被回收;
3.该类的java.lang.Class对象没有被任何对象或变量引用 只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。
3.0 垃圾收集算法
3.1 标记—清楚算法(Mark-Sweep)
分为两个阶段:1.标记需要被回收的对象;2.清楚被标记的对象。
缺点:
- 效率低。标记和清楚两个过程效率都不高;
- 碎片化空间。标记清楚之后会产生大量不连续的内存碎片,导致无法分配大对象,降低了空间利用率。
3.2 复制算法(Copying)
将内存分成两份,每次只使用其中的一份,当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一块内存上,最后将第一块内存全部清除。
分析
虽然避免了碎片空间,但内存缩小了一半。
现在的商业虚拟机都采用这种收集算法来回收新生代。
解决空间利用率问题
新生代中的对象98%都是“ 朝生夕死 ”的,所以不需要按照1:1的比例来划分内存空间。而是将内存区分为Eden区和两块Survivor区,比例8:1:1,每次只使用Eden区和其中的一块Survivor区。当回收时,将Eden区和Survivor中还存活的对象一次性地复制到另外一块Survivor区中,最后清理掉Eden区和Survivor中使用过的空间。
什么是分配担保?
内存的分配担保,如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
3.3 标记–整理算法(Mark–Compact)
首先将所有废弃的对象做上标记,然后将所有未被标记的对象移到一边,最后清空另一边区域即可。
分析:
它是一种老年代的垃圾收集算法。老年代中的对象一般存活时间较长,因此每次垃圾收集会有大量的对象存活。如果使用复制算法,每次都需要复制大量的对象,效率不高 。而且,在新生代中使用复制算法,当Eden+Survivor区中都装不下某个对象时,可以使用老年代的进行分配担保,而如果在老年代使用该算法,没有其他区域作分配担保。因此,老年代一般使用标记-整理算法。
3.4 分代收集算法
根据对象的存活周期不同,一般将内存划分为老年代和新生代。新生代中存放“ 朝生夕死 ”的对象,每次收集都会有大量的对象死去,使用复制算法。老年代中存放的对象寿命较长、没有额外空间对它进行分配担保,使用标记–整理或者标记–清除算法。
5.0 垃圾收集器
垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
5.1 Serial收集器
这是一个单线程收集器,它在进行垃圾收集时,必需暂停其他所有的工作线程。使用的不多。Serial收集器没有线程交互的开销,自然可以获得最高的单线程收集效率。对于运行在Client模式下的虚拟机来说是一个很好的选择。
5.2 ParNew收集器
它是Serial收集器的多线程版本,使用多线程进行垃圾收集。
5.3 Parallel Scavenge收集器
它是一个新生代收集器,使用复制算法,也是并行的多线程收集器。
5.4 Serial Old收集器
它是Serial收集器的老年代版本。单线程收集器,使用“标记–整理”算法。主要给Client模式下的虚拟机使用。
5.5 Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“ 标记–整理 ”算法。
5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。使用“ 标记–清除 ”算法(缺点:会产生碎片化空间)。过程包括:初始标记、并发标记、重新标记、并发清除。并发收集,分别处理,停顿时间短,在垃圾收集过程,JVM还可以运行。
5.7 G1收集器
G1(Garbage-First)是一款面向服务端的收集器。运行过程:初始标记、并发标记、最终标记、筛选回收。优点:不仅停顿时间短,而且并发量大。
6.0 内存的分配与回收策略
对象的内存分配,大方向上就是堆上分配。对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配规则不固定。
6.1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将进行一次Minor GC。-XX:+PrintGCDetails参数负责打印内存回收日志,并且在内存退出时输出当前内存各区域的分配情况。
Minor GC和Full GC的区别
新生代(Minor GC)指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,回收速度也快。多线程并行执行。
老年代(Major GC / Full GC)指发生在老年代的GC,MajorGC经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。尽量做到让对象在Minor GC阶段被回收。(可通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc())。
Perm Gen(永久代)也会触发Full GC,可以增大永久带的大小或者转为CMS GC回收永久代。
6.2 大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的Java对象,比如字符串和数组。
虚拟机提供了一个-XX:PretenureSizeThreshold参数。令大于这个设置值的对象直接分配在老年代。避免在Eden区和Survivor区之间发生大量的内存复制。
6.3 长期存活的对象进入老年代
虚拟机采用分代收集的思想来管理内存,那么内存回收时必须能识别哪些对象应放在新生代,哪些对象应放在老年代。因此,虚拟机给每个对象定义了一个对象年龄(Age)计数器。没经过一次Minor GC并且存活下来,年龄+1,默认年龄增加到15(默认阈值),会被放入老年代中。(阈值通过参数-XX:MaxTenuringThreshold设置)
6.4 动态对象年龄判断
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenureingThreshold才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenureingThreshold年龄要求。
6.5 空间分配担保
当垃圾收集器准备要在新生代发起一次MinorGC时,首先会检查“ 老年代中最大的连续空闲区域的大小 是否大于 新生代中所有对象的大小?”,也就是老年代中目前能够将新生代中所有对象全部装下?
Y:若老年代能够装下新生代中所有的对象,那么此时进行MinorGC没有任何风险,然后就进行MinorGC。
N:若老年代无法装下新生代中所有的对象,那么此时进行MinorGC是有风险的,垃圾收集器会进行一次预测:根据以往MinorGC过后存活对象的平均数来预测这次MinorGC后存活对象的平均数。
<:如果以往存活对象的平均数小于当前老年代最大的连续空闲空间,那么就进行MinorGC,虽然此次MinorGC是有风险的。
>:如果以往存活对象的平均数大于当前老年代最大的连续空闲空间,那么就对老年代进行一次Full GC,通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。
这个过程就是分配担保。
当大量对象在MinorGC后仍然存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
注:
- 分配担保是老年代为新生代作担保;
- 新生代中使用“复制”算法实现垃圾回收,老年代中使用“标记-清除”或“标记-整理”算法实现垃圾回收,只有使用“复制”算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保。