垃圾回收 (Garbage Collection)的区域:
对于程序计数器、虚拟机栈、本地方法栈而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,当方法结束或者线程结束时,内存就自然跟着线程回收了。对于方法区,是类加载时使用的,类卸载时释放,但是类卸载操作是一个非常低频的操作。所以内存分配和回收主要关注的是堆这个区域。
Java 堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。
引用计数描述的算法为:
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。
缺点:
观察循环引用问题
class Test {
public Object instance = null;
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
}
}
test1、test2 两个对象的引用计数器都不为 0, 但是其实 test1 和 test2 都是无法被外界代码访问到的。
Java并不采用引用计数法来判断对象是否已"死",而采用"可达性分析"来判断对象是否存活。
此算法的核心思想为 :
以下图为例:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:
优点:
克服了基于引用计数的两个缺点:空间利用率低和循环引用问题。
缺点:
系统开销大,遍历一次可能比较慢。
将死亡对象标记出来之后就可以进行垃圾回收操作了。
"标记-清除"算法是最基础的收集算法。
算法分为"标记"和"清除"两个阶段 :
"标记-清除"算法的不足主要有两个 :
后续的收集算法都是基于这种思路并对其不足加以改进而已。
"复制"算法是为了解决 “标记-清理” 的内存碎片问题。
这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
复制算法的缺点:
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
注意:
标记整理算法也涉及到对象的复制,相比于复制算法,解决了空间利用率低的问题,但是仍然没有解决复制搬运元素开销大的问题。
虽然标记整理算法同样需要复制, 但是标记整理算法通常需要复制(移动)较少的对象,因为老年代的对象生命周期较长,一些对象在不同的垃圾回收周期中仍然存活,不需要频繁地移动。相比之下,复制算法在每次垃圾回收时都要复制大部分对象。
针对对象进行分类,(根据年龄划分)一个对象熬过一轮 GC 扫描成为涨了一岁,针对不同的对象,采用不同的方案。
新生代中使用复制算法,老年代中使用标记整理算法。
现在的商用虚拟机 (包括 HotSpot 都是采用复制算法来回收新生代)
当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot 默认 Eden 与 Survivor 的大小比例是8 : 1,也就是说 Eden:Survivor From : Survivor To = 8:1:1。
所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
基本经验规律:一个对象越老继续存活的可能性越大,(要死早死了),所以老年代的扫描频率远远低于新生代,老年代中使用标记整理的方式进行回收。
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
哪些对象会进入新生代?哪些对象会进入老年代?
面试题 : 请问了解 Minor GC 和 Major GC 么,这两种 GC 有什么不一样吗?
STW(Stop-The-World)是垃圾回收中的一种现象,它表示在某些情况下,应用程序的所有线程都会被暂停,以便执行垃圾回收操作。STW 是为了确保垃圾回收器可以安全地执行清理和整理内存的工作,而不会受到应用程序线程的干扰。
为了确保垃圾回收操作的正确性和安全性。尽管 STW 会导致应用程序线程的暂停,但它是必要的,因为在垃圾回收过程中需要满足以下条件:
一致性:在执行垃圾回收期间,垃圾回收器需要准确地知道哪些对象是活动的,哪些是垃圾的。如果在执行垃圾回收时,应用程序线程仍然可以创建、修改或引用对象,那么垃圾回收器将无法确定对象的状态,可能会导致对象被错误地清理或保留。
引用更新:在垃圾回收期间,垃圾回收器需要更新对象之间的引用关系,以确保引用的正确性。如果应用程序线程在垃圾回收期间继续运行,可能会导致引用关系混乱,垃圾回收器无法正确地更新引用。
内存整理:在垃圾回收中,可能会发生内存整理操作,例如对象的移动,以减少内存碎片或提高内存利用率。如果应用程序线程继续操作内存,可能会导致内存整理的不一致性,从而破坏程序的状态。
垃圾对象清理:STW 允许垃圾回收器安全地删除不再被引用的垃圾对象,而不会涉及到应用程序线程对这些对象的操作。
minor GC (新生代垃圾回收)和 major GC (老年代垃圾回收)都会触发 STW。
垃圾收集器就是内存回收的具体实现。
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:
上图展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。
所处的区域,表示它是属于新生代收集器还是老年代收集器。在讲具体的收集器之前我们先来明确三个概念:
注意:这里的并行和并发只是指 GC 时的并行和并发。
吞 吐 量 = 运 行 用 户 代 码 时 间 /(运 行 用 户 代 码 时 间 +垃 圾 收 集 时 间)
例如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
为什么会有这么多垃圾收集器?
自从有了 Java 语言就有了垃圾收集器,这么多垃圾收集器其实是历史发展的产物。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
缺点:
CMS收集器对CPU资源非常敏感
在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS收集器会产生大量空间碎片
CMS 是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC。
G1(Garbage First)垃圾回收器是用在 heap memory 很大的情况下,把heap划分为很多很多的 region 块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
G1垃圾回收器回收 region 的时候基本不会STW,而是基于 most garbage 优先回收(整体来看是基于"标记-整理"算法,局部(两个 region 之间)基于"复制"算法) 的策略来对 region 进行垃圾回收的。
一个region有可能属于Eden,Survivor 或者 Tenured 内存区域。图中的 E 表示该 region 属于 Eden 内存区域,S 表示属于 Survivor 内存区域,T 表示属于 Tenured 内存区域。图中空白的表示未使用的内存空间。
G1垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region 大小的50%的对象。
年轻代垃圾收集:
在 G1 垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把 Eden 区和 Survivor 区的对象复制到新的 Survivor 区域。
如下图:
老年代垃收集
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟 CMS 垃圾收集器一样,但略有不同:
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的 CMS 收集器。 如果你的应用追求低停顿,G1 可以作为选择;如果你的应用追求吞吐量,G1并不带来特别明显的好处。
一个对象的一生:我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区(S1 区),居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每次GC加一岁)然后被回收了。