1.判断对象"已死"
1.引用计数法(Reference Counting)
每有一个引用其引用计数器+1,单纯的引用计数无法解决互相循环引用的问题
2.可达性分析(Reachability Counting)
将一系列GC Roots根对象作为初始节点集,从这些节点根据引用关系向下搜索。一个对象到GC Roots间没有任何引用链相连,则该对象不再被使用。
GC Roots根:
- 栈帧中的局部变量
- 已加载类的静态变量
- 方法区中常量引用的对象
- JNI(Java Native Interface) handles 即本地方法栈引用的对象
关于引用
1.强引用(Strongly Reference)
最普遍存在的引用赋值。
引用关系还存在,就不会被回收 :Object obj = new Object();
2.软引用(Soft Reference)
有用,但没那么有用的对象。
系统即将发生内存溢出异常时,会将软引用对象进行二次回收,如果还是内存不足,才抛出内存移除异常
接口类:SoftReference
3.弱引用(Weak Reference)
只要发生垃圾回收就会被收回的对象。
接口类:SoftReference
4.虚引用(Phantom Reference)
最弱的引用关系,又成 幽灵引用 、幻影引用。并不会影响对象的生存时间。
为一个对象设置虚引用关系时,可以在该对象被回收时收到一个系统通知
接口类:PhantomReference
常量、类的回收
因为其较苛刻的判断条件及回收成果,方法区的垃圾回收"性价比"相对较低。
常量的判断:
没有任何字符串对象引用常量池的该常量
类的判断:
- 类的所有实例已回收
- 该类的类加载器已被回收(除非是经过精心设计的可替换类加载器)
- class对象没有任何地方被引用(无法通过放射来访问方法)
虚拟机相关参数:
- -Xnoclassgc: 禁用类的垃圾收集(GC)。这样可以节省一些GC时间, 但也可能导致更多的内存被永久占用,如果不谨慎使用,将抛出内存不足异常
- -XX:+TraceClassLoading :跟踪类加载
- -XX:TraceClassUnloading 跟踪类卸载
2.垃圾回收算法
分代收集理论
绝大多数对象时朝生夕灭的,熬过越多次垃圾收集过程的对象越难以消亡,基于这两个假说奠定了一个原则:收集器应基于对象年龄将堆划分为不同的区域。在低年龄区域只关注如何保留少量对象,以较低的频率回收高年龄区域,这样同时兼顾时间开销和空间的有效利用。于是便有了"Minor GC/Young GC"、"Major GC/Old GC"、"Full GC"。同时发展出了 "标记复制"、"标记清除"、"标记整理"等真针对性的算法来应对不同区域的对象来做来垃圾收集。
- Minor GC/Young GC: 新生代垃圾收集
- Major GC/Old GC:老年代垃圾收集
- Mixed GC: 混合收集,整个新生代和部分老年代(G1收集器)
- Full GC:整堆和方法区的垃圾收集
跨代引用
新生代对象被老年代引用。假如要现在进行一次只局限于新生代区域内的收集, 但新生代中的对象是完全有可能被老年代所引用的, 为了找出该区域中的存活对象, 不得不在固定的GC Roots之外, 再额外遍历整个老年代中 所有对象来确保可达性分析结果的正确性
解决: 新生代建立一个全局的数据结构(Remembered Set记忆集),该结构将老年代划分为若干个小块,标记老年代哪一块内存会存在跨代引用
标记清除(Mark-Sweep)
最基础的垃圾收集算法,标记对象,清除未存活的对象
缺点:标记、清除的执行效率随对象数量增长而降低,且导致内存空间碎片化,可能在运行过程中需要分配大对象时因连续内存不足导致二次垃圾收集
标记复制
解决标记清除面对大量可回收对象时的低效。内存按容量划分为大小相等的两块,每次只使用其中的一块,这一块内存区域用完时,将存活的对象复制到另一块上,再将之的内存的空间一次清理掉。结合新生代"朝生夕灭"的特点,不需要按1:1的比例来划分新生代的内存空间。
HotSpot Serial、ParNew等新生代收集器均采用按8:1的比例来划分Eden 和 Survivor空间(两块内存较小的区域),每次分配内存只使用Eden和一块Survivor。发生垃圾收集时将Eden和Survivor存活的对象一次性复制到另一块Survivor空间上,并清理Eden和之前的Survivor区域。这样就只有10%的空间浪费。
当Survivor空间不足以容纳一次Minor GC之后的对象,需要依赖其他内存区域(老年代)进行分配担保(Handle Promotion)。
缺点: 存在空间浪费,需要依赖其他内存区域进行分配担保。
标记整理
标记复制在对象存活率较高时需要进行较多的复制操作,效率会降低,同时如果不想浪费50%的空间,就需要额外的区域进行内存担保,所以标记复制并不适用于老年代。
标记整理标记过程同标记清除,标记完成后不是直接进行清理,而是将会存活对象向内存空间一侧移动,然后清除边界之外的内存。
缺点:移动存活对象并更新所有指向这些对象的引用本身就是一种负担较重的操作。
3.HotSPort算法细节
1.根节点枚举
查找所有的GC Roots根需要暂停用户线程来保障分析过程中,根节点集合的对象引用关系处于稳定。
HotSpot使用一组称为OopMap(Ordinary Object Pointer普通对象指针)的数据结构,当类加载动作完成时,对象内什么偏移量上是什么类型的数据就会被计算出来。(即时编译也会在特定位置记录栈里、寄存器里哪些位置是引用。)因此不需要检查所有上下文和引用位置,根节点枚举不是那么的耗时。
2.安全点
用户程序执行时并非在代码指令流的任意位置都能停顿下来进行垃圾收集,而是必须指定到安全点后才能暂停。 安全点出现在
- 方法调用
- 循环跳转(使用int或范围更小的数据类型作为索引值的循环成为可数循环Counted Loop默认不会放置安全点,使用long或者范围更大的数据类型作为索引值的循环成为不可数循环Uncounted Loop会被放置安全点)
- 异常跳转
等"具有让程序长时间执行的特征" 的位置。
中断
所有线程都跑到最近的安全点并停顿下来
抢先式中断:系统将所有用户线程全部中断,用户线程不在安全点击恢复该线程执行,一会再重新中断,直到跑到安全点- 主动式中断:垃圾收集需要中断线程时设置一个标志位,用户线程主动轮询该标志位,发现中断标志位为true时主动中断挂起。轮询标志的地方和安全点的地方是重合的。此外创建对象和在堆上分配内存时也有轮询标志位的指令,目的是检查是否将发生垃圾收集,避免没有足够内存分配对象。
3.安全区域(Safe Region)
Sleep 或Blocked状态的线程无法响应中断请求,不能走到安全点中断挂起自己。安全区域就是一段代码片段之中,引用关系不会发生变化。当线程离开安全区域时,需要检查是否完成根节点枚举(或垃圾收集过程中其他需要暂停用户线程的阶段),如果完成则继续,否则等待直到收到可以离开安全区域的信号
4.卡表(Card Table)
记忆集最常见的一种实现方式。HotSpot使用一个item为512字节的内存块(卡页Card Page) 组成的字节数据数组来组成卡表Card Table。一个卡页的内存中通常包含多个对象,只要卡页中存在跨代引用则标记为Dirty,垃圾收集时只要筛选出脏页并加入GC Roots一并扫描。
4.垃圾收集器
就像CAP理论总结的 一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)3 个要素最多只能同时满足两个,不可兼得。(分区容忍性是不可或缺的)。 垃圾收集器的三个指标:内存占用(Footprint)、吞吐量(Throughput :运行用户代码的时间 与 用户代码时间+ 垃圾回收时间 的 占比) 和延迟(Latency) 三者构成了一个"不可能三角",一款优秀的收集器通常只会满足其中两项。其中延迟(垃圾收集时用户线程的停顿时间)的重要性愈发明显
指标关注点:
- 内存占用:客户端应用、嵌入式应用
- 吞吐量:数据分析、科学计算类任务尽快算出结果
- 延迟:SLA服务型应用
Serial
单线程标记复制算法新生代收集器,垃圾收集时,必须暂停所有工作线程直到收集结束。单线程专注垃圾收集效率,在客户端模式下(内存占用小)是个不错的选择。
ParNew
标记复制算法,Serrial的多线程并行版(并行指的是垃圾收集 和用户线程同时都在工作),除Serial外能与CMS收集器配置工作。JDK9 后不再支持
Parallel Scavenge
标记复制算法,目标是达到一个可控的吞吐量(运行用户代码的时间 与 用户代码时间+ 垃圾回收时间 的 占比)。jdk9 之前默认使用(如果你是java8 用java -XX:+PrintCommandLineFlags -version 就会发现这个-XX:+UseParallelGC 下文有解释)。
- -XX:MaxGCPauseMillis :大于0的ms数,最大垃圾收集停顿时间
- -XX:GCTimeRation: 大于0小于100的整数,允许的最大垃圾回收时间占总时间的比率 。默认值为99,设置为19,则允许比率为 1/(1+19) = 5%;
- -XX: +UseAdaptiveSizepollcy : :JDK 1.8 默认使用 UseParallelGC 该垃圾回收器默认启动了 AdaptiveSizePolicy ,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false。
使用UseAdaptiveSizePolicy 不再需要人工指定新生代的大小,Eden 和 Survivro的比例,晋升老年代的对象大小(-XX:PretenureSizeThreshold)等细节参数,其会动态调整 Eden、Survivor 的大小, 某些情况存在Survivor自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代, 使用SurvivorRatio参数会使UseAdaptiveSizePolicy 失效
Serial Old
标记整理算法 Serial的老年代版本。单线程收集器。其主要意义也是供客户端使用。
Parallel Old
标记整理算法 Parallel Scavenge 的老年代版本,支持多线程并发收集。
CMS (Concurrent Mark Sweep)
注重低延迟(垃圾收集时用户线程的停顿时间)的老年代收集器。需要搭配新生代收集器一起使用,首次实现了让垃圾收集器与用户线程(基本上)同时工作。由于垃圾回收时用户线程仍在运行,所以不能等到老年代快被填满时才进行回收。jdk6时 -XX:CMSInitialtingOccu-pancyFraction CMS的触发百分比默认设置到了92%,如果预留的内存无法满足程序分配对象时,将冻结用户线程,临时启用Serial Old来重新进行垃圾回收
执行过程
- 初始标记 (Stop the world):标记Gc Roots能直接关联的对象
- 并发标记:从直接关联对象遍历整个对象图
- 重新标记 (Stop the world): 基于写屏障解决增量更新,这阶段时间会比1阶段稍长
- 并发清除
缺点:
- 多线程并发处理占用处理器运行算资源,导致应用程序变慢,在处理器核心数低于4个时尤为明显
- 标记复制 内存碎片化,可能导致二次垃圾回收
G1 (Garbage First)
G1作为CMS的替代者和继承人,目的是建立起"停顿时间模型"。其基于Region的内存布局形式,可以面向堆内存任何部分来组成回收集(Collection Cet,Cset)是一个横跨新生代和老年代的垃圾回收器。仍然遵循分代收集理论,不再坚持固定大小、固定数量的分代区域划分,将连续的堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 整理算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。
Region中有一类特殊的Humongous区域,专门用来存储大对象。每个Region的大小通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB之间 2的N次幂。只要超过Region大小一般的对象即可判定为大对象,超过了整个Region大小的超大对象被存放至N个连续的Humongous Region。
G1每次回收的内存空间都是Region的整数倍,有计划的避免全区域收集。基于后台维护的优先级列表,根据用户设置的收集停顿时间(-XX:MaxGCPauseMillis 默认200ms) 优先处理回收价值最大的那些Region,这也就是"Garbage First"的由来,保证其在有限的时间内获取尽可能高的收集效率,从而建立可预测的停顿时间模型。
G1的记忆集本质上一个哈希表,,每个Region都维护了一份双向卡表结构,不仅记录了我指向谁,还记录了谁指向我。
G1 基于 原始快照 (SATB Snapshot At The Beginning) 解决引用改变问题(第2阶段未暂停用户线程带来的问题)
G1 为每一个Region设计了两个TAMS(Top at Mark Start)指针,将Region中的一部分空间用于并发阶段的新对象分配。如果内存回收的速度赶不上内存分配的速度,G1收集器也要冻结用户线程执行进行Full GC产生长时间的Stop the world。
小内存应用上CMS表现大概率优于G1,大内存(6~8GB)上G1的优势能充分发挥
执行过程
- 初始标记 (短暂Stop the world) :标记GC Roots能直接关联到的对象
- 并发标记
- 最终标记 (短暂Stop the world) 处理并发标记阶段与 SATB发生变动的对象,比CMS第3阶段时间会短
- 筛选回收 (Stop the world) 更新Region的统计数据,对各个Region的回收价值和成本进行排序并指定回收计划。 将决定回收的那部分Region的存活对象 复制到空的Region中,清理掉旧的Region。
缺点
- 内存占用高。堆中每个Region都有一份卡表,导致G1的记忆集占用更多内存空间
- 除使用写后屏障维护卡表外,还需要写前屏障来跟踪并发时的指针变化以实现快照搜索(SATB),消耗更多的计算机资源
低延迟垃圾收集器
CMS(增量更新)和 G1(原始快照)收集器都实现了并发标记,不会因堆内存变大而停顿时间变长,但标记阶段之后的处理终归还是要停顿的(CMS产生碎片淤积最终停顿,G1小颗粒度回收仍有停顿)。而低延迟垃圾收集器的目标是实现任何堆内存大小都将垃圾收集停顿时间限制在十毫秒以内,这意味着除了要并发标记外,还要进行并发的清理。
Shenandoah
同G1一样也是基于Region,但摒弃了G1记忆集的做法,取而代之的是"连接矩阵"(简单的理解为二维表格)的全局数据结构来记录跨Region的引用关系
执行过程
- 初始标记(短暂stop the world)
- 并发标记
- 最终标记(短暂stop the world): 处理SATB,构建回收集
- 并发清理:清理一个存活对象都没有的Region
- 并发回收:基于读屏障和转发指针(Brooks Pointers/Forwarding Pointer)将存活对象复制到一份未被使用的Region。并发回收运行时间取决于回收集的大小
- 初始引用更新(非常短暂stop the world):确保并发回收阶段的复制任务都已结束,为引用更新作准备
- 并发引用更新:
- 最终引用更新(stop the world): 修正GC Roots的引用,停顿时间与 GC Roots数量相关
- 并发清理:经过之前的阶段,回收集中已无存活的对象,并发的回收这些空间
关于并发回收
1.转发指针 在对象布局结构最前面加一个新的引用字段,默认指向自己,每次对象访问都有一次的转向开销。但也仍比传统的做法要好,(在移动对象原有的内存上设置保护陷阱,访问对象旧地址是发生自陷中断,进入预设的异常处理器中,由预设的代码逻辑将其转发至新的内存地址。这样做会有用户态频繁切换内核态的开销。),但同时也需要使用CAS来确保用户更新对象、收集器复制对象、收集器更新对象引用字段的有序进行。
2.读屏障 针对引用类型的对象比较、对象哈希值计算、对象加锁等对象访问操作都需要加读写屏障来实现转发指针
ZGC
同样基于Region(或称之为ZPage、Page)。不同的是ZGC的Region具有动态性:动态创建、销毁、容量大小。 基于染色指针技术(提取引用指针的4位来存储标志位:三色标记(还未被垃圾收集器扫描,扫描后是安全的,扫秒后但该对象还有至少一个引用未扫描)、是否移动、是否只能通过finalize方法访问),压缩了原本46位的地址空间,导致ZGC能管理的内存只有2的42次幂也就是4TB,好处就是不需要访问对象仅通过指针就能判断对象引用变化状态,同时只要有一个Region存活对象被移走Region就能马上回收重利用。但这也决定了其不能用于32位平台,不能支持压缩指针。
ZGC没有跨代引用问题,因为其本身就不支持分代收集
执行过程
- 并发标记(短暂stop the world):遍历对象图做可达性分析,标记染色指针
- 并发预备重分配:扫描所有Region,用范围更大的成本省去G1记忆集的维护成本,筛选出哪些对象需要重新复制到其他Region
- 并发重分配:将重分配集中的对象复制到新的Region中,并未重分配集中的每个Region维护一个转发表,记录旧新对象的转向关系。用户线程在此时访问重分配集中的对象时,将被预置的内存屏障截获,基于转发表转发至复制的新对象上,并更新引用指针(指针的自愈能力Self-Healing),好处是只有一次转发开销,不像Brooks Pointer转发指针一样每次都有开销。
- 并发重映射:修正整个堆中指向重分配集中的旧对象的所有引用。即使不马上修正,由于指针的自愈也是没有问题的。这一段是为了"不变慢"以及清理转发表。所以该阶段可以合并到下一次垃圾收集循环的开始,并发标记工作一起进行,省去对象图遍历的开销。
缺点
尽管全流程都是可以并发进行,舍去了记忆集、分代的设定,也没有写屏障,但应对很大的堆,且对象新生速率很高的场景时,会产生大量浮动垃圾(堆体积大,回收时间长,新生对象未进入回收范围,导致回收的后的空间比之前还小,因为产生了新的对象),假设这样的的状态持续较久,堆的可用空间将越来越小,只能尽可能增加堆容量的大小以获得喘息。要解决这个问题还是要引入分代收集。
5.其他内存分配策略
- 对象优先在Eden分配,空间不足进行Minor GC,Survivor不足进入老年大
- 大对象基于参数PretenureSizeThreshold(默认3M)直接进入老年代
- 基于MaxTenuringThreshold(默认15),长期存活的对象进入老年代
- 动态年龄判断超过阈值的对象进入老年代
动态年龄判断算法:Survivor区的对象年龄从小到大进行累加,当累加到 X 年龄时的总和大于50%
(可以使用-XX:TargetSurvivorRatio=80 来设置保留多少空闲空间,默认值是50),那么大于或等于X的都会晋升到老年代。
假设第一代年龄对象总和已大于 Survivor空间的一半,那么所有的该区域的对象全部提前进入老年代
6.jvm参数
-
-XX:+PrintCommandLineFlags 打印所选垃圾收集器和堆空间大小等设置
-
-Xms 初始堆
-
-Xmx 最大堆大小
-
-Xmn 新生代大小(Sun推荐新生代为整个堆的3/8)
-
-Xss 线程栈默认大小1M
-
-XX:SurvivorRatio=8 Survivor*2 :Eden = 2:8 即一个Survivor 为1/10
-
-XX:MaxMetaspaceSize 最大元空间
-
-XX:+PrintGC 打印基本信息
-
-XX:+PrintGCDetails 打印gc细节
-
-XX:+PringGCApplicationConcurrentTime 收集过程中用户线程并发时间
-
-XX:+PringGCApplicationStoppedTime 收集过程中用户线程停顿时间
-
-XX:+PrintSafepointStatistics
-
-XX:+PrintSafepointStatisticsCount=1 打印安全点日志
-
-XX:+SafepointTimeout
-
-XX:+SafepointTimeoutDelay=2000 安全点超时时间,超过2000ms会报错 输出问题线程名称
-
-XX:+PrintHeapAtGC :每次GC前后打印堆信息
-
-XX:+PrintTenuringDistribution: 收集后剩余对象年龄分布信息
-
-Xloggc:/xx/xxx/gc.log : gclog日志路径
-
-XX:+UseGCLogFileRotation 开启日志文件分割
-
-XX:NumberOfGCLogFiles=14 最多分割几个文件,超过之后从头开始写
-
-XX:GCLogFileSize=100M 每个文件上限大小,超过就触发分割
-
-XX:MaxGCPauseMillis=200 : 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 (Parallel scanvenage / g1 )
-
-XX:PretenureSizeThreshold=3145728 超过3M的对象直接在老年代分配 (只对Serial及ParNew两款收集器有效)
-
-XX:MaxTenuringThreshold=15 阈值15 分代年龄晋升老年代
-
-XX: +UseAdaptiveSizepollcy : Parallel Eden,from, to 比例动态调整
JDK 1.8 默认使用 UseParallelGC 该垃圾回收器默认启动了 AdaptiveSizePolicy
JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false
使用SurvivorRatio参数会使UseAdaptiveSizePolicy 失效
由于AdaptiveSizePolicy会动态调整 Eden、Survivor 的大小, 被有些情况存在Survivor自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代
-
-XX:-UseAdaptiveSizepollcy : 禁用动态调整
-
-XX:+PrintAdaptiveSizePolicy:打印UseAdaptiveSizepollcy开启的自动调节信息
-
-XX:+ParallelGCThreads: 并行GC时内存回收的线程数
-
-XX:+HeapDumpBeforeFullGC :fullgc前导出dump
-
-XX:HeapDumpPath=./heap.hprof :导出路径
-
-XX:MaxDirectMemorySize=256m 可分配堆外内存大小,默认64m,最大为sun.misc.VM.maxDirectMemory() , nio需要用到, gateway 可设置大一点,如512m,1024m, DirectByteBuffer分配的堆外内存到达指定大小后,会触发Full GC
-
-XX:TraceClassLoading 类加载
-
-XX:TraceClassUnloading 类卸载 这两个参数在你感觉到元空间异常时可以使用到
-
-XX:G1NewSizePercent 新生代最小值 默认5%
-
-XX:G1MaxNewSizePercent 生代最大值 默认60%
-
-XX:+UseParallelGC: JDK9之前 默认的内存回收配置 Parallel Scanvenge + Serial Old
-
-XX:+UseParallelOldGC: Parallel Scanvenge + Parallel Old
-
-XX:+UseConcMarkSweepGC :ParNew + CMS +Serial Old来进行垃圾回收
推荐配置
Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍
年轻代Xmn的设置为老年代存活对象的1-1.5倍。