对比C/C++没有垃圾自动回收机制,从而需要开发人员自己释放内存。如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源, 最终可能会导致内存溢出。
而在Java语言中,为了让程序员更加专注代码本身的实现,不用过多的考虑内存释放的问题,从而有了自动的垃圾回收机制。
即使这样,对于垃圾回收也是很有必要去深入了解掌握。
什么是垃圾回收
程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存 资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了。
垃圾寻找的算法
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 引用计数法 | 给对象添加一个引用计数器,每当一个地方引用这个对象,计数器+1;当引用失效时,则-1 | 判定效率高 | 不能解决对象之间循环引用的问题;频繁大量的引用变化,带来大量的额外运算 |
| 可达性分析 | 通过一系列的GC Roots作为起始点,从这些节点向下搜索,当GC Roots到某个对象不可达时,这个对象就是可回收的 | 更加精确和严谨,可以解决对象之间循环引用的问题 | 实现复杂,需要分析大量的数据,消耗大量时间 |
引用计数法
引用计数法会给对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器的值+1,当引用失效时,计数器-1。当计数器的值为0的时候,则判定该对象是垃圾。
引用计数法解决不了对象间的相互引用。
可达性分析
可达性分析算法是从根上开始搜索,当一个程序启动后,马上需要的这些个对象叫做根对象,然后根据这条线一直找到那些所有的对象。
GC Roots的对象有以下几种
- 虚拟机栈中引用的对象
- 方法区静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈JNI引用的对象
垃圾清理的算法
标记-清除算法
首先从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,标识出所有要回收的对象。然后回收器检查堆中每一个对象,并将所有未被标记的对象进行回收。
缺点
- 标记,清除的效率都不高
- 清除后产生大量的内存碎片,空间碎片太多会导致分配大的内存对象无法找到连续的内存空间从而再次触发GC
标记-整理算法
与标记清除类似,但不是在标记完成之后进行直接清除,而是将存活的对象移动一端,然后直接清除掉边界外的对象
优点
- 解决了内存不连续的问题
缺点
- 效率低下,不仅需要标记所有的存活对象,还需要标记所有存活对象的引用地址
标记-复制算法
将内存分为两块相同大小的区域,每次在其中一块区域中进行内存分配。当这块区域占满时,则会将存活的对象复制到另外一块区域中,并清除当前的这块区域
优点
- 简单高效
缺点
- 浪费一半的内存区域空间
分代搜集算法
分代搜集就是根据对象的存活周期将内存分为新生代和老年代。
- 新生代对象朝生夕死,每次搜集都有大量对象消亡。所以采用标记-复制算法。
- 老年代对象生存几率高,对象存活较高,此时选择标记-清除和标记-整理则比较合适。
新生代
在新生代中,分为一个Eden区,2个幸存区(Survivor)。当新生代中的Eden区分配满的时候,就会触发新生代的GC。具体过程图下:
- 在Eden区执行一次GC之后,存活的对象会被移动到其中一个Survivor区。(from区域)
- Eden区再次GC,这时会采用复制算法,将Eden和from区一起清理。存活的对象将会被复制在to区,然后只需要清空from区就可以了。
在这个过程中,总会有一个Survivor分区是空置的。Eden、from、to的默认比例是8:1:1,所以只会造成10%的空间浪费,这个比例是由参数 -XX:SurvivorRatio 进行配置的(默认为8)
老年代
进入到老年代的对象途径如下:
- 当对象的年龄达到一定的次数会晋升至老年代
- 新生代回收存活的对象大于10%时,因Survivor空间不够存储,对象会直接在老年代上分配
- 超出一定大小的对象会直接分配在老年代上
GC垃圾收集器
java虚拟机针对新生代和老年代提供了多种不同的GC垃圾收集器。
| 名称 | 分类 | 作用位置 | 使用算法 | 特点 | 使用场景 |
|---|---|---|---|---|---|
| Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
| ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
| Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
| Serial Old | 串行 | 老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
| Paraller Old | 并行 | 老年代 | 标记-整理(压缩)算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
| CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
| G1 | 并发、并行 | 新生代、老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 响应速度优先 |
新生代收集器
Serial收集器(单线程、复制算法)
处理GC的只有一条线程,并且在垃圾回收的过程中暂停一切用户线程。
ParNew收集器(Serial+复制算法)
ParNew是Serial的多线程版本。由多条GC线程并行地进行垃圾清理。清理过程依然要停止用户线程。
Paraller Scavenge收集器(多线程复制算法)
也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量。主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
老年代收集器
Serial Old 收集器(单线程标记整理算法)
与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。年轻代的 Serial,使用复制算法。老年代的 Old Serial,使用标记-整理算法。
Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 收集器(多线程标记清除算法)
并发标记清除(Concurrent Mark Sweep,CMS)垃圾回收器,是一款致力于获取最短停顿时间的收集器,使用多个线程来扫描堆内存并标记可被清除的对象,然后清除标记的对象。
工作步骤
- 初始标记: 会导致用户线程的暂停;仅标记出GC Roots能直接关联到的对象
- 并发标记: 进行GC Roots遍历的过程,寻找出所有可达对象
- 重新标记: 会导致用户线程的暂停;并发标记结束后,启动重新标记,处理在并发标记过程中,可能存在的引用变化
- 并发清除: 清除垃圾
CMS中存在的问题
- 浮动垃圾: 当重新标记完成之后,并发清除之前导致的垃圾对象无法被当前清除,只能等到下次操作进行处理
- 内存碎片: 没有整理存活对象的一个过程,导致内存碎片过多
新生代和老年代收集器(G1)
相比于 CMS 收集器,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
- G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
G1收集器中的堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous(巨大的),这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
Minor GC与Full GC
- Minor GC通常在新生代内存区域满了的情况下,进行触发
- Full GC通常在老年代和方法区满了的情况下,进行触发