在第 6 讲中咱们说到 Java 虚拟机的内存结构,提到了这部分的规范实际上是由《Java 虚拟机规范》指定的,每一个 Java 虚拟机可能都有不一样的实现。其实涉及到 Java 虚拟机的内存,就不得不谈到 Java 虚拟机的垃圾回收机制。由于内存老是有限的,咱们须要一个机制来不断地回收废弃的内存,从而实现内存的循环利用,这样程序才能正常地运转下去。html
比起 Java 虚拟机的内存结构有《Java 虚拟机规范》规定,垃圾回收机制并无具体的规范约束。因此不少时候不一样的虚拟机有不一样的实现方式,下面所说的垃圾回收都是以 HotSpot 虚拟机为例。java
要进行垃圾回收,最为重要的一个问题是:判断谁是垃圾?算法
联想其平常生活中,若是一个东西常常没被使用,那么这个对象能够说就是垃圾。在 Java 中也是如此,若是一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收。数据结构
根据这个思想,咱们很容易想到使用引用计数的方法来判断垃圾。在一个对象被引用时加一,被去除引用时减一,这样咱们就能够经过判断引用计数是否为零来判断一个对象是否为垃圾。这种方法咱们通常称之为「引用计数法」。eclipse
上面的这种方法虽然简单,可是其存在一个致命的问题,那就是循环引用。jvm
A 引用了 B,B 引用了 C,C 引用了 A,它们各自的引用计数都为 1。可是它们三个对象却从未被其余对象引用,只有它们自身互相引用。从垃圾的判断思想来看,它们三个确实是不被其余对象引用的,可是此时它们的引用计数却不为零。这就是引用计数法存在的循环引用问题。jsp
而现今的 Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。其大概的过程是这样:从 GC Root 出发,全部可达的对象都是存活的对象,而全部不可达的对象都是垃圾。性能
能够看到这里最重要的就是 GC Root 这个集合了,其实 GC Root 就是一组活跃引用的集合。可是这个集合又与通常的对象集合不太同样,这些集合是通过特地筛选出来的,一般包括:学习
简单地说,GC Root 就是通过精心挑选的一组活跃引用,这些引用是确定存活的。那么经过这些引用延伸到的对象,天然也是存活的。优化
到这里,咱们了解了什么是垃圾以及 JVM 是如何判断垃圾对象的。那么识别出垃圾对象以后,JVM 是如何进行垃圾回收的呢?这就是咱们下面要讲的内容:如何进行垃圾回收?
垃圾回收算法简单地说有三种算法:标记清除算法、复制算法、标记压缩算法。
标记清除算法。从名字能够看到其分为两个阶段:标记阶段和清除阶段。一种可行的实现方式是,在标记阶段,标记全部由 GC Root 触发的可达对象。此时,全部未被标记的对象就是垃圾对象。以后在清除阶段,清除全部未被标记的对象。标记清除算法最大的问题就是空间碎片问题。若是空间碎片过多,则会致使内存空间的不连续。虽然说大对象也能够分配在不连续的空间中,可是效率要低于连续的内存空间。
复制算法。复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。以后清除正在使用的内存块中的全部对象,以后交换两个内存块的角色,完成垃圾回收。该算法的缺点是要将内存空间折半,极大地浪费了内存空间。
标记压缩算法。标记压缩算法能够说是标记清除算法的优化版,其一样须要经历两个阶段,分别是:标记结算、压缩阶段。在标记阶段,从 GC Root 引用集合触发去标记全部对象。在压缩阶段,其则是将全部存活的对象压缩在内存的一边,以后清理边界外的全部空间。
对比一下这三种算法,能够发现他们都有各自的优势和缺点。
标记清除算法虽然会产生内存碎片,可是不须要移动太多对象,比较适合在存活对象比较多的状况。而复制算法虽然须要将内存空间折半,而且须要移动存活对象,可是其清理后不会有空间碎片,比较适合存活对象比较少的状况。而标记压缩算法,则是标记清除算法的优化版,减小了空间碎片。
试想一下,若是咱们单独采用任何一种算法,那么最终的垃圾回收效率都不会很好。其实 JVM 虚拟机的建造者们也是这么想的,所以在实际的垃圾回收算法中采用了分代算法。
所谓分代算法,就是根据 JVM 内存的不一样内存区域,采用不一样的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只须要复制少许对象,即可完成垃圾回收,而且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不须要移动太多的内存对象。
试想一下,若是没有采用分代算法,而在老年代中使用复制算法。在极端状况下,老年代对象的存活率能够达到100%,那么咱们就须要复制这么多个对象到另一个内存区域,这个工做量是很是庞大的。
在这里咱们再深刻地聊一聊新生代里采起的垃圾回收算法。如咱们上面所说,新生代的特色是存活对象少,适合采用复制算法。而复制算法的一种最简单实现即是折半内存使用,另外一半备用。但实际上咱们知道,在实际的 JVM 新生代划分中,却不是采用等分为两块内存的形式。而是分为:Eden 区域、from 区域、to 区域 这三个区域。那么为何 JVM 最终要采用这种形式,而不用 50% 等分为两个内存块的方式?
要解答这个问题,咱们就须要先深刻了解新生代对象的特色。根据IBM公司的研究代表,在新生代中的对象 98% 是朝生夕死的,因此并不须要按照1:1的比例来划份内存空间。因此在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。
经过这种方式,内存的空间利用率达到了90%,只有10%的空间是浪费掉了。而若是经过均分为两块内存,则其内存利用率只有 50%,二者利用率相差了将近一倍。
分代思想按照对象的生命周期长短将其分为了两个部分(新生代、老年代),但 JVM 中其实还有一个分区思想,即将整个堆空间划分红连续的不一样小区间。
每个小区间都独立使用,独立回收,这种算法的好处是能够控制一次回收多少个区间,能够较好地控制 GC 时间。
到这里咱们基本上把 JVM 的垃圾回收都将清除了,从一开始什么是垃圾,到以后如何判断垃圾,到如何回收垃圾,到垃圾回收的两个重要思想:分代思想、分区思想。经过这么一个脉络,咱们了解了垃圾回收的总体归纳。在下面的章节中,咱们将深刻介绍这其中的细节。
若是只是看,其实没法真正学会知识的。为了帮助你们更好地学习,我建了一个虚拟机群,专门讨论学习 Java 虚拟机方面的内容,每周针对我所发文章进行讨论答疑。若是你有兴趣,关注「Java技术精选」公众号,经过右下角菜单「入群交流」加我好友,小助手会拉你入群。