JAVA GC 是JAVA虚拟机中的一个系统或者说是一个服务,专门是用于内存回收,交还给虚拟机的功能。
JAVA语言相对其他语言除了跨平台性,还有一个最重要的功能是JAVA语言封装了对内存的自动回收。俗称垃圾回收器。所以有时候我们不得不承认,我们写的每一行JAVA代码都是垃圾!
那么JAVA虚拟机中,对应内存的回收常见的有哪些的呢。 我知道的有:
标记清除法、标记整理算法、复制清除、分代收集;其中最常用的是分代收集。
标记清除法:
示意图:
初始状态是一块空白未被使用的内存。java语言类中声明的对象运行,在堆中使用内存块。红的和绿的部分。
过了一段时间,垃圾回收器工作,检测哪些对象在使用,哪些对象不使用了进行标记。绿色部分为存活对象,红色部分为被标记。
然后将红色可回收的对象,清除释放内存。
优点:效率很高,快速。
缺点:使内存碎片化,不方便分配大内存。
标记整理算法:
示意图:
标记整理法比标记法多做一步工作,就是标记清除后,对碎片进行整理,移动。空出更大的内存块。
优点:尽可能空闲出较大块的内存块。
缺点:垃圾回收器比较忙碌。在移动内存对象过程中,消耗性能和效率。
复制清除:
示意图:
复制清除法可用内存一分为二,每次只用一块,当这一块内存不够用时,便触发 GC,将当前存活对象复制(Copy)到另一块上,以此往复 Java 对象的存活时间极短。
Java 对象高达 98% 是朝生夕死的,这也意味着每次 GC 可以回收大部分的内存,需要复制的数据量也很小,这样它的执行效率就会很高。
优点:效率高
缺点:只能使用50%的内存。
分代收集法:
示意图:
分代收集法:
根据对象一般都是先在 Eden区创建
当Eden区满,触发 Young GC,此时将 Eden中还存活的对象复制到 S0中,并清空 Eden区后继续为新的对象分配内存
当Eden区再次满后,触发又一次的 Young GC,此时会将 Eden和S0中存活的对象复制到 S1中,然后清空Eden和S0后继续为新的对象分配内存
每经过一次 Young GC,存活下来的对象都会将自己存活次数加1,当达到一定次数后,会随着一次 Young GC 晋升到 Old区
Old区也会在合适的时机进行自己的 GC。
新生代(Young generation)
绝大多数新创建的对象都会被分配到这里,这个区域触发的垃圾回收称之为:Minor GC 。
空间结构:
默认情况下,新生代(Young generation)、老年代(Old generation)所占空间比例为 1 : 2 。
它被分成三个空间:
· 1个伊甸园空间(Eden)
· 2个幸存者空间(Fron Survivor、To Survivor)
默认情况下,新生代空间的分配:Eden : Fron : To = 8 : 1 : 1
为什么要这样的布局?是因为新生代里的对象绝大多数是朝生夕死的,非常适合使用标记-复制算法,后面的回收算法章节会详细说。
新生代GC收集的执行顺序如下:
1、绝大多数新创建的对象会存放在伊甸园空间(Eden)。
2、在伊甸园空间执行第 1 次GC(Minor GC)之后,存活的对象被移动到其中一个幸存者空间(Survivor)。
3、此后每次 Minor GC,都会将 Eden 和 使用中的Survivor 区域中存活的对象,一次性复制到另一块空闲中的Survivor区,然后直接清理 Eden 和 使用过的那块Survivor 空间。
4、从以上空间分配我们知道,Survivor区内存占比很小,当空闲中的Survivor空间不够存放活下来的对象时,这些对象会通过分配担保机制直接进入老年代。
5、在以上步骤中重复N次(N = MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。
我们需要重点记住的是,新创建的对象,是保存在伊甸园空间的(Eden)。那些经历多次GC依然存活的对象会经由幸存者空间(Survivor)转存到老年代空间(Old generation)。
也有例外出现,对于一些大的对象(指需要占用大量连续内存空间的对象)则直接进入到老年代。
Java提供了 -XX:PretenureSizeThreshold 来指定对象大于这个值,直接分配到老年代。
老年代(Old generation)
对象在新生代周期中存活了下来的,会被拷贝到这里。通常情况下这个区域分配的空间要比新生代多。正是由于对象经历的GC次数越多越难回收,加上相对大的空间,发生在老年代的GC次数要比新生代少得多。这个区域触发的垃圾回收称之为:Major GC 或者 Full GC
老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间里绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor 空间中熬过来的,它们绝不会轻易狗带。因此,Major GC 或 Full GC 发生的次数不会有 Minor GC 那么频繁。
2、老年代使用的是标记-整理算法,清理完成内存后,还得把存活的对象重新排序整理成连续的空间,成本更高。
优点:低频率触发GC,更好的触发内存回收。
下面才是重点:知道了原理那怎么使用或者制定使用GC。java虚拟机中有很多种GC,我们可以通过配置指定,语法:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:UseParallelGC -XX:ParallelGCThreans = 20
-Xmx3800m:最大堆大小
-Xms3800m:初始堆大小,此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g: 设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,
不能无限生成,经验值在3000~5000左右。
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即该配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreans = 20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值的配置最好与处理器数目相等。
--------------------------------------------------------------------------------------------------------------------------
常见配置汇总
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
堆设置
收集器设置
垃圾回收统计信息
并行收集器设置
并发收集器设置
调优总结
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
-XX:MaxHeapFreeRatio=30
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时
间。最优化的方案,一般需要参考以下数据获得:减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
并发垃圾收集信息
持久代并发收集次数
传统GC信息
花在年轻代和年老代回收上的时间比例
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年轻代大小选择
年老代大小选择
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
-XX:MaxHeapFreeRatio=30
-------------------------------------------------------------------------------------------------------
java虚拟机常见七种垃圾回收器用法:
其中JAVA8的默认垃圾回收器: Parallel 垃圾回收器(并行垃圾回收器)
1、串行垃圾回收器:UseSerialGC
这是最简单的 GC 实现。它基本上是为单线程环境设计的。此GC垃圾回收实现在运行时冻结所有应用程序线程。它使用单线程进行垃圾收集。因此,在服务器环境等多线程应用程序中使用它并不是一个好主意。
要启用串行垃圾收集器,我们可以使用以下参数:
java -XX:+UseSerialGC -jar App.java
2、并行垃圾回收器:UseParallelGC
并行垃圾收集器也称为吞吐量收集器。与串行垃圾收集器不同,它使用多个线程进行垃圾收集。与串行垃圾收集器类似,这也会在执行垃圾收集时冻结所有应用程序线程。垃圾收集器最适合那些可以承受应用程序暂停的应用程序。
要启用并行垃圾收集器,我们可以使用以下参数:
java -XX:+UseParallelGC -jar App.java
可以使用命令行选项控制垃圾收集器线程的数量
-XX: ParallelGCThreads=<N>
使用命令行选项指定最大暂停时间目标(两次GC之间的间隔 [以毫秒为单位] )
-XX: MaxGCPauseMillis=<N>
最大吞吐量目标(根据执行垃圾收集所花费的时间与在垃圾收集之外花费的时间来衡量)由命令行选项指定
-XX:GCTimeRatio=<N>
使用选项-Xmx <N>指定最大堆占用空间(程序运行时所需的堆内存量)。
3、CMS垃圾回收器:USeParNewGC
并发标记扫描 (CMS) 垃圾收集器使用多个垃圾收集器线程进行垃圾收集。它扫描堆内存以标记要驱逐的实例,然后扫描标记的实例。它专为喜欢更短的垃圾收集暂停的应用程序而设计,并且可以在应用程序运行时与垃圾收集器共享处理器资源。
CMS 垃圾收集器仅在以下两种情况下持有所有应用程序线程
在标记老年代空间中的引用对象期间。
堆内存的任何变化与垃圾回收并行
与并行垃圾收集器相比,CMS 收集器使用更多的 CPU 来确保更好的应用程序吞吐量。如果我们可以分配更多的 CPU 以获得更好的性能,那么 CMS 垃圾收集器是优于并行收集器的首选。
要启用 CMS 垃圾收集器,我们可以使用以下参数:
java -XX:+USeParNewGC -jar App.java
4、G1垃圾回收器
G1(垃圾优先)垃圾收集器专为在具有大内存空间的多处理器机器上运行的应用程序而设计。它从JDK7 Update 4和更高版本开始可用。
它将堆内存分成多个区域,并在其中并行收集。G1 也会在回收内存后立即压缩空闲堆空间。但是 CMS 垃圾收集器会在停止世界 (STW) 情况下压缩内存。G1收集器将取代CMS收集器,因为它更高效。
在 G1 中收集器包含两个阶段;
打标
扫地
与其他收集器不同,G1收集器将堆划分为一组大小相等的堆区域,每个区域都是连续的虚拟内存范围。在执行垃圾回收时,G1显示了一个并发的全局标记阶段,以确定整个堆中对象的活跃度。
标记阶段完成后,G1知道哪些区域大部分是空的。它首先在这些区域收集,这通常会产生大量可用空间。这就是为什么这种垃圾收集方法被称为 Garbage-First 的原因。
要启用 G1 垃圾收集器,我们可以使用以下参数:
java -XX:+UseG1GC -jar App.java
5.epsilon垃圾回收器UseEpsilonGC
Epsilon 是一个不可操作的或被动的垃圾收集器。它为应用程序分配内存,但不收集未使用的对象。当应用程序耗尽 Java 堆时,JVM 将关闭。这意味着 Epsilon 垃圾收集器允许应用程序耗尽内存并崩溃。
此垃圾收集器的目的是测量和管理应用程序性能。活动垃圾收集器是在 JVM 中与您的应用程序一起运行的复杂程序。Epsilon 消除了 GC 对性能的影响。没有 GC 周期或读取或写入障碍。使用 Epsilon GC 时,代码是独立运行的。Epsilon 有助于可视化垃圾收集如何影响应用程序的性能以及内存阈值是多少,因为它会在耗尽时显示。例如,如果我们认为我们的应用程序只需要 1 GB 的内存,我们可以使用 -Xmx1g 运行它并查看行为。如果该内存分配不足,请使用堆转储重新运行它。请注意,我们必须启用此选项才能获得堆转储。
XX:HeapDumpOnOutOfMemoryError
如果我们需要充分利用应用程序的性能,Epsilon 可能是 GC 的最佳选择。但是我们需要对我们的代码如何使用内存有一个完整的了解。如果它几乎不产生垃圾,或者您确切知道它在运行期间使用了多少内存,那么 Epsilon 是一个可行的选择。
要启用 Epsilon 垃圾收集器,我们可以使用以下参数:
java -XX:+UseEpsilonGC -jar App.java
6、Z垃圾回收器UseZGC
ZGC 并发执行所有昂贵的工作,不会停止应用程序线程的执行超过 10 毫秒,这使其适用于需要低延迟和/或使用非常大堆的应用程序。根据 Oracle 文档,它可以处理数 TB 的堆。Oracle 在 Java 11 中引入了 ZGC。Z 垃圾收集器在其线程中执行其循环。它平均暂停应用程序 1 毫秒。G1 和 Parallel 收集器平均大约 200 毫秒。
在 Java 12 中,即使 Z 仍处于实验状态,Oracle 也添加了性能修复和类卸载。它仅在 64 位 Linux 上可用。但是,ZGC 通过一种称为指针着色的技术来利用 64 位指针。彩色指针存储有关堆上对象的额外信息。这是它仅限于 64 位 JVM 的原因之一。
ZGC会尝试自己设置线程数,通常是对的。但是如果 ZGC 有太多的线程,它会饿死你的应用程序。如果它没有足够的,您将创建垃圾比 GC 收集它的速度更快。ZGC 的阶段说明了它如何在不影响应用程序内存增长的情况下管理大型堆。
要启用 Z 垃圾收集器,我们可以使用以下参数:
java -XX:+UseZGC -jar App.java
7、Shenandoah垃圾回收器
Shenandoah 是一个超低暂停时间的垃圾收集器,它通过与正在运行的 Java 程序同时执行更多的垃圾收集工作来减少 GC 暂停时间。CMS 和 G1 都执行活动对象的并发标记。Shenandoah 增加了并发压缩。
Shenandoah 使用内存区域来管理哪些对象不再使用,哪些对象是活动的并准备好进行压缩。Shenandoah 还为每个堆对象添加了一个转发指针,并使用它来控制对对象的访问。Shenandoah 的设计以并发 CPU 周期和空间换取暂停时间的改进。转发指针使移动对象变得容易,但激进的移动意味着 Shenandoah 比其他 GC 使用更多的内存并且需要更多的并行工作。但它通过非常短暂的停顿来完成额外的工作。
Shenandoah 在许多小阶段处理堆,其中大部分与应用程序并发。这种设计使 GC 可以有效地管理大堆。
Shenandoah 提供与 ZGC 相同的优势,具有大堆但更多的调整选项。根据您的应用程序的性质,不同的启发式方法可能非常适合。它的暂停时间可能不如 ZGC 的那么短,但它们更容易预测。
要启用 Shenandoah 垃圾收集器,我们可以使用以下参数:
java -XX:+UseShenanodoahC -jar Application.java