欢送关注公众号:五小竹
什么是垃圾
内存中不在被应用到的内存空间,就是垃圾。
Java 中的内存是动态分配和主动回收的。学习垃圾收集机制,调优策略,可能帮忙咱们解决和应答各种工作中面临的内存透露问题。
Java 虚拟机运行时数据辨别为程序计数器、虚拟机栈、本地办法栈、堆、办法区。
其中程序计数器、虚拟机栈、本地办法栈这 3 个区域是线程公有的,会随线程沦亡而主动回收,所以不须要治理。
因而垃圾收集只须要关注堆和办法区。
Java 的内存调配
堆上调配
- 如果启动了本地线程调配缓冲,将按线程优先调配在 TLAB 上。
- 对象优先在 Eden 上调配
- 大对象进入老年代
- 长期存活的对象进入老年代
栈上调配
基于逃逸剖析技术,如果一个对象始终在一个办法内。确定这个对象不会逃逸办法之外,那让这个对象在栈上分配内存,这样对象就会随着办法的完结而主动销毁,升高了垃圾回收的压力。
如何断定垃圾
援用计数法
给对象增加一个援用计数器,有对这个对象的援用就对计数加 1,援用生效就减 1,任何时刻计数器为 0 的对象就是断定为垃圾。
- 长处:实现简略,效率高。
- 毛病:不能解决对象循环援用的问题。在多线程环境下,援用计数变更也要进行低廉的同步操作,性能较低。
可达性分析法
目前支流的虚拟机才有的算法。他是从根节点(GC Root)向上来搜寻对象节点,搜寻走过的门路称为援用链,当一个对象到根节点之间没有联通的话,这个对象断定为垃圾。
GC Root 对象
能够作为 GC Roots 的对象包含:
- 虚拟机栈中援用的对象
- 办法区类动态属性信用的对象
- 办法去常量援用的对象
- 本地办法栈中 JNI(Native 办法)援用的对象
援用类型
判断一个对象的存活跟援用无关,jdk1.2 之前,援用只是指援用类型的数据中存储了另一块内存的地址。jdk1.2 之后,java 的援用分为以下四种。
- 强援用:Object obj = new Object(); 最常见的援用,这样通过 new 创立的会产生该对象的强援用。只有对象有强援用指向,并且 GC Roots 可达,那么就不会回收该对象。
- 软援用: SotfReference 类来实现。示意一些还有用然而非必须的对象。在 OOM 前,垃圾收集器会把这些软援用指向的对象退出回收范畴,对于软援用关联的对象,只有在内存不足的时候才会回收。
- 弱援用:WeakReference, 示意非必要对象,在 YGC 时候会被回收。因为 YGC 工夫不确定,所以弱援用随时都有可能被回收。
- 虚援用:PhantomReference, 无奈通过该援用获取指向的对象。在任何时候都可能被垃圾回收器回收。虚援用次要用来跟踪对象被垃圾回收器回收的流动,必须和援用队列配合应用:
如
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>("Hello", queue);
当垃圾回收器筹备回收一个对象时,发现他有虚援用,就会在回收这对象的内存前,把这个虚援用退出队列,程序通过援用队列中是否有该虚援用,来理解对象是否要被垃圾回收器回收,而后能够在回收前做一些事件。
垃圾收集算法
标记 - 革除(Mark-Sweep)
最根底的算法,第一步,先标记,从 GC Roots 登程,顺次标记对象援用关系,而后,将没有被标记的对象革除。
毛病:一个是效率问题,标记和革除两个过程效率都不高;另外,革除过后会产生大量不间断的内存碎片。导致当咱们须要调配大对象时候,无奈找到足够的间断内存而又触发一次 GC。
复制(Copy)
复制算法:把内存分为两块大小雷同的区域,每次应用其中一块,当应用完了,就把这一块区域上还存活对象拷贝到另一块,而后把这一块革除掉。不会造成内存碎片,然而内存利用率不高,会造成一半的空间节约。
新生代上的对象大多数“朝生夕死”,HotSpot 默认将新生代内存划分为一个大的 Eden 区和两个小的 Surivor 区。在 GC 时,将 Eden 区和一个 Surivor 区中存活的对象,复制到另外一个 Surivor 区。Eden 区和两个 Surivor 区比例默认 8:1:1,所以这时候只会节约 10% 的空间。
标记 - 整顿 (Mark-Compact)
和标记革除一样,先标记,然而后续不间接回收对象清理,而是让存活的对象向一端挪动,而后间接清理掉端边界之外的内存。
因为复制算法在存活对象比拟多的时候,效率比拟低,且有空间节约,所以老年代个别会选用标记 - 整顿算法。
### 三种 GC 算法比照
回收算法 | 长处 | 毛病 |
---|---|---|
标记 - 革除 | 实现简略 | 存在内存碎片 |
复制 | 无碎片,性能好 | 内存使用率低 |
标记 - 整顿 | 无碎片 | 整顿过程开销大 |
分代收集
将下面的各种算法组合在一起,就地取材。个别咱们把堆空间分为新生代和老年代。依据他们的特定采纳不同的垃圾回收算法,在新生代,每次 GC 都会有大量对象死去,多数存活,所以采纳复制算法。只用付出多数对象的复制老本,不会造成不间断的内存碎片。而在老年代,对象存活率高,则采纳标记整顿或者标记革除。
垃圾收集器
术语
- STW: “Stop The World” 简写,也叫全局进展。会造成服务暂停,没有响应。
- 串行收集:GC 单线程内存回收,会暂停所有的用户线程。比方 Serial, Serial Old
- 并行收集: 多线程进行并发 GC,此时暂停用户线程。比方 Parallel
- 并发收集:用户线程和 GC 线程同时执行,不须要进展用户线程. 适宜对响应工夫有要求的场景。比方 CMS 收集器
Serial/Serial Old
Serial 是一个单线程的收集器,它只会应用一个 CPU 或一个收集线程去做垃圾收集,并且在做垃圾收集时须要进行所有的工作线程,晓得收集工作完结。Serial 进行用户线程,采纳复制算法收集年老代,Serial Old 采纳标记 - 整顿算法收集老年代。
特点:单线程收集,STW
应用 -XX:+UseSerialGC 开启 Serial + Serial Old
ParNew
Serial 的多线程版本,应用多线程进行垃圾收集。他是新生代的垃圾收集器。须要配合老年代的 CMS 收集器应用。所以开启须要应用 CMS, 新生代默认 ParNew.
能够通过 ==-XX:ParallelGCThreads== 参数来管制收集的线程数,过程也是 STW
Parallel Scavenge/Paraller Old
Parallel Scavenge 是新生代的收集器,采纳复制算法,多线程的收集器,Paraller Old 是老年代收集器,也是多线程收集,采纳标记 - 整顿算法。
主要参数
- -XX:UseParallelGC 开启
- -XX:MaxGCPauseMillis 最大垃圾收集进展工夫
- -XX:GCTimeRatio 设置吞吐量大小
可控的吞吐量
通过参数 -XX:MaxGCPauseMillis 管制最大 GC 进展工夫。-XX:GCTimeRatio 设置吞吐量大小
晋升吞吐量能够高效利用 CPU 工夫,尽快实现程序工作。
自适应 GC 策略
解决提供了下面两个参数管制吞吐量大小以外,Parallel Scavenge 还能够通过 -XX:+UseAdptiveSizePolicy, 开启指示仪 GC 策略。关上后,就不再须要手动设置新生代大小,Eden/Surivor 比例等参数,虚构机会依据零碎运行状况,动静调整这些参数,从而达到最优的进展工夫,和最高吞吐量。
CMS
- 1. 初始标记 :只标记 GC Roots 能间接关联到的对象,会 STW。
- 2. 并发标记 :进行 GC Roots Tracing 的过程,GC 线程和用户线程同时执行。
- 3. 从新标记 :修改并发标记期间,因程序运行导致标记发生变化的那些对象。会 STW
- 4. 并发革除 :并发回收垃圾对象(GC 线程和用户线程同时执行)
- 5. 并发重置:清理本次 CMS GC 上下文信息,为下一次 GC 做筹备。
长处:低进展,并发执行
毛病:
- 因为并发执行,所以对 CPU 资源压力大。
- 无奈解决在收集过程中产生的浮动垃圾。
- 因为采纳标记 - 革除算法,所以会才是大量内存碎片。而导致在须要调配大对象是内存不足,触发 FullGC。
应用 -XX:UseConcMarkSweepGC 开启 ParNew+CMS/SerialOld 收集器组合,即新生代采纳 ParNew, 老年代 CMS,当 CMS 出错后,SerialOld 备用。
为了解决内存碎片问题,CMS 能够通过 -XX:+UseCMSCompactAtFullCollection,强制 JVM 在 FullGC 实现后对老年代进行压缩,执行碎片整顿,同时会 STW。想要缩小 STW 次数,能够配置 -XX:+CMSFullGCsBeforeCompaction 参数,在执行设置的次数后,JVM 再在老年代进行空间整顿。
JDK9 曾经将 CMS 标记为弃用,在 JDK14 中曾经将 CMS 删除。
G1
JDK7 推出的新一代收集器,
是一个面向服务端利用的收集器。相比下面的收集器,
G1 作用在整个堆,而其余的收集器都是只作用在新生代或老年代。
G1 将 Java Heap 宰割成一些大小雷同的 Region, 通过参数 -XX:G1HeapRegionSize 指定 Region 的大小,取值范畴为 1~32M,应为 2 的 N 次幂。G1 对每个 Region 做了分类,别离包含:Eden,Surivor,Old,Humongous, 其中 Humongous 相当于一个大的 Old,用来寄存大对象。
如下图:G1 的堆内存布局和传统的堆内存布局不同。
G1 将空间分成多个区域,跟踪每个区域外面的垃圾沉积的价值大小,构建一个优先列表,优先收集垃圾最多的区域,这也是它为什么叫 Garbage-First 的起因。
G1 与 CMS 相比的,有何特点
- 并发与并行:充沛的利用多 CPU,缩短 STW 工夫。并发标记阶段可与用户线程并发执行,最终标记阶段 GC 线程可并行执行。
- 分代收集:G1 能够不与其余垃圾收集器配合,独立实现整个 GC 堆的垃圾收集。
- 空间整合:G1 整体看是采纳“标记 - 整顿”算法实现的,部分也有 Eden 和 Surivor Region 看上去是采纳“复制”算法来实现。整个过程防止了产生内存碎片。
- 进展工夫可管制:G1 除了谋求低进展,还能建设可预测的进展工夫模型,能让用户指定在一个时间段内,耗费在收集上的工夫不超过一个时间段。
G1 垃圾收集模式
Young GC
- 1. 所有 Eden Region 都满了时,就会触发 Young GC
- 2.Eden Region 里的对象会转移到 Surivor Region
- 3. 原 Surivor Region 中的对象转移到另一个 Surivor, 或者降职到 Old Region
- 4. 闲暇 Region 会被放入闲暇列表,期待下次应用。
Mixed GC
当老年代占整个 Heap 的大小百分比达到一个阀值(-XX:InitialingHeapOccupancyPercent)时, 默认 45%,就会触发 Mixed GC,
收集整个新生代以及局部老年代。
Mixed GC 回收过程
- 1. 初始标记:只标记 GC Roots 能关联到的对象。批改 TAMS 的值,此阶段会 STW.
- 2. 并发标记: 从 GC Root 开始对堆内存中的对象进行可达性剖析,找出存活的对象,此过程能够和用户线程并发执行。
- 3. 最终标记:修改在并发标记阶段用户线程持续运行导致标记产生变动的记录。此过程 STW,然而能够并行执行。
- 4. 筛选回收:对各个 Region 的回收价值和老本进行排序,依据用户冀望的进展工夫制订回收打算。
Full GC
当复制对象内存不够时,或无奈调配足够空间时,触发 Full GC
Full GC 模式是采纳 Serial Old 收集的,所以会 STW。
如何缩小 Full GC?
- 增大 -XX:G1ReserverPercent, 来减少预留内存。
- 缩小 -XX:InitialingHeapOccupancyPercent, 当老年代达到这个值是就触发 Mixed GC,
- 减少 -XX:ConcGCThreads 并发阶段的线程数。
总结
对以上介绍的垃圾回收器总结
- Serial 串行,作用于新生代,采纳复制算法
- ParNew 并行,作用于新生代,采纳复制算法
- Serial Old 串行,作用于老年代,标记 - 整顿算法
- Parallel 并行,作用于新生代,复制算法
- Parallel Old 并行,作用于老年代,标记 - 整顿算法
- CMS 并发,作用于老年代,标记 - 革除算法
- G1 并发 + 并行,作用于整个堆,复制算法,标记 - 整顿
参考资料
《深刻了解 Java 虚拟机 JVM 高级个性与最佳实际》