一、介绍
- 什么是垃圾?
- 垃圾是指运行在程序中没有任何指针指向的对象。
- 为什么要进行垃圾回收?
- 如果不及时对内存的垃圾进行清理,那么这些垃圾所占用的内存会一直保存到应用程序结束,被保留的空间就无法被其他对象所使用,同时也增加了内存溢出的概率。
- 什么是自动内存管理?
- 自动内存管理是:无需开发人员手动参数内存的分配以及回收,降低内存泄漏和内存溢出的风险,让程序员更专注于业务开发。
- 垃圾收集器位于JVM模型的哪里,它作用于哪?
- 垃圾收集器在执行引擎中。
- 垃圾收集器可以对年轻代、老年代甚至是全堆和方法区的回收。它频繁发生于新生区,较少发生于老年区,几乎不会发生在永久代(元空间)。
二、垃圾回收算法
1.标记阶段
在GC执行垃圾回收之前,需要先区分出内存中那些是存活对象,哪些是死亡对象。只有标记已经死亡的对象后才可以对它们进行回收。而这个阶段称为垃圾标记阶段。
这个阶段主要的算法有两种:引用计数算法和可达性分析算法。
引用计数算法:
- 实现:引用一个计数器,对每个对象保存一个整形的属性,用于记录被引用的个数。比如当对象A被引用时,A的引用计数器就加1;当引用失效时,A的引用计数器就减1。若引用计数器为0的时候,说明就没有对象引用到它,它就是一个可回收的垃圾。
- 优点:实现简单,易于辨别,回收没有延迟。
- 缺点:
- 小缺点:需要引用计数器,内存开销。需要更新计数器,时间开销。
- 严重的问题:无法处理循环引用①的问题,所以Java中并没有引用它。
①循环引用:
当对象p设为null时,右侧的三个对象的计数器互相指向对方,此时明显这三个对象已经成为"垃圾",但引用计数并不为0,所以并不会对他们进行回收,造成内存泄露。
可达性分析算法(根搜索算法、追踪性垃圾收集算法):
-
实现:
- 从根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象所连接的目标是否可达。
- 如果内存中的对象被任何一个根对象直接或者间接连接着,则为存活对象,这段连接的路径称为引用链。
- 如果并没有跟任何一个跟对象所连接,则该对象已经死亡,可以标记为垃圾对象。
-
优点:同样具备简单和执行高效,同时还有效的解决了引用计数算法的循环引用问题,也是目前Java、C#的选择。
-
使用该算法判断内存是否可回收,那么分析工作必须在一个能保证一致性的快照中进行,如果不满足分析的话结果准确性就无法保证,这也是**GC的时候必须"Stop The World"**的一个原因
- 即使是几乎不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的。
GC Roots包括哪些对象?
- 虚拟机栈中引用的对象。例如:各个线程被调用的方法中使用到的参数、局部变量等。
- 本地方法栈内JNI引用的对象。
- 方法区中类静态属性引用的对象。例如Java类的引用类型静态变量。
- 所有被同步锁synchronized持有的对象。
- Java虚拟机内部的引用。
总结:
如果一个对象,他指向堆内存中的实例,但自己又不存放在堆内存中,则它就是一个Root。
对象的finalization机制:
-
在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处 于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
-
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做 F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象 进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
-
例子:
public class CanReliveObj { public static CanReliveObj obj;//类变量,属于 GC Root //此方法只能被调用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用当前类重写的finalize()方法"); obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系 } public static void main(String[] args) { try { obj = new CanReliveObj(); // 对象第一次成功拯救自己 obj = null; System.gc();//调用垃圾回收器 System.out.println("第1次 gc"); // 因为Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } System.out.println("第2次 gc"); // 下面这段代码与上面的完全相同,但是这次自救却失败了 obj = null; System.gc(); // 因为Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
输出:
第1次 gc 调用当前类重写的finalize()方法 obj is still alive 第2次 gc obj is dead
2.清除阶段
标记-清除算法:
- 标记阶段:收集器从引用根节点开始遍历,标记出所有被引用的对象,也就是存活的对象。
- 清除阶段:收集器对堆内存从头到尾进行线性的遍历,如果发现对象没有被标记,则对其进行清除(这里的清除并不是置空,而是将"死亡"的对象的地址存放在空闲表里,若有新的对象加载时,再将其覆盖)
- 缺点:
- 效率不算高
- 清理后空间不是连续的,产生内存碎片,这时候需要一个空闲列表来维护这些内存空间。
复制算法:
- 实现:将内存空间分成两块,每次只使用一半,在垃圾回收时将正在使用的那块空间的存活对象复制到另外一内存块中,之后清楚前面使用的内存块中所有对象。
-
优点:
- 没有标记和清楚过程,实现简单,运行高效
- 由于是复制过去,不会出现空间碎片
-
缺点:
- 只能使用到一半的内存空间
- G1收集器拆成大量region的GC,由于是复制所以地址也会发生改变,因此G1需要开销时间去重新引用对象地址。
-
适用于存活对象较少的场景
标记-整理算法:
- 实现:第一阶段和标记清除算法一致标记所有存活的对象,第二阶段将所有存活的对象整理到内存的一段,按顺序排放,最后清理边界外所有的空间。
- 优点:
- 相对于标记-清除算法:没有了空间碎片
- 相对于复制算法:内存使用率提高
- 缺点:
- 效率上比两者都慢
- 跟复制算法一样改变了对象的地址,所以还需要重新调整引用
- 整理过程中,会引起STW
分代收集算法:
- 不同生命周期的对象采用不同的手机方式来提高回收的效率。
- 目前几乎所有的GC都采用的是分代收集算法:
- 年轻代:
- 对象生命周期短,存活率低,回收频率高,采用复制算法。
- 老年代:
- 对象生命周期长,存活率高,回收频率低,采用标记清除和标记-整理的混合实现(例如Hotspot中,CMS收集器使用标记清除算法,产生的空间碎片以Serial Old收集器作为补偿。当回收内存不佳,采用Serial Old进行碎片整理)
- 年轻代:
增量收集算法:
- 基本思想:
- 由于有时候一次性回收过多导致STW的时间过长,会让用户感觉程序"卡顿",所以让垃圾回收线程和应用程序线程交替执行。
- 允许垃圾收集线程分阶段的方式完成标记、清理或者复制工作。
- 缺点:
- 因为线程切换和上下文转换的消耗,会使垃圾回收的成本上升,造成系统吞吐量的下降。
分区算法:
- 一般来说,在相同条件下,堆空间越大,一次GC时 所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制Gc产生的停顿时间,将一块大的内 存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
- 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
三、补充
System.gc():
- 通过System.gc()【内部调用的是Runtime.geiRuntime().gc()方法】会显式触发Full GC。
- System.gc()有一个免责声明,无法保证一定会调用。
System.gc()的回收行为:
public class LocalVarGC {
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
//14172K->11152K(188416K)
}
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
//14172K->880K(188416K)
}
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
//14172K->11184K(188416K)
}
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
//14172K->880K(188416K)
}
public void localvarGC5() {
localvarGC1();
System.gc();
//10974K->734K(188416K)
}
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
}
localvarGC3():GC发生的时候并不是直接置空数据,当有新对象进来的时候覆盖掉,在localvarGC4()方法中,为value变量分配空间后才进行了回收。
内存溢出和内存泄漏:
- 内存溢出的原因:
- 堆内存设置不够
- 创建了大量大对象,并且长时间没有进行回收
- 内存泄漏:
- 对象不再被引用,但GC无法对他们进行回收,例如:
- 单例模式:单例模式的生命周期跟程序是一样长的
- 数据库连接、网络连接、io连接操作未关闭close
- 对象不再被引用,但GC无法对他们进行回收,例如:
垃圾回收的串行、并行、并发:
- 串行:单线程执行,回收的时候只能有一条垃圾回收线程进行回收
- 并行:多线程执行,回收的时候能够有多条垃圾回收线程进行垃圾回收,但是用户程序线程还是处于暂停状态
- 并发:用户线程与垃圾回收线程交互执行
安全点和安全区域:
- 安全点:
- 程序在执行的时候,并不是在任何时候都可以进行GC,只有在特定的位置上才可以进行GC,而这个位置就称作安全点。
- 安全点的个数:太少会引起GC等待时间过长,太多的话会导致GC频率过高,开销过大。
- 安全点的选择:通常选择具有长时间执行的片段的点,例如循环跳转、异常跳转、方法调用等。
- 检查所有线程是否跑到安全点停下来:
- 抢占式中断(被淘汰)中断所有线程,如果还有线程不在安全点则恢复线程让没有到安全点的线程跑到安全点。
- 主动式中断:设置一个标记,当程序运行到安全点的时候去询问这个标记,如果标记为真的话则自己主动挂起。
- 安全区域:
- 当线程在睡眠或者阻塞状态的时候无法响应中断请求,但在这期间对象的引用并不会发生改变,所以引用了安全区域(安全区域是指在一段代码片段中,对象的引用并不会发生改变,在这段区域中的任何位置进行GC都是安全的)
- 当运行到安全区域时候,这段时间发生GC,JVM会忽略标识为安全区域的线程,当离开安全区域的时候,会检查JVM是否完成了GC,如果完成了继续执行用户线程,如果未完成直到线程收到通知才可以离开。
四、对象的引用(Reference)
-
java中常见的引用有强引用、软引用、弱引用、虚引用、以及终结引用,除了强引用其他四个都在java.lang.ref包下的Reference里。
-
强引用:最常用的引用关系,例如"Object obj=new Object()",在对象还存在引用关系时,垃圾收集器就不会对其回收。
-
软引用:垃圾收集器进行回收的时候先对"死亡"的对象进行回收,如果回收后内存还是不够再对这些软引用进行回收,如果还是不够则会报出OOM,通常使用在缓存。
-
软引用的使用:
-
//构造了软引用 SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
-
//从软引用中重新获得强引用对象 System.out.println(userSoftRef.get());
-
-
-
弱引用:垃圾收集器进行回收的时候会对软引用的对象一起进行回收。
-
弱引用的使用:
-
//构造了弱引用 WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk")); //从弱引用中重新获取对象 System.out.println(userWeakRef.get());
-
-
-
虚引用:无法访问到对象的实例,当对象被回收的时候,会加入引用队列(ReferenceQueue)中,可以通过引用队列查看对象被回收的通知。
-
虚引用的使用:
-
ReferenceQueue<User>phantomQueue = new ReferenceQueue<User>(); User user = new User(); PhantomReference<User> phantomRef = new PhantomReference<User>(user, phantomQueue);
当user对象被回收的时候,会加入到phantomQueue队列中。
-
-
五、垃圾收集器
评估收集器的主要两个标准:
-
吞吐量:即用户线程时间/(用户线程时间+垃圾回收线程时间),吞吐量越高则越优秀
-
暂时时间:即STW时间,越短则越好。