可达性分析
主要是用来解决哪些对象是不可用的,可以被回收。
以一系列称为GC Roots的对象为起点,若对象到GC Roots之间没有任何引用关系,则认为该对象是不可用的,可以被回收.
具体可作为GC Roots的对象如下(不是全部):
- **所有Java线程中当前栈帧的引用(例如局部变量),也就是活着的线程,**这个就是我们通常意义上Java代码new一个对象引用,这个对象引用所在的地方.
- 所有ClassLoader
- 所有全局变量
对于classloader和全局变量,因为其一定伴随着应用的整个生命周期,所以能从此处开始遍历。对于线程中局部变量,因为此时一定处于活跃状态,所以此处一定能遍历出去
常见的清除算法
主要是进行垃圾回收时怎么具体去操作内存,主要分为如下几种类型,但实际上虚拟机通常都是在不同的代上使用不同的算法组合实现。
标记-清除算法
首先标记所有需要被回收的对象,当标记完成之后,再统一清理。CMS收集器使用该算法实现。
该算法存在两个问题:
-
标记和清除的效率不高
-
清除的时候容易导致内存不连续, 产生内存碎片
复制算法
为了解决标记-清除算法时导致内存碎片的产生,可以将内存划分为相等的两部分(不一定相等,此处方便描述),每次只使用其中的一块,当这一块用完之后,将其中存活的对象复制到另外一份,同时清空这份内存。
HotSpot中新生代Eden,S0,S1的划分就是依据此算法实现,其划分比例默认是8:1。
该算法对于那些存活时间短的对象清除效率高,因为在复制的时候大部分对象都已经不可用了,所以复制的对象较少。但是对于存活时间长的对象由于需要多次进行复制操作,效率会变低。
该算法还有另外一个弊端, 需要额外占用一部分内存。
标记-整理算法
上述的复制算法对于存活时间长的对象不友好。该算法的标记过程与标记-清除算法一样,但是在清理的时候,将存活的对象向一端移动,然后清理掉在该边界之外的所有对象。
一般老年代使用此算法实现。
常见垃圾收集器
在JDK8中server模式下默认的是: Parallel Scavenge + Parallel Old(其实就是Parallel并行收集器,分为老年代和新生代)
下面具体分析JVM中几种垃圾收集器
Serial 串行收集
单线程执行。会stop the world,也就是在进行收集时,会暂停所有其他用户线程。
该收集器包括两个版本,一个是新生代收集器,使用复制算法;一个是老年代,使用标记-整理算法
Parallel 并行收集
并行执行。目的是达到一个可控制的吞吐量
其主要特点如下:
- 并行执行
- 能自适应堆中各个区域的大小关系
- 目的是达到一个可控制的吞吐量
该收集器包括两个版本:
一个是新生代收集器, Parallel Scavenge,使用复制算法实现;
一个是Parallel Old, 使用标记-整理算法实现。
CMS
concurrent mark sweep, 是一个和用户线程并行执行的垃圾收集器。
目的是获取最短回收停顿时间。
基于标记-清除算法实现。
其收集流程如下:
-
初始标记 该过程需要stop the world, 仅仅标记GC ROOTS直接关联的对象,所以很快
-
并发标记 该过程和用户线程并行执行,占用时间较长, 进行GC roots tracing, 也就是对象的可达性分析
-
重新标记 该过程需要stop the world,标记在并发标记阶段新产生的需要回收的对象,占用时间较短
-
并发清除 该过程进行清除,占用时间较长
整个过程中占用时间最长的是并发标记和并发清除两个阶段。
CMS收集器的缺点如下:
- 由于使用了标记-清除算法,会导致内存碎片产生
- 需要预留一部分内存空间给用户,用以进行并发标记
G1收集器
G1 的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量。
G1 和 CMS 相同的地方在于,它们都属于并发收集器,在大部分的收集阶段都不需要挂起应用程序。区别在于,G1 没有 CMS 的碎片化问题(或者说不那么严重),同时提供了更加可控的停顿时间。
如果应用使用了较大的堆(如 6GB 及以上)而且还要求有较低的垃圾收集停顿时间(如 0.5 秒),此时G1是个较好的选择。
在内存划分上,之前介绍的分代收集器将整个堆分为年轻代、老年代和永久代,每个代的空间是确定的。而 G1 将整个堆划分为一个个大小相等的小块(每一块称为一个 region),每一块的内存是连续的。
和分代算法一样,G1 中每个块也会充当 Eden、Survivor、Old 三种角色,但是它们不是固定的,这使得内存使用更加地灵活。这里的eden,survivor和old是一个标签,只是一个逻辑表示,不是物理表示。如下图所示, E, S, O和其他的收集器不一样, 并不是连续的, 可以间隔存在。
在JVM中,存活下来的对象被虚拟机从一个region里被移动到另一个region中。这些小块(region)的回收是并行回收的,期间其他的应用线程照常工作。
G1中还存在一种特殊类型的region,叫Humongous, 主要是用来存储那些比标准块大50%,甚至更大的对象。这些大对象被保存到一整块连续的区域。需要注意的是,该区域目前还没有被优化到最佳,所以尽量不要创建很大的对象。
G1收集器主要有如下几个特点:
- 并行与并发。 可以通过多个CPU使用并发的方式来进行工作
- 空间整合。 从整体上看G1是基于标记-整理算法进行清除, 但是在两个region之间, 是基于复制算法进行清除
-
可预测的停顿。这是G1的一大特点。用户可设置在一个时间段内, 消耗在垃圾收集上的时间不能超过多长时间。具体实现是因为G1在进行垃圾回收时,首先会对每个region进行估值(主要是根据回收能获取到的空间大小以及回收需要时间的经验值), 并维护一个优先列表, 这样每次在进行回收的时候,根据用户设置的回收时间, 优先回收价值较大的那些region(也就是garbage first)。
现在有一个问题, 如果某个对象被其他不在该region的对象所引用, 那么在做对象的可达性分析时, 难道需要扫描整个堆吗?
明显不可能每次扫描整个堆, 这样效率会很低。G1(包括CMS也是一样)会为每个region维护一个remembered set, 当对该region内的引用对象有写操作时, 会将相关的引用信息记录到remembered set中, 在可达性分析的根节点GC Roots加入这些remembered set, 就能避免全堆扫描。
G1收集器的工作流程和CMS有些类似, 具体如下:
- 初始标记。和CMS类似, 只标记GC Roots直接关联的对象,所以时间很短。需要stop the world
- 并发标记。对对象进行可达性分析,耗时较长, 和用户线程并发执行
- 最终标记。对并发比较阶段由用户线程新产生的对象进行重新标记,并和初始标记的结果进行合并。 需要stop the world。
-
筛选回收。对各个region进行估值, 然后根据用户设置的停顿时间制定回收计划。由于只是回收部分region且暂停用户线程可以提高回收效率, 所以一般上也是需要stop the world的。
G1收集器常见的一些参数说明如下:
- ‐XX:+UseG1GC 使用G1收集器
- ‐XX:G1HeapRegionSize 指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
- ‐XX:G1NewSizePercent 新生代内存初始空间(默认整堆5%)
- ‐XX:G1MaxNewSizePercent 新生代内存最大空间
参考资料:
深入理解Java虚拟机