介绍
Java虚拟机
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
垃圾回收
Java语言与C/C++语言最大的区别在于内存的管理,在C/C++中,内存的申请和释放都必须由程序员手动管理,而在Java语言中,程序员只需要关注对象的创建即可。虚拟机中包含了垃圾回收器,专门负责内存的回收。
垃圾回收是一种自动的内存管理机制。当内存中的对象不再需要时,就应该予以释放,以让出存储空间,这种内存资源管理,称为垃圾回收(garbage collection)。
垃圾回收包括两个问题需要解决:
收集:确定哪些对象已经不会再被使用到
回收:释放这些对象以回收内存
GC
垃圾回收机制是由垃圾收集器Garbage Collection GC来实现的,GC是后台的守护进程。它的特别之处是它是一个低优先级进程,但是可以根据内存的使用情况动态的调整他的优先级。因此,它是在内存中低到一定限度时才会自动运行,从而实现对内存的回收。这就是垃圾回收的时间不确定的原因。
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
什么时候触发回收
GC有两种类型:Minor GC和Full GC。
- Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 - Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用
上一次GC之后Heap的各域分配策略动态变化
什么对象需要回收
在对堆进行对象回收前,首先要判断哪些是无效对象。我们知道,一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收。
一般有两种判别方式:
- 引用计数法
每个对象都有一个计数器,当这个对象被一个变量或者另一个对象引用一次,该计数器加一;若该引用失效,则计数器减一。当计数器为0时,就认为该对象是无效对象。 - 可达性分析法(根搜索算法)
所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象。算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链(Reference Chain)”。当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,可以被回收的。
回收做了什么事
主要做了清理不使用的对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。例如新生代采用了复制算法,老年代采用了标记整理法。在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。
垃圾回收算法
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就+1,;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能在被使用。
优点
判定效率很高。
缺点
不会完全准确,因为如果出现两个对象相互引用的问题就不行了。
标记-清除算法
标记要清除的对象(存活对象、可回收、未使用)
标记
- 第一次标记。在可达性分析后发现对象到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;对有必要执行finalize()方法的对象,被放入F-Queue队列中。
- 第二次标记。GC将对F-Queue队列中的对象进行第二次小规模标记; 在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合。
清除
两次标记后,还在"即将回收"集合的对象将被统一回收。执行过程如下图:
优点
基于最基础的可达性分析算法,它是最基础的收集算法。
缺点
效率问题:标记和清除两个过程的效率都不高。
空间问题:标记清除后会产生大量不连续的内存碎片。这会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次垃圾收集动作。
应用场景
针对老年代的CMS收集器
复制算法
把内存划分为大小相等的两块,每次只使用其中一块
当一块内存用完了,就将还存活的对象复制到另一块上(而后使用这一块)
再把已使用过的那块内存空间一次清理掉,而后重复步骤2;
执行过程如下图:
优点
实现简单,运行高效。这使得每次都是只对整个半区进行内存回收,内存分配时也不用考虑内存脆皮等问题。
缺点
空间浪费:可用内存缩减为原来的一半,太过浪费(解决:可以改良,不按1:1比例划分)
效率随对象存活率升高而变低:当对象存活率较高时,需要进行较多复制操作,效率将会变低。
应用场景
Serial收集器、ParNew收集器、Parallel Scavenge收集器、、G1(从局部看)。
标记-整理算法
标记
标记过程与"标记清除"算法一样
整理
不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理存活对象端边界以外的内存。
执行过程如下图:
优点
不会像复制算法,效率随对象存活率升高而变低。
不会像标记-清除算法,产生内存碎片。
缺点
主要是效率问题:除像标记-清除算法的标记过程外,还多了需要整理的过程,效率更低。
应用场景
Serial Old收集器、G1(从整体看)
分代收集算法
弱代理论:
- 大多数分配了内存的对象并不会存活太长时间,在处于年轻代时就会死掉。
- 很少有对象会从老年代变成年轻代。
基于弱代理论,根据对象存活周期的不同将内存划分为几块,这样就可以根据各个年代的特点采用最适当的收集算法。
新生代
每次垃圾收集都有大批对象死去,只有少量存活,所以可采用复制算法。
老年代
对象存活率高,没有额外的空间可以分配担保,使用"标记-清理"或"标记-整理"算法。
执行过程如下图:
优点
可以根据各个年代的特点采用最适当的收集算法。
缺点
仍然不能控制每次垃圾收集的时间
应用场景
HotSpot虚拟机中全部垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留)
火车算法
火车算法也称列车算法,是一种更彻底的分区域处理收集算法,是对分代收集算法的一个有力补充。
在火车算法中,内存被分为块,多个块组成一个集合。为了形象化,一节车厢代表一个块,一列火车代表一个集合;火车与车箱都按创建顺序标号,每个车厢大小相等,但每个火车包含的车厢数不一定相等;每节车箱有一个被记忆集合,而每辆火车的记忆集合是它所有车厢记忆集合的总和;记忆集合由指向车箱中对象的引用组成,这些引用来自同一辆火车中序号较高的车箱中的对象,以及序号较高中的对象。垃圾收集以车厢为单位,整体算法流程如下:
选择标号最小的火车。
如果火车的记忆集合是空的, 释放整列火车并终止, 否则进行第三步操作。
选择火车中标号最小的车厢
对于车厢记忆集合的每个元素:如果它是一个被根引用引用的对象, 那么, 将拷贝到一列新的火车中去;如果是一个被其它火车的对象指向的对象, 那么, 将它拷贝到这个指向它的火车中去;假设有一些对象已经被保留下来了, 那么通过这些对象可以触及到的对象将会被拷贝到同一列火车中去; 如果一个对象被来自多个火车的对象引用, 那么它可以被拷贝到任意一个火车去;这个步骤中, 有必要对受影响的引用集合进行相应地更新。
释放车厢并且终止。
执行过程如下图:
优点
可以在成熟对象空间提供限定时间的渐近收集,而不需要每次都进行一个大区域的垃圾回收过程,即可以控制垃圾回收的时间,在指定时间内进行一些小区域的回收。
缺点
实现较为复杂,一些场景下可能性价比不高。
应用场景
JDK7后HotSpot虚拟机G1收集器采用类似的算法
垃圾回收器
收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。如图所示:
说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。
Serial 收集器
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
ParNew 收集器
可以认为是 Serial 收集器的多线程版本。
并行:Parallel
指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发:Concurrent
指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
Serial Old 收集器
收集器的老年代版本,单线程,使用标记 — 整理
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记—整理
CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记—清除 算法实现。
运作步骤:
- 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
- 并发标记(CMS concurrent mark):进行 GC Roots Tracing
- 重新标记(CMS remark):修正并发标记期间的变动部分
- 并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记—清除 算法带来的空间碎片。
G1 收集器
面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
总结
介绍了下Java虚拟机垃圾回收,了解Java虚拟机垃圾回收的几种常见算法,以及简单介绍了下垃圾收集器,这些都比较浅显的介绍,后续会再拿出一些篇幅进行深入分析。