S1.垃圾回收器(Garbage Collector)介绍
前文讲到过执行引擎,了解到垃圾回收器是执行引擎的一部分。它的作用是自动管理应用程序在运行过程中所分配的内存。在Java中,我们申请内存是通过new关键字来进行的,而垃圾回收器则负责在程序运行过程中,自动释放那些不再使用的内存,以便于其他的内存需求。
需要注意的是,Java垃圾回收器虽然自动管理内存,但并不意味着我们可以不关心内存的使用。在Java程序中,如果存在内存泄漏(Memory Leak)等问题,则会影响程序的运行效率和稳定性。因此,在编写Java程序时,我们仍然需要关注内存的使用情况,避免出现内存泄漏等问题。
本文我们会重点讲解如何定位垃圾对象,如何回收垃圾对象和常用的垃圾回收器都有哪些。
![](https://i-blog.csdnimg.cn/blog_migrate/7b50700cd7c9fa5ad553ca46c8e598bc.jpeg)
S2.可达性分析算法
可达性算法是垃圾对象的定位算法,定位后对可以被垃圾回收器回收的对象做出标记,可达性算法是目前主流的虚拟机都采用的算法。
我们把没有被外界引用的对象被称为垃圾对象,它在周后的程序运行中不会被使用到,所以应当被垃圾回收器进行内存回收。
可达性算法是程序把所有的引用关系看作一张图,从一个一定不能被回收内存的节点,我们称为GC Roots开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。这就是可达性分析算法。
GC Roots,一般包括,虚拟机栈(栈帧中的本地变量表)中引用的对象、本地方法栈中 JNI(即一般说的 Native 方法)引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。
引用方式一般包括:强引用,默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收); 软引用,软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC); 弱引用,与软引用很类似,但在GC时一定会被GC回收;虚引用,只是用来得知对象是否被GC。
S3.回收算法
通过可达性分析算法,对垃圾对象做出标记,接下来就是要对这些对象进行回收,这就要用到回收算法。回收算法分为三类。标记-清除(Mark-Sweep)、复制(Copying)和标记-整理(Mark-Compact)。
S3.1.标记-清除(Mark-Sweep)
这个算法分为两个阶段,标记阶段和清除阶段,标记阶段,通过对应的垃圾定位算法,标记垃圾对象,然后在清楚阶段,直接清除掉被被标记的对象。
这个算法存在明显两个缺陷,一个是标记的时候需要对整个内存对象进行遍历,效率十分低下。另一个是,清除时会导致内存中出现很多不连续的空区,出现内存碎片问题,使得之后的内存使用效率降低。
S3.2.复制(Copying)
这个算法就是,通过对应的定位算法,定位到活跃的对象,将它们从一个内存空间复制到另一个内存空间中,然后再将原来的内存空间全部清除,这样没有被复制的对象就是垃圾对象,被清除掉了。
它解决标记-清除算法的两个问题,首先只有复制过程对内存的一次遍历,并且只复制了活跃对象,负担很小,优化了效率。然后由于复制到一个新的内存空间,不会出现内存碎片问题。
但是缺点也同样明显,就是需要两倍内存空间,是明显的空间换时间的方式。且存活对象增多的话,算法的效率会大大降低。
S3.3.标记-整理(Mark-Compact)
标记-整理算法分为两个阶段,标记阶段和整理阶段,与标记-清除算法类似,标记阶段通过对应的垃圾定位算法,标记垃圾对象。然后是整理阶段,清除垃圾对象并且把存活对象进行整理按顺序排放。
它解决了标记-清除算法的空间碎片问题和复制算法的空间占用问题,但是由于两次遍历和对象的移动,使得效率也有所下降。
S3.4.分代回收
分代回收,不是单独的一种回收算法,应该是一种思想。因为它需要基于以上介绍的三种算法。分代回收的本质是把对象按照存活相对时间的长短进行划代,分别存储在不同的区域。并且根据不同的区域执行不同的算法。
在内存结构篇章我们讲过堆内存的结构,我们知道JVM堆内存分为新生代和老年代,这就是分代方式,存活时间较长的存在老年代,存活时间较短的存在新生代。
![](https://i-blog.csdnimg.cn/blog_migrate/ce980eeb21d057029e46252cde768c4c.png)
新生代中采用的是改良后的复制算法,我们知道新生代包括一个伊甸园区和两个幸存者区,绝大多数情况新创建的对象会被放在伊甸园区,然后采用复制算法,把幸存的对象存入其中幸存者区,之后的回收会将上一次使用的幸存者区和伊甸园区的幸存对象用复制算法另一幸存者区,值得注意的是总有一个幸存者区保持为空,用于复制。根据大量数据统计,绝大部分对象会很快被回收,所以伊甸园区和两个幸存者区的空间比例为8:1:1。
当多次回收后,存活周期很长的对象,被认定为不容以被回收,将会被转移到老年代,老年代则采用了标记-整理或标记-清除算法。
新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集;老年代中对象的存活率高,不适合采用复制算法,而且如果老年代采用复制算法,它是没有额外的空间进行分配担保的,因此必须使用标记/清理算法或者标记/整理算法来进行回收。
S3.4.1.次要垃圾回收(Minor GC)
Minor GC指的是发生在年轻代或者说新生代中的垃圾回收过程,也有人称其为young gc或者ygc。
S3.4.2.主要垃圾回收(Major GC)
Major gc指的是发生在老年代(Tenured space)中的垃圾回收过程,也有人称为old gc,o gc等.
S3.4.3.完全垃圾回收(Full GC)
Full GC这个概念是没有官方定义的,在通常意义上人们口中说的Full GC为一次特殊GC行为的描述,这次GC会回收整个堆的内存,包含老年代,新生代,源空间等。在gc.log中会发现在部分gc日志头中也有Full GC这样的字眼,是说在这次GC的全过程中所有用户线程都是处于暂停的状态。
S4.常用回收器
说完了定位算法和回收算法,大概了解了垃圾回收器的作用和原理,接下来讲解一下执行这些过程的常用垃圾回收器都有哪些。按照工作方式我们可以将其分为串行回收器、并行回收器和并发回收器,按照作用域,可以分为新生代回收器、老年代回收器和整理堆的。值得注意的时垃圾回收器工作时都会出现必须暂停其他所有的工作线程(Stop The World,STW)的问题。
![](https://i-blog.csdnimg.cn/blog_migrate/eeda0d481ef843f66ff2c55fdac86311.jpeg)
S4.1.串行回收器
串行意义不只是使用一个CPU或一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,只有这一个线程在工作,SWT时间也会比较长,直到它收集完成。它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。但在多核CPU上效率则不如并行回收器。
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。
S4.2.并行回收器
并行垃圾回收相对于串行,是通过多线程运行垃圾收集的。但是任然存在SWT,在多核CPU上只是由于多线程,有效缩短了STW时间,适合Server模式以及多CPU环境。其中包括ParNew、Parallel Scavenge和Parllel Old。
ParNew,Serial收集器的多线程版本,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改ParallelGCThreads设定。用于新生代收集,复制算法。
Parallel Scavenge是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew都一样。ParNew等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用cpu时间,尽快完成程序的运算任务可以设置最大停顿时间MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。用于新生代收集,复制算法。Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。
Parllel Old,和Serial Old收集器一样,工作在JAV虚拟机的老年代。这种垃圾收集器使用多线程和“标记-整理”算法。它在JDK 1.6中才开始提供。
S4.3.CMS(Concurrent-Mark-Sweep)回收器
在jdk1.5时期,HotSpot退出了一款在强交互应用中几乎认为有划时代意义的垃圾收集器,CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器线程与用户线程同时工作,CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验.目前很大一部分的Java应用集中在web网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求,它工作在CMS的垃圾收集算法采用标记-清除算法。它的回收过程分为4个阶段,分别为初始标记、并发标记、重新标记和并发清除
初始标记:标记一下GC Roots能直接关联到的对象,串行的,任务量不大,会很快,但会STW。
并发标记:通过跟可达算法,判断对象是否存活,做出标记,可以和用户线程并发执行,无STW。
重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会STW。
并发清除:清除对象,可以和用户线程并发执行。
CMS收集器的优点在于并发收集、低停顿。
但是CMS还远远达不到完美,首先CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降;其次由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉,可能出现并发模式故障(Concurrent Mode Failure),失败后而导致另一次Full GC的产生;还有就是,由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数来提供触发百分比,设置的过高将会很容易导致并发模式故障,性能反而降低;最后就是,CMS是基于“标记-清除”算法实现的收集器,会产生大量不连续的内存碎片。空间碎片太多时,如果无法找到一块足够大的连续内存存放对象时,将不得不提前触发一次Full GC。
S4.4.Garbage First(G1)回收器
G1收集器是JDK1.7提供的一个新收集器,,JDK9中默认的垃圾回收器,与CMS收集器相比,最突出的改进有两点,一点是基于“标记-整理”算法实现,不会产生内存碎片。第二点可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。其原理是采用了分区的思想,使用了区域划分和优先级区域回收机制,G1收集器并不采用新生代和老年代物理隔离的传统布局方式(仅在逻辑上划分新生代和老年代),而是将整个堆内存划分为2048个大小相同的独立区域块,每个区域块的大小根据堆的实际大小而定,整体被控制在1M-32M之间。G1收集器跟踪区域块中的垃圾堆积情况,并在后台维护一个优先级列表,每次根据设置的垃圾回收时间,回收优先级最高的区域,这样可以避免整个新生代或整个老年代的垃圾回收,使得STW的时间更短、更可控,同时在有限的时间内可以获得最高的回收效率,简单的说就是在有限的时间内能回收多少就回收多少。
G1的回收过程分为4各阶段:
初始标记,它标记了从GC Root开始直接可达的对象,和CMS一样是串行的,任务量不大,会很快,但会STW。
并发标记,从GC Roots开始对堆中对象进行可达性分析,找出存活对象
最终标记,标记那些在并发标记阶段发生变化的对象,将被回收,会SWT
筛选回收,首先对各个区域的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收划,回收一部分区域。
G1的有点还包括:独特的分代垃圾回收器,分代GC: 分代收集器, 同时兼顾年轻代和老年代;使用分区算法, 不要求年轻代或老年代的空间都连续;并行性,回收期间, 可由多个线程同时工作, 有效利用多核cpu资源;空间整理,回收过程中, 会进行适当对象移动, 减少空间碎片,可预见性,G1可选取部分区域进行回收, 可以缩小回收范围, 减少全局停顿。