java垃圾收集器

如何确定对象是否需要回收

垃圾收集前的第一步,就是需要判断哪些对象需要被回收,哪些不需要

引用计数法

在对象中添加一个计数器,被引用时就+1,引用失效时就-1。任何时刻计数器为0的对象就是不可以被引用的。原理简单,判定高效,大多数情况下是一个不错的算法。但是java中被没有使用,一个简单的原因,并不难解决两个对象循环依赖的问题

可达性分析算法

可达性分析法的主要思想是:通过一系列的“GC Roots”的根对象作为起始点集,根据引用关系向下搜索,搜索过程中走过的路径叫做引用链。如果某个对象和 GC Roots间没有任何引用链相连接,则证明该对象不可能再被使用。image-20220610145701706

GC Roots对象包括
  • 虚拟机栈(栈帧中的局部表量表)所引用的对象。
  • 方法区中静态属性引用的对象,如:java类中引用类型的静态变量
  • 方法区中常量引用的对象,如字符串常量池中的引用
  • 本地方法栈中的引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 被同步锁(synchronized)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

引用

强引用

最传统的引用,指程序代码之中最常见的引用赋值,类似“Object obj=new Object()”这种引用关系。只要强引用关系还在,垃圾收集器永远不会回收掉被引用对象

软引用

软引用描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出之前,会被列入回收范围之中进行二次回收,回收后还没有足够的内存才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用

弱引用也是用来描述非必需的对象。被弱应用关联的对象只能生存到下一次垃圾收集之前,无论内存是否充足,都会被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用

虚引用

一个对象是否有虚引用的存在,对其生存时间没有任何影响。也无法通过虚引用获取对象实例。设置虚引用的唯一目的是为了能在这个对象被收集器回收时得到系统的一个通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

finalize()

任何一个对象的finalize()方法都只会被系统自动调用一次。

被可达性分析法判定为不可达的对象,并不是一定会被清理,还需要进行一次筛选,筛选条件是,此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者已经被虚拟机调用过一次,那么就回被认为没有必要执行,就回被收集器回收

如果确定需要执行,放在一个为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。只会触发运行,并不保证等待运行结束。(防止某一个对象finalize()出问题,影响整个垃圾收集系统)。如果对象在finalize()方法中重新被引用,就不会被回收。

注意:不建议使用。运行代价高、不稳定、官方声明不推荐使用

垃圾收集算法

分代收集理论

当前的商业化虚拟机的垃圾收集器,大多数都是遵循分代手机理论设计的。分代收集建立在两个分代假说之上:

  1. 弱分代假说:绝大说对象都是朝生夕灭的

  2. 强分代假说:熬过越多次垃圾收集的对象就越难以消亡。

  3. 跨代引用假说:跨代引用相对应同代引用仅占极少数

    针对极少数的情况,并不是不处理了。在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描

标记-清除算法

首先标记出所有需要回收的对象,标记完成后统一回收标记的对象,也可以反过来

主要有两个缺点:第一个是执行效率不稳定,对内对象越多,效率越低

​ 第二个是内存空间碎片化问题,标记清楚产生大量不连续的内存碎片。

标记-复制算法

将可用内存按照大小分为两块儿,每次只使用其中的一块儿,当这一块儿使用完了,就把存活到的对象全部复制到另一块儿,然后把已使用的空间一次性清理掉。

优点:解决了标记-清除算法中效率和内存碎片问题

缺点:可用内存空间减少了一半,浪费

优化:针对对象朝生夕灭(IBM研究 98%的对象逃不过第一次回收),Andrew Appel提出了优化策略。具体做法是将新生代分为一块儿较大的Eden和两个较小的Survivor,每次分配时只使用Eden和一个survivor,当发生垃圾收集时,将存活的对象复制到另一个survivor中。然后直接清理掉Eden和Survivor空间。

HotSopt中Serial、ParNew等新生代垃圾收集器均采用了这种策略,HotSpot中Eden空间和两个survivor空间的比例时8:1:1

当一次收集,survivor不足以存储存活对象时,就需要依赖其他内存区域(老年代)进行担保分配,即会直接分配到老年代。

标记-整理算法

老年代对象存活率较高,上述两种策略都不太适用。有人提出了 标记-整理 策略:将所有存活的对象移动到内存的一边,然后直接清理掉边界以外的内存

垃圾收集器

Serial

serial是一个单线程的垃圾收集器,“单线程”的意义并不只是说它只会活着只使用一个处理器活着一个线程完成垃圾收集工作,更重要的是强调垃圾收集过程中必须停掉其他所有工作线程,直到收集结束。Serial 运行示意图image-20220611092307909

新生代垃圾收集器,简单高效、内存消耗小客户端模式下默认的垃圾收集器

ParNew

ParNew实际上是Serial的多线程版本。除了使用多线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Word、对象分配规则、回收策略都与Serial一致

image-20220611094602178

新生代垃圾收集器,JDK7之前,服务端模式下常用的收集器

Parallel Scavenge

parallel Scavenge,新生代垃圾收集器,他的关注点与其他收集器的关注点不同。CMS等收集器的特点是尽量减少垃圾收集时用户线程的停顿时间,而parallel Scavenge的目标是达到一个可控制的吞吐量。

-XX:MaxGCPauseMillis参数,控制最大垃圾收集停顿时间

-XX:GCTimeRatio参数,以及直接设置吞吐量大小

Serial Old

Serial 的老年代版本

Parallel Old

Parallel Old 是Parallel Scavenge 的老年代版本。JKD 6之前,Parallel Scavenge收集器能够配合的老年代收集器只有Serial Old。由于Serial Old的单线程收集无法发挥服务器多处理器的资源,这种组合的吞吐量不一定不 ParNew + CMS 高。

Parallel Old 出现后,在注重吞吐量及资源比较稀缺场合可以考虑使用 Parallel Scavenge加Parallel Old收集器这个组合。JDK 8的默认收集器

image-20220613103622157

CMS收集器

CMS收集器是一种以最短回收停顿时间为目标的收集器。应用场景为:比较关注响应速度,希望停顿时间尽可能短,常见的互联网网站,B/S架构的应用等。CMS是基于标记-清楚算法的,他的运行过程分为四个部分

  • 初始标记

    该阶段仍然需要 Stop The Word(停止用户线程)。但是该阶段只标记GC Roots直接关联的对象,速度很快。

  • 并发标记

    该阶段是通过上一步获取的直接关联对象,遍历整个对象图的过程,这个阶段耗时较长,但是不需要停止用户线程,用户线程可以和收集线程并发执行。

  • 重新标记

    该阶段仍然需要 Stop The Word(停止用户线程),主要为了修正并发标记期间因为用户程序继续运行导致的标记产生变动的对象。比初始标记时间长,但远小于并发标记的时间。

  • 并发清除

    清理掉已经标记为垃圾的对象,与用户线程并发执行。

image-20220613141655397

优点: 并发收集,低停顿。

缺点: 1. 资源敏感。处理器核心小于4时对用户程序影响较大。核数在四个核心或者以上时,收集线程占用不超过25%的处理器运算资 源,并且核心越多占用比例越小。

  1. CMS无法处理浮动垃圾。CMS并发标记阶段用户线程仍在继续运行的,期间也要产生垃圾,这部分垃圾便不能在本次收集过程清理掉他们,只能等到下一次垃圾收集处理。CMS还需要预留一部分内存空间,供给用户线程使用,

  2. 基于标记-清除,存在内存碎片。为解决内存碎片问题:一方面引入 -XX:+UseCMS-CompactAtFullCollection开关参数(默认开启),在进行fullGc时进行内存整理,无法并发,需要停止用户线程;另一方面,提供了另外一个参数-XX:CMSFullGCsBefore-

    Compaction 指定CMS收集器进行 n次不整理内存的full GC后下一次进行内存整理。

相关参数

//开启CMS垃圾收集器

-XX:+UseConcMarkSweepGC

//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用

-XX:+UseCMSCompactAtFullCollection

//默认0 几次Full GC后开始整理

-XX:CMSFullGCsBeforeCompaction=0

//辅助CMSInitiatingOccupancyFraction的参数,不然CMSInitiatingOccupancyFraction只会使

用一次就恢复自动调整,也就是开启手动调整。

-XX:+UseCMSInitiatingOccupancyOnly

//取值0-100,按百分比回收

-XX:CMSInitiatingOccupancyFraction 默认-1

G1收集器

G1是面向服务端的收集器,开创了收集器面向局部的收集思路,和基于Region的内存布局方式。JDK8 已支持,JDK 9默认的垃圾收集器。

G1的目标是:建立可预测的停顿时间模型。(能够在指定时间段M毫秒的时间内,消耗在垃圾回收的时间不超过N毫秒。)

G1开创的基于Reigon的内存布局方式是实现这一个目标的关键。G1把连续的java堆划分为大小相等的独立区域(Reigon),每个Reigon都可以更具需求担任Eden、Survivor、或者老年代空间。具体思路就是,G1去跟踪每个Reigon中垃圾堆积的“价值的大小”(回收得到的空间,和回收所需时间等)然后维护一个优先级列表,每次根据设定的停顿时间(-XX:MaxGCPauseMillis 默认200毫秒),在有限的是时间内处理回收价值最大的Region。

不计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1的收集过程如下

  • 初始标记

    标记GC Roots直接关联到的对象,并且修改STAB(为解决并发收集过程中新创建的对象的问题,Region中引入的两个指针,并发收集过程中新创建的对象需在这两个指针位置以上)指针的值。这个阶段需要停顿用户线程,时间很短,并且是借用Minor GC时进行的,没有额外的停顿。

  • 并发标记

    从GC Roots开始对堆中对象进行可达性分析,递归整个堆里的对象图,找出要回收的对象。耗时较长、但是可以与用户线程并发执行。还需要处理STAB记录下的在并发时引用变动的对象。

  • 最终标记

    对用户线程做一个短暂的停顿,用于处理在并发标记阶段结束后仍留下来的少量的STAB记录。

  • 筛选回收

    负责Reigon的数据统计,对各个Region的回收价值的成本做排序,根据指定的停顿时间制定回收计划,任意选择多个Region当作回收集进行回收,将其中存活的对象复制到新的空Reigon中,清理掉旧的Region。涉及到存活对象的移动,需要暂停用户线程,由多个收集线程完成。

运行示意图如下:

image-20220616095052261

可以由用户指定停顿时间是G1一个强大的功能,但是也不能是随便指定需要合理的值,默认时间是200毫秒。太短了会导致收集速率赶不上分配速率,导致Full Gc。合理的值二三百毫秒

从G1开始,垃圾收集器的设计思路都变为追求应付应用的内存分配速了,而不是追求一次性把堆清理赶紧。

相比CMS优点:可指定停顿时间、分Reigon的内存布局、按收益动态回收、基于标记-整理无内存碎片。

相比CMS缺点:垃圾收集内存占用、程序运行的额外负载较高

小内存应用CMS可能优于G1,这个平衡点在java堆容量在6-8G之间,经验之谈

相关参数

-XX: +UseG1GC 开启G1垃圾收集器

-XX: G1HeapReginSize 设置每个Region的大小,是2的幂次,1MB-32MB之间

-XX:MaxGCPauseMillis 最大停顿时间

-XX:ParallelGCThread 并行GC工作的线程数

-XX:ConcGCThreads 并发标记的线程数

-XX:InitiatingHeapOcccupancyPercent 默认45%,代表GC堆占用达到多少的时候开始垃圾收集

Shenandoah 收集器

Shenandoah 与G1非常相似,是有RedHat公司研发,后贡献给了openJDK。OpenJDK12开始引入,Oracle JDK是不支持的。

不同之处:首先,支持并发整理;其次,默认不使用分代收集;最后摒弃了记忆集,使用链接矩阵描述夸Region引用,节省内存和计算资源。

工作过程如下(未说明的就是和G1一样):

  • 初始标记

  • 并发标记

  • 最终标记

  • 并发清理

    清理整个Reigon中一个存活对象都没有的Reigon。

  • 并发回收

    并发回收阶段是Shenandoah与其他Hotspot其他收集器的核心差异。首先把回收集中存活的对象复制一份到其他Reigon中。难点就是,并发过程中用户线程仍然不停的访问对象,移动对象后,内存中所有对这个对象的引用仍然是指向旧的地址很难瞬间改变过来的。Shenandoah通过读屏障和Brooks Pointers转发指针解决这个问题

  • 初始引用更新

    并发回收结束后,需要将堆中所有指向旧对象的引用修复到复制后新对象的地址。该阶段并没有做引用更新的具体操作,主要是为了建立一个线程集合点,确保并发回收阶段所有的收集器线程都已经完成他们的对象移动任务。会产生短暂的停顿。

  • 并发引用更新

    真正开始引用更新操作,同用户线程并发执行。不需要沿着对象图搜索,执行按照物理内存地址顺序,线性的搜索出引用类型,把旧值改为新值

  • 最终引用更新

    修正GC Roots中的引用。需要停顿,时间与GC Roots的数量有关。

  • 并发清理

    经过并发回收和引用更新之后,回收集中所有Region都没有存活对象了,在执行一次并发清理来回收这些空间。

Brooks Pointer 转发指针

Shenandoah的并发整理主要核心就是 Brooks Pointer 转发指针。

之前要做类似的并发操,通常是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到就对象的内存就会产生自陷中段,进入预设好的异常处理器中再由其中逻辑代码把访问转发到新对象的上,这种方案将导致用户态频繁切换到内核态,代价很大,不能频繁使用

Brooks 提出不需要内存保护陷阱的方案而是在对象布局结构的最前面加一个新的引用字段,正常不处于并发移动情况下该引用指向对象自己。当对象有了新的副本,将就对象的转发指针指向新对象就可以了

ZGC收集器

目标

ZGC的目标是在尽可能在对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间控制在10毫秒之内。

内存布局

也是基于Reigon的堆内存布局,ZGC的Reigon(也被称作Page或者 Zpage)具有动态性,动态的创建和销毁,动态的区域容量大小。小型(Small Region)容量固定为2M,存放小于256k的对象;中型(Medium Reigon)容量固定32M,存放256k~4M的对象;大型(Large Region)容量不固定,但是必须是2的整数倍,每个大型Region中只放一个对象,最小容量为4M.

并发整理算法

染色指针是ZGC的一种标志性技术。它是一种将少量信息直接存储在指针上的技术。

Linux下64位指针的高18位不能用来寻址,但是剩余的46位所能支持的64TB内存仍然能够充分满足大型服务器的内存需求。ZGC的染色指针从剩下来46位中选取4位用来存储标志信息,分别是其引用对象的三色标记状态、是否进入了重新分配集(被移动过)、是否只能通过finalize()方法才能被访问到。这样就导致了ZGC能够管理的内存空间不超过4TB(2的42次幂)。染色指针如下图所示。

image-20220617172958353

染色指针的优势:

  1. 可以在一个Regino中对象中存活对象全部被移走后立即清理掉这个Region,不必等到对象引用修正完成后。
  2. 可以大幅度减少收集过程中内存屏障的使用数量,提高运行效率
ZGC的运作过程
  • 并发标记

    遍历对象图,做可达性分析,会有短暂停顿。ZGC的标记是在指针上而不是对象上,标记阶段会更新染色指针中Marked0 和Marked1标志位。

  • 并发预备重分配

    统计出本次需要清理的Region,组成重分配集。

  • 并发重分配

    重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系。,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力

  • 并发重映射

    重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用。前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

常见垃圾收集器汇总

image-20220620092838796

常用参数

参数含义说明
-XX:CICompilerCount=3最大并行编译数如果设置大于1,虽然编译速度会提高,但是同样影响系统稳定性,会增加JVM崩溃的可能
-XX:InitialHeapSize=100M初始化堆大小简写-Xms100M
-XX:MaxHeapSize=100M最大堆大小简写-Xms100M
-XX:NewSize=20M设置年轻代的大小
-XX:MaxNewSize=50M年轻代最大大小
-XX:OldSize=50M设置老年代大小
-XX:MetaspaceSize=50M设置方法区大小
-XX:MaxMetaspaceSize=50M方法区最大大小
-XX:+UseParallelGC使用UseParallelGC新生代,吞吐量优先
-XX:+UseParallelOldGC使用UseParallelOldGC老年代,吞吐量优先
-XX:+UseConcMarkSweepGC使用CMS老年代,停顿时间优先
-XX:+UseG1GC使用G1GC新生代,老年代,停顿时间优先
-XX:NewRatio新老生代的比值比如-XX:Ratio=4,则表示新生代:老年代=1:4,也就是新生代占整个堆内存的1/5
-XX:SurvivorRatio两个S区和Eden区的比值比如-XX:SurvivorRatio=8,也就是(S0+S1):Eden=2:8,也就是一个S占整个新生代的1/10
-XX:+HeapDumpOnOutOfMemoryError启动堆内存溢出打印当JVM堆内存发生溢出时,也就是OOM,自动生成dump文件
-XX:HeapDumpPath=heap.hprof指定堆内存溢出打印目录表示在当前目录生成一个 heap.hprof文件
-Xss128k设置每个线程的堆栈大小经验值是3000-5000最佳
-XX:MaxTenuringThreshold=6提升年老代的最大临界值默认值为 15
-XX:InitiatingHeapOccupancyPercent启动并发GC周期时堆内存使用占比G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45.
-XX:G1HeapWastePercent允许的浪费堆空间的占比默认是10%,如果并发标记可回收的空间小于10%,则不会触发MixedGC。
-XX:MaxGCPauseMillis=200msG1最大停顿时间暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。
-XX:ConcGCThreads=n并发垃圾收集器使用的线程数量默认值随JVM运行的平台不同而不同
-XX:G1MixedGCLiveThresholdPercent=65混合垃圾回收周期中要包括的旧区域设置占用率阈值默认占用率为 65%
-XX:G1MixedGCCountTarget=8设置标记周期完成后,对存活数据上限为G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标次数默认8次混合垃圾回收,混合回收的目标是要控制在此目标次数以内
-XX:+PrintGC查看GC信息
-XX:+PrintGCDetails查看GC详细信息
-XX:+PrintHeapAtGC查看GC前后的堆、方法区可用容量变化
-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTimeGC过程中用户线程并发及停顿时间
-XX:+PrintAdaptiveSizePolicy查看收集器Ergonomics机制自动调节的相关信息自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持

内存分配策略

  • 对象优先在Eden区分配,当Eden区没有足够的空间时,将触发一次Minor GC。发生Minor Gc时,无法放入Survivro空间的对象会被直接放入老年代

  • 大对象直接进入老年代(-XX:PretenureSizeThreshold=3145725 参数,不能写成M, 指定大于该设置值的对象直接在老年代分配。JDK 8默认的Parallel Scavenge 不支持该参数)

  • 长期存活的对象将进入老年代(以 -XX:MaxTenuringThreshold=15,默认值为15,对象的分代年龄,每经过一次Minor GC,分代年龄+1)

  • 动态对象年龄判定(如果Survivor 空间中相同年龄的对象大小总和大于Survivor的一半,则年龄大于等于该年龄的对象直接进入老年代)

  • 空间分配担保。发生Minor Gc之前,虚拟机先要检查老年代的最大可用的连续空间是否大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。(JDK 1.6之前由 -XX:HandlePromotionFailure参数控制,是否开启分配担保,即检是否还需要查历次晋升至老年代的对象大小,之后可以立即为该参数为true)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值