1、JVM 的内存空间
在 Java 虚拟机规范中(具体章节请看“这里”),提及了如下几种类型的内存空间:
1.栈内存(Stack):每个线程私有的。
2.堆内存(Heap):所有线程公用的。
3.方法区(Method Area):有点像以前常说的“进程代码段”,这里面存放了每个加载类的反射信息、类函数的代码、编译时常量等信息。
4.原生方法栈(Native Method Stack):主要用于 JNI 中的原生代码,平时很少涉及。
2、垃圾回收机制简介
总的来说,对于jvm,垃圾是指运行程序中没有任何指针指向的对象,这个对象就需要被回收。
垃圾回收(Garbage Collection,简称GC)是内存管理的核心组成部分,它负责自动回收不再使用的内存空间。在Java中,程序员不需要手动释放对象占用的内存,一旦对象不再被引用,垃圾回收器就会在适当的时机回收它们所占用的内存。这样可以避免内存泄漏和野指针,从而大大减轻了程序员的负担,也使得Java成为一个相对安全、易于开发的编程语言。
防止内存泄漏:手动管理内存容易导致内存泄漏,而GC可以自动回收不再使用的对象,防止内存泄漏的发生。
提高开发效率:程序员不再需要关心内存释放的问题,可以更加集中精力在业务逻辑的实现上。
系统性能和稳定性:通过有效的垃圾回收策略,可以保证系统的性能和稳定性。
其实 Java 虚拟机规范中并未规定垃圾回收的相关细节。垃圾回收具体该怎么做,完全取决于各个 JVM 的设计者。所以,不同的 JVM 之间,GC 的行为可能会有一定的差异。

分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)[1]的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1) 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3) 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。
1.什么时候进行垃圾回收
一般情况下,当 JVM 发现堆内存比较紧张、不太够用时,它就会着手进行垃圾回收工作。但是要认清这样一个残酷的事实:JVM 进行 GC 的时间点是无法准确预知的。因为 GC 启动的时刻会受到各种运行环境因素的影响,随机性太大。
虽说无法准确预知,但如果想知道每次垃圾回收执行的情况,还是很方便的。可以通过 JVM 的命令行参数“-XX:+PrintGC”把相关信息打印出来。
另外,调用 System.gc() 只是建议 JVM 进行 GC。至于 JVM 到底会不会真的去做,只有天晓得。所以,通常不建议自己手动调用 System.gc(),还是让 JVM 自行决定比较好。另外,使用 JVM 命令行参数“-XX:+DisableExplicitGC”可以让 System.gc() 不起作用。
根据回收时间,我们可以引出另一个问题,对象的生命周期
在Java中,对象的生命周期包括以下几个阶段:
创建 (Creation): 当使用new关键字创建对象时,对象进入创建阶段。
使用 (Usage): 在对象被引用并使用的期间,对象处于使用阶段。
不可达 (Unreachable): 当对象不再被任何强引用指向时,它变成不可达状态,可能会被垃圾回收。
回收 (Collection): 垃圾回收器会在适当的时机回收不可达对象的内存空间。
终结 (Finalization): 如果对象有定义finalize方法,它会在被回收前被调用。
死亡 (Death): 对象的内存被回收,对象完成其生命周期。
public class ObjectLifecycle {
public static void main(String[] args) {
ObjectLifecycle obj = new ObjectLifecycle(); // 创建阶段
// 使用阶段
obj = null; // 不可达阶段
System.gc(); // 触发垃圾回收,进入回收阶段
// 对象进入死亡阶段
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("对象终结阶段,finalize方法被调用");
}
}
2.谁来负责垃圾回收
一般情况下,JVM 会有一个或多个专门的垃圾回收线程,由它们负责清理回收垃圾内存。
3.如何发现垃圾对象
1)引用计数法
引用计数法是为对象添加一个引用计数器,然后用一块额外的内存区域来存储每个对象被引用的次数,当对象每有一个地方引用它时,那我们对该对象的引用计数就会加1,反之每有一个引用失效时,我们对该对象的引用计数就会减1,当对象的被引用次数为0时,那么我们可以认为这个对象是不会被再次使用了,通过这种方式我们能快速直观的定位到这些可回收的对象,从而进行清理。
但是,两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
}
}

2)可达性分析法
引用计数法虽然很直观高效,但是通过引用计数法是没办法扫描到一种特殊情况下的“可回收”对象,这种特殊情况就是对象循环引用的时候,比如A对象引用了B,B对象引用了A,除此之外他们两个没有被任何其他对象引用,那么其实这部分对象也属于“可回收”的对象,但是通过引用计数法是没办法定位的。另外一个方面是引用计数法需要额外的空间记录每个对象的被引用的次数,这个引用数也需要去额外的维护。

垃圾回收线程采用可达性分析法从“根集(Root Set)”开始进行对象引用的遍历。所谓的“根集”,就是正在运行的线程中,可以访问的【引用变量】的集合(比如所有线程当前函数的参数和局部变量、当前类的成员变量等等)。垃圾回收线程先找出被根集直接引用的所有对象(不妨叫集合1),然后再找出被集合1直接引用的所有对象(不妨叫集合2),然后再找出被集合2直接引用的所有对象......如此循环往复,直到把能遍历到的对象都遍历完。
凡是从“根集”通过上述遍历可以到达的对象,都称为可达对象或有效对象;反之,则是不可达对象或失效对象(也就是垃圾)。
这个收集器成为“跟踪收集器”,叫做可达性分析算法。

哪些对象对象我们称之为"GC Roots"对象呢
当然普通的对象肯定是不行的,如果要作为GC Roots 对象那么它自身肯定得满足一个条件,那就是他自己一定在很长一段时间内都不会被GC 回收掉。那么只有满足这个条件的对象才可能作为GC Roots了,GC Roots的类型大致如下:
1、在虚拟机栈(栈帧中的本地变量表)中引用的对象:
Java public void method() { Object localVariable = new Object(); // localVariable是GC Roots }
2、方法区中静态属性引用的对象。
Java public class MyClass { private static Object staticObject = new Object(); // staticObject是GC Roots }
3、方法区中常量引用的对象。
Java public class MyClass { private static final String CONSTANT_STRING = "constant"; // CONSTANT_STRING是GC Roots }
4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象:
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
5、虚拟机内部的引用对象(类记载器、基本数据对应的Class对象,异常对象)。
6、所有被同步锁(Synchronnized)持有的对象。
Javapublic synchronized void synchronizedMethod() { // 当前对象(this)在执行同步方法时是GC Roots }
7、描述虚拟机内部情况的对象(如 JMXBean、JVMTI中注册的回调、本地缓存代码)。
8、垃圾搜集器所引用的对象。4.引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java中有四种类型的引用,它们对垃圾回收的影响不同:
强引用 (Strong Reference): 最常见的引用类型,只要对象有强引用指向,它就不会被垃圾回收。
软引用 (Soft Reference): 软引用可以帮助垃圾回收器回收内存,只有在内存不足时,软引用指向的对象才会被回收。
弱引用 (Weak Reference): 弱引用指向的对象在下一次垃圾回收时会被回收,不管内存是否足够。
虚引用 (Phantom Reference): 虚引用的主要用途是跟踪对象被垃圾回收的状态,虚引用指向的对象总是可以被垃圾回收。
javaCopy code
import java.lang.ref.*;
public class ReferenceTypes {
public static void main(String[] args) {
Object strongRef = new Object(); // 强引用
SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>()); // 虚引用
System.gc(); // 触发垃圾回收
System.out.println("Strong Reference: " + strongRef);
System.out.println("Soft Reference: " + softRef.get());
System.out.println("Weak Reference: " + weakRef.get());
System.out.println("Phantom Reference: " + phantomRef.get());
}
}
5.如何清理/回收垃圾
通过上述阶段,就把垃圾对象都找出来。然后垃圾回收线程会进行相应的清理和回收工作,包括:把垃圾内存重新变为可用内存、进行内存的整理以消除内存碎片、等等。
1)垃圾回收算法
1-标记-清除 (Mark-Sweep)


标记清除算法分为两个主要步骤:标记和清除。
标记阶段: 在标记阶段,垃圾回收器会从GC Roots开始,遍历所有可达的对象,并标记它们为活动对象。
清除阶段: 在清除阶段,垃圾回收器会遍历整个堆,回收所有未被标记的对象的内存。
该算法有两个问题:
效率问题:标记和清除过程的效率都不高;
空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
2-复制 (Copying)


复制算法将堆内存分为两个相等的区域,只使用其中一个区域。当这个区域的内存用完时,垃圾回收器会将所有活动对象复制到另一个区域,并回收原区域的所有内存。
优点: 减少内存碎片,提高空间利用率。
缺点: 减半了可用的堆内存,可能增加垃圾回收的频率。
3-标记-整理 (Mark-Compact)


标记整理算法是标记清除算法的改进版本。它在标记和清除的基础上增加了整理阶段,将所有活动对象向一端移动,从而消除内存碎片。
优点: 解决了内存碎片化问题,提高了空间利用率。
缺点: 移动对象增加了额外的开销。
4-分代收集 (Generational Collection)

早期的 JVM 是不采用分代技术的,所有被 GC 管理的对象都存放在同一个堆里面。这么做的缺点比较明显:每次进行GC都要遍历所有对象,开销很大。其实大部分的对象生命周期都很短(短命对象),只有少数对象比较长寿;在这些短命对象中,又只有少数对象占用的内存空间大;其它大量的短命对象都属于小对象(很符合二八原理)。
有鉴于此,从 JDK 1.2 之后,JVM 开始使用分代的垃圾回收(Generational Garbage Collection)。JVM 把 GC 相关的内存分为“年老代”(Tenured)和“年轻代”(Nursery)、“持久代”(Permanent,对应于 JVM 规范的“方法区”)。【大部分】对象在刚创建时,都位于“年轻代”。如果某对象经历了几轮 GC 还活着(大龄对象),就把它移到“年老代”。另外,如果某个对象在创建时比较大,可能就直接被丢到年老代。经过这种策略,使得年轻代总是保存那些短命的小对象。在空间尺寸上,“年轻代”相对较小,而“年老代”相对较大。
因为有了分代技术,JVM 的 GC 也相应分为两种——主要收集(Major Collection)和次要收集(Minor Collection)。“主要收集”同时清理年老代和年轻代,因此开销很大,不常进行;“次要收集”仅仅清理年轻代,开销很小,经常进行。
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)[1]的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1) 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
3) 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。
基于上述假说:
新生代: 使用复制算法,因为新生代中的对象生命周期较短。
老年代: 使用标记整理或标记清除算法,因为老年代中的对象生命周期较长,且数量较少。
新生代(Young Generation)的回收算法(以复制算法为主)
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
老年代(Tenured Generation)的回收算法(以标记-清除、标记-整理为主)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC,Major GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
永久代(Permanet Generation)的回收算法
用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面3个条件:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
加载该类的ClassLoader已经被回收;
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3、GC
垃圾收集器全称:Garbage Collection,简称GC,其实就是各种垃圾算法的一种实现。目前还没有符合所有场景的收集器出现。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
1.Serial 收集器

JVM第一个垃圾收集器,JDK 1.3.1之前都是有这个收集器。可以作用新生代和老年代。它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
算法:新生代-使用复制算法,老年代-使用标记-整理算法。
特点:串行单线程、不支持并发、会导致"stop the world"。
//配置如下使用
-XX:+UseSerialGC- ParNew 收集器

它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。parNew是针对client的版本。
特点:多线程、唯一能够与cms搭配使用的收集器;
算法:年轻代采用复制算法、年老代采用标记-整理
//强制指定使用ParNew;
-XX:+UseParNewGC
//指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
-XX:ParallerGCThreads
- Parallel Scavenge 收集器

与 ParNew 一样是多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
Parallel Scavenge是后个多线程新生代收集器,使用的算法是复制算法。实现方式是当垃圾达到一个可控制的吞吐量(Throughput)则启动垃圾回收。
特点:作用于新生代、老年代由Serial搭配使用,可以有效的减少停顿时间提升用户体验。
算法:新生代复制算法,老年代标记整理
//注意:启动后不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了
-XX:+UseAdaptiveSizePolicy
//使用Parallel收集器+老年代串行
-XX:+UseParallelGC
//启用并行压缩
-XX:+UseParallelOldGC。
- Serial Old 收集器

是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
- Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
- CMS 收集器(Concurrent Mark Sweep)

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
并发清除: 不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
//新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:+UseConcMarkSweepGC
//设定CMS的线程数量
-XX:ParallelCMSThreads- G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
·初始标记(Initial M arking):
仅仅只是标记一下GC Roots能直接关联到的对象。
·并发标记(Concurrent Marking):
从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
·最终标记(Final M arking):
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
·筛选回收(Live Data Counting and Evacuation):
负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
//启用G1垃圾回收器 -XX:+UseG1GC
查看jdk所用内存垃圾回收器
通过命令:
java -XX:+PrintCommandLineFlags -version
| 收集器 | 类型 | 区域 | 算法 | 特性 | 场景 |
|---|---|---|---|---|---|
| Seial | 串行 | 新生代 | 复制算法 | 响应速度优化 | 单CPU环境下的Client模式 |
| Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优化 | 单CPU环境下的Client模式、 CMS的后备预案 |
| ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式 下与CMS配合 |
| Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 后台运算不需要太多交互任务 |
| Parallel Scavenge old | 并行 | 老年代 | 标记整理 | 吞吐量优先 | 后台运算不需要太多交互任务 |
| CMS | 并发 | 老年代 | 标记清除 | 响应速度优先 | 主要用于互联网站或B/S系统 服务端上的Java应用 |
| G1 | 并发 | 新生代/老年代 | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
具备如下特点:
空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
GC的参数整理
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :在新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
3、GC对性能会有什么影响
刚才介绍了GC的大致原理,那GC对性能会造成哪些影响?主要有如下几个方面:
1.造成当前运行线程的停顿
早期的 GC 比较弱智。在它工作期间,所有其它的线程都被暂停(以免影响垃圾回收工作)。等到 GC 干完活,其它线程再继续运行。所以,早期 JDK 的 GC 一旦开始工作,整个程序就会陷入假死状态,失去各种响应。
经过这些年的技术改进(包括采用分代技术),从 JDK 1.4 开始,GC 已经比较精明了。在它干活期间,只是偶尔暂停一下其它线程的运行(从长时间假死变为暂时性休克)。
2.遍历对象引用的开销
试想如果JVM中的对象很多,那遍历完所有可达对象肯定是比较费劲的工作,这个开销可不小。
3.清理和回收垃圾的开销
遍历完对象引用之后,对垃圾的清理和回收也有较大的开销。这部分开销可能包括复制内存块、更新对象引用等等。
4、几种收集器
1.两个性能指标
因为今天讨论的是性能的话题,必然会提到衡量 GC 性能的两个重要指标:吞吐量(Throughput)和停顿时间(Pause Time)。吞吐量这个词不是很直观,解释一下:就是 JVM【不用于】GC 的时间占总时间的比率。“吞吐量”是越大越好,“停顿时间”是越小越好。
不同的应用程序对这两个指标的关注点不一样(后面具体会说),也就是所谓的“众口难调”。很多 JVM 厂商为了迎合“众口”,不得不提供多种几种垃圾收集器供使用者选择。不同的收集器,采用的收集策略是不一样的,回顾上面讲到的几种收集器,下面从分类角度再讨论一下。
串行收集器(Serial Collector)
使用命令行选项“-XX:+UseSerialGC”指定。
这种收集器是最传统的收集器。它使用单线程进行垃圾回收,对于“单 CPU 机器”比较合适。另外,小型应用或者对上述两个指标没有特殊要求的,可以使用串行收集器。
并行收集器(Parallel Throughput Collector)
顾名思义,这种收集器使用多个线程进行垃圾回收以达到高吞吐量。垃圾回收线程的数量通过命令行选项“-XX:ParallelGCThreads=n”指定。可以设置该数值以便充分利用“多CPU 或 多核”。
当使用命令行选项“-XX:+UseParallelGC”时:它会针对年轻代使用多个垃圾回收线程,对年老代依然使用单个线程的串行方式。此选项最早在JDK 1.5引入。
当使用命令行选项“-XX:+UseParallelOldGC”时:它针对年轻代和年老代都使用多个垃圾回收线程的方式。不过此选项从 JDK 1.6 才开始引入。
并发收集器(Concurrent Low Pause Collector)
使用命令行选项“-XX:+UseConcMarkSweepGC”指定。
这种收集器优先保证程序的响应。它会尽量让垃圾回收线程和应用自身的线程同时运行,从而降低停顿时间。此选项从JDK 1.4.1开始支持。
增量收集器(Incremental Collector)
自从 JDK 1.4.2 以来,SUN 官方就停止维护该收集器了。不再赘述。
2. GC 对比
从G1收集器的设计,我们可以看到现代垃圾收集器的发展趋势是追求处理应用的内存分配速度(Allocation Rate),而不是一次性清理整个Java堆。这种设计允许应用程序在分配内存的同时,收集器也在进行垃圾收集。只要收集速度能跟上对象分配速度,系统就能高效运行。这种新设计思路自G1收集器开始流行,标志着垃圾收集器技术的一个里程碑。
相较于CMS,G1有许多优点。除了可以指定最大停顿时间、分Region的内存布局和按收益动态确定回收集等创新设计外,从传统算法理论看,G1具有更多发展潜力。G1是基于“标记-整理”算法实现的收集器,局部(两个Region之间)是基于“标记-复制”算法实现,这两种算法保证G1运作期间不会产生内存空间碎片,能提供规整的可用内存,利于程序长时间运行。
然而,G1并非全方位压倒CMS。
比如,G1的内存占用(Footprint)和程序运行时的额外执行负载(Overload)都较CMS高。
G1和CMS都使用卡表处理跨代指针,但G1的卡表实现更复杂,每个Region都必须有一份卡表,可能占用堆容量的20%或更多。
执行负载方面,由于两收集器的实现细节不同,用户程序的运行负载也有不同。它们都使用写屏障,CMS用写后屏障更新维护卡表;G1除此之外,为实现原始快照搜索(SATB)算法,还需使用写前屏障跟踪并发时的指针变化。虽然原始快照搜索减少了并发标记和重新标记阶段的消耗,避免了CMS在最终标记阶段停顿时间过长的问题,但确实增加了用户程序的额外负担。
吞吐量比较

就吞吐量而言,JDK 8和JDK 11之间没有太大的差异,在并行方面,JDK 17比JDK 8高约15%。在G1方面,JDK 17比JDK 8高18%。ZGC在JDK 11中引入,与JDK 11相比,JDK 17提高了20%以上。
暂停时间比较

JDK 17中的ZGC远低于目标:亚毫秒暂停时间。G1的目标是在延迟和吞吐量之间保持平衡,远低于其默认目标:200毫秒的暂停时间。ZGC的设计是确保暂停时间不随堆大小的变化而改变,我们可以看到当堆扩展到128GB时会发生什么。从暂停时间的角度来看,G1在处理较大堆方面比Parallel更好,因为它可以保证暂停时间达到特定目标。
资源使用情况

上图比较了三种不同收集器的峰值本机内存使用情况。由于Parallel和ZGC在这个角度上都比较稳定,我们应该看一下具体的数字。我们可以看到,G1在这个领域有所改进,主要是因为所有的功能和增强都提高了内存集管理的效率。
Java 8 :
总体来说,哪款收集器更好,往往需要针对具体场景进行定量比较。根据实践经验,小内存应用上CMS可能表现更好,而大内存应用上G1能发挥优势。这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,不过也需根据实际情况进行测试,以得出最合适的结论。
3. Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
一、调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
二、老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
三、空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
四、JDK 1.7 及以前的永久代空间不足(1.7之后元空间不足)
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
五、Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
5、如何降低GC的影响?
1.尽量减少堆内存的使用
由于 GC 是针对存储在堆内存的对象进行的。如果在程序中减少引用对象的分配(也就相应降低堆内存分配),那对于提高 GC 的性能是很有帮助的。
2.设置合适的堆内存大小
JVM 的堆内存是有讲究的,不能太大也不能太小。如果堆内存太小,JVM 老是感觉内存不够用,可能会导致频繁进行垃圾回收,影响了性能;如果堆内存太大,以至于操作系统的大部分物理内存都被 JVM 自个儿霸占了,那可能会影响其它应用程序甚至操作系统本身的性能。
另外,年轻代的大小(或者说“年轻代”与“年老代”的比值)对于 GC 的性能也有明显影响。如果年轻代太小,可能导致次要收集很频繁;如果年轻代太大,导致次要收集的停顿很明显。
JVM 提供了若干和堆内存大小相关的命令行选项,具体如下:
------------------------------
-Xms 设置初始堆内存
-Xmx 设置最大堆内存
-Xmn 设置年轻代的大小
-XX:NewRatio=n 设置年轻代与年老代的比例为“n”
-XX:NewSize=n 设置年轻代大小为“n”------------------------------
一般情况下,JVM 的默认参数值已经够用。不建议轻易动用上述选项。如果非调整不可,一定要做深入的性能对比测试,保证调整后的性能确实优于默认参数值。
3.吞吐量和停顿的取舍
前面提到了不同应用的众口难调。常见的口味有两种:(1)看重吞吐量,对停顿时间无所谓;(2)侧重于停顿时间。
对于某些在后台的、单纯运算密集型的应用,属于第一种。比如某些科学计算的应用。这时候建议使用并行收集器。
对于涉及用户 UI 交互的、实时性要求比较高、程序需要快速响应的,属于第二种。比如某些桌面游戏、某些电信交换系统。这时候建议使用并发收集器。