垃圾回收主要是要解决3件事情:
在强引用的情况下已经“死”了的对象就需要回收,在非强引用的情况下视情况回收。在java里面,几乎所有的对象实例都是在堆上分配,所以垃圾收集器第一件事情就是要判断堆上的这些实例那些是“死去”的,那些还“活着”。判断对象是否存活主要有两种算法,一种是“引用计数算法”,一种是“可达性分析算法”。
“死去”的标准是:不可能再被任何途径使用的对象。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
Java 4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
引用类型 | 实现方式 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|---|
强引用 | Object obj = new Object() | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | SoftReference | 出现OOM之前被回收 | 对象缓存 | 出现OOM之前 |
弱引用 | WeakReference | GC发生时 | 对象缓存 | 下一次GC之前 |
虚引用 | PhantomReference | GC发生时 | 在这个对象被收集器回收时收到一个系统通知 | 下一次GC之前 |
“标记-清除”(Mark-Sweep)算法:算法分为“标记”和“清除”两个阶段:
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,适用于年轻代。
复制算法适用于每次GC后存活对象很好的情况下,比如HotSpot虚拟机中的新生代,据统计新生代的对象存活率是2%。只不过HotSpot虚拟机并不是将新生代直接对半划分,而是分成了Eden和Survivor区,区域默认比值是 Eden:Survivor0:Survivor1=8:1:1
,这样划分后新生代浪费空间就只有10%了。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion),分配担保会将Minor GC后存活的对象直接放到老年代中。
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,适用于老年代。
上面说的三种算法是垃圾回收的基础算法,但是在虚拟机实现的过程中,不可能只使用其中一种算法来完成垃圾收集,所有引入了分代收集的概念。它根据对象存活周期的不同将内存划分为几块不同的区域, 如图:
在新生代中,因为每次Minor GC后,只有少量存活,所以比较适合复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,所以比较适合“标记—清理”或者“标记—整理”算法。
新生代对象首先在Eden进行分配,当Eden满了过后触发Minor GC,然后将存活的对象放到S0区域,再清空Eden区。当Eden再次满了过后触发Minor GC,然后将存活对象放到S1区域,再清空Eden和S0区,如此循环。当survivor区域不足以放下所有存活对象或者对象分代年龄达到临界值时,会将对象放到老年代中。当老年代满了后,会触发Full GC。
在垃圾收集过程中,枚举根节点会导致所有Java线程停顿(“Stop The World”)。为了能尽量减少对应用影响,我们需要尽量减少Java线程停顿时间。在前面我们列举了那些对象是GC Roots,但是我们怎么能快速找到这些GC Roots呢?因为我们越快找到这些对象,那么Java线程停顿时间就越短。
现在主流Java虚拟机都是用的是 准确式GC ,所以Java线程停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置。比如:虚拟机栈的本地变量表中,我们只需要找到其中的引用对象就行了,而非引用对象是不会成为GC Roots的,如果我们每次GC都需要进行全栈扫描去查找GC Roots,那么将增加Java线程的停顿时间。
在HotSpot中,它使用了一种OopMap的数据结构来存储GC Roots的信息,这样,在枚举根节点的时候,就可以避免全栈扫描了。但是什么时候来记录这些信息呢?
HotSpot可以快速且准确地完成GC Roots枚举,但是可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高,所以就有了安全点(Safepoint)。程序只有运行到了安全点才会暂停下来,然后将变化的引用信息记录到OopMap中。
在HotSpot中方法调用、循环跳转、异常跳转等功能才能产生安全点。
当GC发生的时候,需要让所有的线程都到最近的安全点停下来。停顿方案有两种:
安全点可以解决正在执行中的线程到底安全点,记录对象引用信息。但是当线程处于Sleep或者Blocked状态的时候,线程无法响应JVM中断请求,所以安全点对这类线程就无效了,这时候就引入了安全区域(Safe Region)。
**安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。**我们也可以把Safe Region看做是被扩展了的Safepoint。
在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。
垃圾收集器就是内存回收的具体实现,主要有以下几种,以及组合方式:
Serial是一个单线程的 新生代 收集器,采用 复制算法 。Serial Old是一个单线程的 老年代 收集器,采用 标记-整理 算法。
Serial/Serial Old收集器运行示意图:
ParNew收集器起始就是Serial收集器的多线程版,是一个 新生代 收集器,采用 复制算法 。
ParNew/Serial Old收集器运行示意图:
Parallel Scavenge收集器是一个 新生代 的 并行收集器 ,使用 复制算法 。Parallel Old收集器是一个 老年代 的 并行收集器 ,使用 标记-整理 算法。
吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间)
Parallel Scavenge/Parallel Old收集器运行示意图:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的 老年代 收集器,使用 标记-清除 算法。
CMS 收集器运行示意图:
CMS 收集器主要包含4个阶段:
CMC停顿时间短的原因是:最耗时的并发标记和并发清除都是可以和用户线程一起执行的。
CMS使用的是 标记—清除 算法来实现的,所以就存在内存碎片的问题。当空间碎片过多,将会导致无法分配大对象,这时不得不提前触发一次Full GC。
CMS在并发标记和并发清除阶段是和用户线程一起运行的,这是垃圾回收机制就会占用部分线程(CPU资源)进行垃圾回收,线程数量默认为(CPU数量+3)/ 4,这样就会导致应用程序变慢。
在并发清除阶段产生的垃圾,只能在下一次GC的时候被回收,这部分垃圾称为浮动垃圾(Floating Garbage)。CMS收集器因为无法处理浮动垃圾,可能会出现“Concurrent ModeFailure”失败,而导致临时启用Serial Old收集器来重新进行一次Full GC,这时停顿时间就很长了。因此CMS不能等到老年代满了才进行回收,需要留一部分空间,提供给在并发收集过程中运行的线程使用。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。同时适用于 新生代和老年代 ,与其他GC收集器相比,G1具备如下特点:
它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
Remembered Set用来记录 新生代与老年代之间的对象引用或跨区域的对象引用(谁引用了我的对象) 。G1中每个Region都有一个与之对应的Remembered Set,在做YGC的时候,只需使用 年轻代中的region的Remembered Set作为根集,这些Remembered Set记录了old->young的跨代引用,避免了扫描整堆。而Mixed GC的时候,old generation中记录了old->old的Remembered Set,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销,复制回收算法。
回收前:
回收后:
回收所有年轻代里的Region,外加根据全局并发标记(global concurrent marking)统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region。Mixed GC不是full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用Serial old GC(Full GC)来收集整个GC Heap。
回收前:
回收后:
G1收集器的运作大致可划分为以下几个步骤:
1 GC相关的其他主要的参数有:
-XX:G1HeapRegionSize=n
:设置Region大小,并非最终值,取值范围从1M-32M之间,且是2的指数,默认为 size =(堆最小值+堆最大值)/ TARGET_REGION_NUMBER(2048) ,然后size取最靠近2的幂次数值, 并将size控制在[1M,32M]之间
-XX:MaxGCPauseMillis
:设置G1收集过程目标时间,默认值200ms,不是硬性条件 -XX:G1NewSizePercent
:设置新生代最小值,默认值5% -XX:G1MaxNewSizePercent
:设置新生代最大值,默认值60% -XX:ParallelGCThreads
:设置STW期间,并行GC线程数 -XX:ConcGCThreads=n
:设置并发标记阶段,并行执行的线程数 -XX:InitiatingHeapOccupancyPercent
:设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器,适用于年轻代和老年代,主要特点如下:
关键技术:
详细可参考: https://xiaolyuh.blog.csdn.net/article/details/103911166
总的来说就是内存不足的时候进行垃圾回收。
当Eden区满时,且老年代的最大可用连续空间大于新生代所有对象的总和或者老年代最大连续空间比历次晋升的平均值大,就进行Minor GC,否则FullGC。
Mixed GC的触发是由一些参数控制着:
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 适用场景 |
---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 面向服务端应用,将来替换CMS |
ZGC | 并发 | both | 有色指针+复制算法 | 低停顿,高吞吐量 |
参数 | 描述 |
---|---|
-XX:UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old 的收集器组合进行内存回收 |
-XX:UseParNewGC | 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收 |
-XX:UseConcMarkSweepGC | 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用 |
-XX:UseParallelGC | 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收 |
-XX:UseParallelOldGC | 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收 |
SurvivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor0 : Survivor1 = 8 : 1:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
-XX:HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况 |
-XX:ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
-XX:GCTimeRatio | GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效 |
-XX:MaxGCPauseMillis | 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效 |
-XX:CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效 |
-XX:UseCMSCompactAtFullCollection | 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效 |
-XX:CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效 |
-XX:+PrintGCDetails | 虚拟机发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况 |
《深入理解JAVA虚拟机》