垃圾回收
程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而对于 Java 堆和方法区,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
判断一个对象是否可以回收
1. 引用计数法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
2. 可达性分析算法
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
GC Roots 是指:
· Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
· 本地方法栈中引用的对象
· 方法区中常量引用的对象
· 方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
引用
-
强引用
类似 “Object obj = new Object()” 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远不会回收被引用的对象。但是,如果我们***错误地保持了强引用***,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。
new一个对象来创建强引用
Object obj = new Object();
-
软引用
软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来***实现内存敏感的缓存***,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
SoftReference类来创建软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
- 弱引用
弱引用的****强度比软引用更弱****一些。当 JVM 进行垃圾回收时,****无论内存是否充足,都会回收****只被弱引用关联的对象。
使用SoftReference类来创建软引用
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
使用PhantomReference类
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
回收堆中无效对象
对于可达性分析中不可达的对象,也并不是没有存活的可能。
(1)判定 finalize() 是否有必要执行
JVM 会判断此对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。
如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
(2)对象重生或死亡
如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。
任何一个对象的 finalize() 方法只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。
回收方法区内存
方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
· 废弃常量
· 无用的类
(1)判定废弃常量
只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 “bingo” 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 “bingo” 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。
(2)判定无用的类
判定一个类是否是“无用的类”,条件较为苛刻。
· 该类的所有对象都已经被清除
· 加载该类的 ClassLoader 已经被回收
· 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。
垃圾回收算法
标记-清除算法
标记***的过程是:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象***标记为存活的对象。
***清除***的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。
这种方法有两个***不足***:
· 效率问题:标记和清除两个过程的效率都不高。
· 空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-整理算法(老年代)
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
复制算法(新生代)
为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:
· 优点:不会有内存碎片的问题。
· 缺点:内存缩小为原来的一半,浪费空间。
分代收集算法
根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。
· 新生代:复制算法
· 老年代:标记-清除算法、标记-整理算法
垃圾回收器
- 串行
- 单线程
- 堆内存小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW的时间最短 0.2 0.2 =0.4
- 相应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让STW的时间最短 0.1 0.1 0.1 0.1 0.1 =0.5
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)
新生代垃圾收集器
(1)Serial 垃圾收集器(单线程)
只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)。
一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。因此 Serial 垃圾收集器适合客户端使用。
由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高效。
(2)ParNew 垃圾收集器(多线程)
ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
ParNew 追求低停顿时间,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。
(3)Parallel Scavenge 垃圾收集器(多线程)
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:
· Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
· ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。
· 通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。
· 通过参数 -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。
· 通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄,以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。
老年代垃圾收集器
(1)Serial Old 垃圾收集器(单线程)
Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。
Parallel Old 垃圾收集器(多线程)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
(2)CMS 垃圾收集器
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
· 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
· 并发标记:使用****多条****标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
· 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
· 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,****总体上说****,CMS 收集器的内存回收过程是与用户线程****一起并发执行****的。
(3)CMS 的缺点:
· 吞吐量低
· 无法处理浮动垃圾,导致频繁 Full GC
· 使用“标记-清除”算法产生碎片空间
对于产生碎片空间的问题,可以通过开启 -XX:+UseCMSCompactAtFullCollection,在每次 Full GC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction告诉 CMS,经过了 N 次 Full GC 之后再进行一次内存整理。
G1通用垃圾收集器
G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
内存分配和回收策略
Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
对象优先在Eden分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
Minor GC VS Major GC / Full GC
· Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
· Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。
在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。
大对象直接进入老年代
大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。
一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。(还记得吗,新生代采用复制算法回收垃圾)
长期存活的对象将进入老年代
JVM 给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后,存活下来的对象年龄 +1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。
使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。
动态对象年龄判定
如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC
Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
1. 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。