概述
上一篇文章我们已经了解了 Java 的这几块内存区域。对于垃圾回收来说,针对或者关注的是 Java 堆这块区域。因为对于程序计数器、栈、本地方法栈来说,他们随线程而生,随线程而灭,所以这个区域的内存分配和回收可以看作具备确定性。对于方法区来说,分配完类相关信息后内存大小也基本确定了,加上在 JAVA8 中引入的元空间,所以这个部分也不用关注。
目的
对于堆中存储的那些不用的或者死掉的对象进行清理。
如何判断对象已死?
-
引用计数器
每当有一个地方引用它时,计数器的值就加一,如果引用失效时,计数器值减一。简单高效,但是没办法解决循环引用的问题。
-
可达性分析算法
这个算法的基本思路是通过一系列名为 GC ROOTS 的对象作为起始点,从这些节点开始向下搜索。当一个对象到 GC ROOTS 没有任何引用链时,则证明此对象不可用。可以作为 GC ROOTS 的对象包括下面几种?
- 方法里面引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法中引用的对象。
更为简单的理解方式为:
- 当前各线程执行方法中的局部变量(包括形参)引用的对象
- 已被加载的类的 static 域引用的对象
- 方法区中常量引用的对象
- JNI 引用
如何回收
当前的商业虚拟机的垃圾收集都采用分代垃圾回收的算法,这种算法并没有什么新的思想。只是根据对象的存活周期将不同的内存划分为几块。一般是把 Java 堆分为新生代
和老年代
,根据新生代和老年代存活时间的不同采取不同的算法,使虚拟机的 GC 效率提高了很多。新生代采用复制算法,老年代采用标记-清除或者标记-整理算法。
回收算法
-
标记-清除
算法分为
标记
和清除
两个阶段,首先要标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 -
复制
为了解决效率问题,复制算法出现了,将内存按容量划分为大小相等的两块,每次只使用其中的一块。清除后将活着的对象复制到另外一块上面。简单高效。现在的商业虚拟机都采用这种收集算法来回收
新生代
。因为新生代
中的对象98%都是朝生夕死的,所以并不需要按1:1划分内存,而是按8:1:1分为 Eden,survivor,survivor。每次只使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 上。8:1:1 是在复制算法的基础上改良而来的。
当 Survivor 空间不够用时,需要依赖
老年代
进行分配担保。(图片来源于网络)
-
标记-整理
标记-整理算法和标记-清除算法的标记过程一样,后序有一个对内存进行整理的动作。和标记-整理算法一样,比较适合要清除对象不多的情况。复制算法在对象存活率较高时就要执行较多的复制操作,效率会变的很低。而且如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对对象 100% 存活的极端情况,所以
老年代
一般不选复制算法,而选择标记-清除或者标记-整理算法。
新生代为什么按 8:1:1 分
因为新建出来的对象 98% 都是朝生夕死的,真正能在一轮 GC 之后留下的非常少,所以按照复制算法最初的 5:5 分是非常浪费空间的。所以将新生代分为 8:1:1 的 Eden survivor survivor。对象优先在 Eden 中分配,大多数情况下,对象在新生代 Eden 中分配,当 Eden 没有足够的空间进行分配时,虚拟机将发起一次 Minor GC
。
在 GC 开始的时候,对象只会存在于 Eden 区和名为 From 的 Survivor 区,名为 To 的 Survivor 区是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到 To,而在 From 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过 -XX:MaxTenuringThreshold 来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到 To 区域。
经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From ,新的 From 就是上次 GC 前的 To 。
另外还有两条对象分配策略是:
- 大对象直接进入老年代,大对象指的是那些需要连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。直接进入老年代避免了大对象在 Eden 区和 Survivor 区之间发生大量的内存拷贝。
- 长期存活的对象将进入老年代,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在 Eden 出生并经过一次 Minor GC 后仍然存活,并且能被 Survivor 容纳就会被移动到 Survivor 中,并且年龄增加 1。当年龄达到某个阙值(默认为 15)时,就会晋升到老年代。
对于新生代,需要选择速度比较快的垃圾回收算法,因为新生代的垃圾回收是频繁的。所以选择复制算法。
对于老年代,需要考虑的是空间,因为老年代占用了大部分堆内存,而且针对该部分的垃圾回收算法,需要考虑到这个区域的垃圾密度比较低。所以选择标记清除和标记整理算法。
看完了上面的知识点那么我们对什么时候进行 Minor GC,什么时候进行 Full GC 也就明白了。
Eden 满了进行 Minor GC,升到老年代的对象大于老年代剩余空间进行 Full GC。
在讲 Minor GC 和 Full GC
大家在通过这张图来了解一下堆内存的划分,堆内存分为 Eden,Survivor 和 Old 空间嘛,如下图所示:
在年轻代进行的内存回收称为 Minor GC,对老年代进行的内存回收称为 Major GC,而 Full GC 是对整个堆进行的。Major GC的速度一般会比Minor GC慢10倍以上。
下面列出几种进行 Full GC 的条件。
-
System.gc()
这个方法的调用是建议 JVM 进行 Full GC。
-
老年代空间不足
老年代只有新生代对象转入或者新建大对象的时候才会出现不足的情况,如果执行 Full GC 后空间仍然不足,那么则会抛出 OOM error 了。
-
永久代空间不足
方法区中存放的是一些 class 的信息,常量,静态变量等数据,当系统中要加载的类,反射的类或调用的方法较多时,永久代可能会被填满。在为配置为采用 CMS GC 的情况下也会执行 Full GC。
-
CMS GC promotion failed 和 concurrent mode failure
对于采用 CMS 进行老年代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure 两种状态,这两种状态可能会触发 Full GC。
-
HandlePromotionFailure
在发生 Minor GC 之前,虚拟机会先检查老年代的最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,那么 Minor GC 时可以确保安全的,否则不成立。虚拟机会查看 HandlePromotionFailure 设置是否允许担保。如果允许,会检察老年代的连续可用空间是否大于历次晋升的平均大小,如果大雨,尝试着进行一次 Minor GC,尽管有风险,如果小于或者 HandlePromotionFailure 设置为不允许冒险,则要进行一次 Full GC。
垃圾收集器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。下面介绍基于 HotSpot 虚拟机中的垃圾收集器。对于垃圾收集器,大家有个概念就可以了,没有必要去深究垃圾收集器的底层原理,当然如果有余力,了解底层原理当然是最好的。
-
Serial 收集器
最早的垃圾收集器,回收新生代,单线程。这里的单线程不仅仅说明它只会使用一个 CPU 或者一条收集线程去完成垃圾收集工作,重要的是,在进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)。
-
ParNew 收集器
新生代垃圾回收,ParNew 收集器其实就是 Serial 收集器的多线程版本,在收集算法,Stop The World 和对象分配规则,回收策略上都与 Serial 相同。ParNew 在单核甚至双核 CPU 上的表现不如 Serial,更多的 CPU 才能体现他的优点。
-
Parallel Scanvnge 收集器
新生代垃圾回收,采用复制算法,关注吞吐量,不关注停顿时间。停顿时间越短就越适合需要于用户交互的程序,良好的响应速度能提升用户的体验。高吞吐量则可以最高效率地利用 CPU 时间,尽快完成运算任务,适合在后台运算而不需要太多交互的任务。
-
Serial Old 收集器
Serial 的老年代版本,单线程,使用标记-整理算法。
-
Parallel Old 收集器
Parallel New 的老年代版本,使用标记-整理算法。
-
CMS 收集器
CMS 是一种以获取最短回收停顿时间为目标的收集器,注重响应速度。基于标记-清除算法实现的。不同于其他收集器的全程 Stop The World,CMS 会有两次短暂的 Stop The World,垃圾收集和工作线程并发执行。整个过程分为 4 个步骤:
- 初始标记(Stop The World),标记 GC Roots 能关联到的对象。
- 并发标记
- 重新标记(Stop The World)
- 并发清除
-
G1 收集器
基于
标记-整理
实现。可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,新生代和老年代都可以回收。
CMS 收集器
目前 CMS 时最常用的收集器(JDK8 的应用一般都切换到了 G1 收集器了)。这个收集器和其他收集器的区别是不会全程 Stop-The-World,可以做到垃圾回收线程和应用程序线程同时运行。
对于许多程序来说,吞吐量不如响应时间来的重要。通常年轻代的垃圾收集不会停顿多长时间,但是老年代的垃圾回收,虽然不频繁,但是可能会导致长时间的停顿,尤其是当堆内存比较大的时候。为了解决这个问题,HotSpot 虚拟机提供了 CMS 收集器,也叫做低延时收集器。
新生代使用 CMS 收集器
和其他新生代并行收集器一样,并行清除-> stop-the-world->复制。
老年代使用 CMS 收集器
在老年代的垃圾收集过程中,大部分收集任务和应用是并发执行的。
CMS 会有一段小停顿 stop-the-world,叫做初始标记阶段,用于确定 GC Roots。然后是并发标记阶段,标记 GC Roots 可达的存活对象,由于这个阶段应用也在运行,所以并发标记结束后,并不能标记所有的存活对象,所以需要在此停顿,再次标记阶段,遍历在并发标记阶段应用程序修改的对象,这次停顿会比较长,会使用多线程并行执行来增加效率。
再次标记结束后,接下来进入并发清理阶段。
CMS 是唯一不进行压缩的收集器,就是它使用标记清除算法。