JVM垃圾回收
目录
简介
释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
一、对象是否可回收
1.如何判断一个对象已经无效
- 引用计数法:对象添加一个引用计数器,每当一个地方引用它,计数器+1;当引用失效,计数器-1.计数器为0的对象就是可回收的对象。(无法解决两个对象互相引用,导致计数器永不为0的情况)
- 可达性分析:以GCRoot为起点进行搜索,可达的对象都是存活的, 不可达的对象可被回收。
GCRoot一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象。
- 本地方法栈中 JNI 中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中的常量引用的对象。
2.引用类型
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
JDK1.2之前,Java 中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。
1. 强引用
被强引用关联的对象不会被回收,使用new一个新对象的方式来创建强引用。
如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
Object obj = new Object();
2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。使用SoftReference类来创建软引用。
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用通常用来实现内存敏感(比如图片)的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存.如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3. 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用WeakReference类来创建弱引用。
弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中.弱应用同样可用于内存敏感的缓存。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4. 虚引用
虚引用也叫幻象引用,为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知,使用PhantomReference来创建虚引用。
无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中.程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
3.不可达的对象是否一定被回收
即使在可达性分析法中不可达的对象,也并非是必须回收的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
4.如何判断一个常量是否是废弃常量
常量池中的字符串,如果当前没有任何String对象引用该字符串常量的话,那么就认为是废弃常量,可以回收。
5.如何判断一个类是无用的类
类需要同时满足下面 3 个条件才能算是 “无用的类”:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
二、垃圾收集算法
1. 标记-清除算法
标记阶段为活动对象头部打上标记,清除阶段,进行对象回收并取消标志位。
不足之处:标记和清除过程效率都不高;会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2. 标记-整理算法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点:不会产生内存碎片。
缺点:需要移动大量对象,处理效率比较低。
3. 复制算法
将内存分为大小一样的两块区域,每次使用其中一块,当其中一块内存使用完就将存活对象移动到另一块内存上,然后清理使用过的内存空间。
不足之处:每次只使用一半的内存。
4. 分代收集算法
根据对象存活周期将内存划分为几块,不同块采用不同的算法,一半将堆划分为新生代和老年代。
一般将堆分为新生代和老年代:
- 新生代:复制算法。
- 老年代:标记 - 清除或者标记 - 整理 算法。
三、垃圾收集器
1. Serial收集器
- 串行收集器,单线程,进行垃圾回收的时候会暂停其他所有工作线程。
- 优点是简单高效,在单个CPU环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
- Serial收集器对于运行在Client模式(默认收集器)下的虚拟机来说是个不错的选择。
- 新生代采用复制算法,老年代采用标记-整理算法。
2. ParNew收集器
- Serial收集器的多线程版本。
- 它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配合使用。
- 新生代采用复制算法,老年代采用标记-整理算法。
3. Parallel Scavenge收集器
- 多线程收集器,关注点为吞吐量(高效利用CPU)。
- 所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
- 新生代采用复制算法,老年代采用标记-整理算法。
4. Serial Old收集器
- Serial收集器的老年代版本,单线程,也是给Client场景下的虚拟机使用。
- 在server场景下有两大用途:
- 在 JDK1.5以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5. Parallel Old 收集器
- Parallel Scavenge收集器的老年代版本。
- 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge加Parallel Old收集器。
- 使用多线程和“标记-整理”算法。
6. CMS 收集器
- CMS(Concurrent Mark Sweep),Mark Sweep指的是标记-清除算法。
- 是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 过程:
- 初始标记:暂停所有的其他线程,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。
- 进行GC Roots Tracing的过程,同时开启GC和用户线程,它在整个回收过程中耗时最长,不需要停顿。
- 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 开启用户线程,同时GC线程开始对未标记的区域做清扫,不需要停顿。
- 缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用Serial Old来替代CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次Full GC。
7. G1收集器
- G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。
- G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,通过引入Region的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收,通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,每个Region都有一个 Remembered Set,用来记录该Region对象的引用对象所在的 Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
- G1 收集器的运作步骤:
- 初始标记。
- 并发标记。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
- 特点:
- 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU 或者 CPU 核心)来缩短Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。