JVM 是 Java 虚拟机 Java Virtual Machine 的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入 Java 语言虚拟机后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
从宏观上来说 JVM 内存区域分为三部分线程共享区域、线程私有区域、直接内存区域。
☞ 线程共享区域
堆区
:堆区 Heap 是 JVM 中最大的一块内存区域,基本上所有的对象实例都是在堆上分配空间。堆区细分为年轻代和老年代,其中年轻代又分为 Eden、S0、S1 三个部分,他们默认的比例是 8:1:1 的大小。
元空间
:在 Java 中用永久代来存储类信息,常量,静态变量等数据不是好办法,因为这样很容易造成内存溢出。同时对永久代的性能调优也很困难,因此在 JDK8 中把永久代去除了,引入了元空间 metaspace,原先的 class、field 等变量放入到 metaspace。
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。
移除永久代官方解释 This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.【移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代】
☞ 直接内存区域
直接内存
:一般使用 Native 函数操作 C++ 代码来实现直接分配堆外内存,不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。这块内存不受 Java 堆空间大小的限制,但是受本机总内存大小限制所以也会出现 OOM 异常。分配空间后避免了在 Java 堆区跟 Native 堆中来回复制数据,可以有效提高读写效率,但它的创建、销毁却比普通 Buffer 慢。
☞ 线程私有区域
程序计数器
:是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈
:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
:与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。
☞ 什么是类加载 类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
☞ 类的生命周期 类加载的过程包括了五个阶段: ♞ 加载:查找并加载类的二进制数据 ♞ 验证:确保被加载的类的正确性 ♞ 准备:为类的静态变量分配内存,并将其初始化为默认值 ♞ 解析:把类中的符号引用转换为直接引用 ♞ 初始化:为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。 在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
☞ 类的加载方式 ♞ 命令行启动应用时候由JVM初始化加载 ♞ 通过 Class.forName() 方法动态加载 ♞ 通过 ClassLoader.loadClass() 方法动态加载
☞ 双亲委派模型 双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
☞ 双亲委派机制 ♞ 当 AppClassLoader 加载一个 class 时,它首先不会自己去加载这个类,而是委派给父类加载器 ExtClassLoader 去完成。 ♞ 当 ExtClassLoader 加载一个 class 时,它首先也不会自己去加载这个类,而是委派给 BootStrapClassLoader 去完成。 ♞ 如果 BootStrapClassLoader 加载失败(例如在 $JAVA_HOME/jre/lib 里未查找到该 class),会使用 ExtClassLoader 来尝试加载; ♞ 若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException。
☞ 标配参数
参数 | 说明 |
---|---|
-version | 查看版本号 |
-help | 查看帮助 |
☞ -X 参数
参数 | 说明 |
---|---|
-Xint | 解释执行 |
-Xcomp | 第一次使用就编译成本地代码 |
-Xmixed | 混合模式 |
☞ -XX 参数
Boolean 类型:-XX:+/-属性值,+ 标识开启,- 标识关闭 KV 类型:-XX:key=value 查看当前运行程序配置:jinfo -flag 配置项 进程编号,进程编号使用 jps -l 查看
参数 | 说明 |
---|---|
-XX:+PrintGCDetails | 输出详细 GC 收集日志信息 |
-XX:+PrintFlagsInitial | 主要是查看初始默认值 |
-XX:+PrintFlagsFinal | 主要查看修改更新 |
-Xms | 初始内存大小,默认为物理内存的 1/64,等价于 -XX:InitialHeapSize |
-Xmx | 最大分配内存,默认为物理内存的 1/4,等价于 -XX:MaxHeapSize |
-Xss | 设置单个线程栈的大小,一般默认为 512k ~ 1024k,等价于 -XX:ThreadStackSize |
-Xmn | 设置年轻代内存大小,等价于 -XX:NewSize |
-XX:MetaspaceSize | 设置元空间大小,元空间的大小仅受本地内存限制 |
-XX:SurvivorRatio | Eden 区与 Survivor 区的大小比值,默认为 8,则比例为 8:1:1 |
-XX:NewRatio | 年轻代与年老代的比值,默认为 4,则比例为 4:1 |
-XX:MaxTenuringThreshold | 垃圾最大年龄,0 ~ 15 之间如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代 |
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,因此强引用是造成 Java 内存泄漏的主要原因之一。当内存空间不足的时候,Java 虚拟机宁愿抛出 OOM 异常,也不会靠随意回收有强引用的 对象来解决内存不足的问题,如果这个对象用完不用的时候一般将其赋值为 null,使用这种方式来淡化引用,一般认为就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
软引用是一种相对强引用弱化了一些的引用,需要用 java.lang.ref.SoftReference
类来实现,可以让对象豁免一些垃圾收集。如果一个对象只有软引用,当内存充足时不会回收它,当内存不足时就会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class SoftReferenceDemo {
public static void main(String[] args) {
Student stu = new Student("张三");
SoftReference<Student> studentSoftReference = new SoftReference<>(stu);
System.out.println(stu);
System.out.println(studentSoftReference.get());
try {
stu = null;
System.out.println("内存充足 ===========================");
System.gc();
System.out.println(stu);
System.out.println(studentSoftReference.get());
// -Xms5m -Xmx5m
byte[] bytes = new byte[10 * 1024 * 1024];
} finally {
System.out.println("内存不够 ===========================");
System.out.println(stu);
System.out.println(studentSoftReference.get());
}
}
}
弱引用需要用 java.lang.ref.WeakReference
类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class WeakReferenceDemo {
public static void main(String[] args) {
Student stu = new Student("张三");
WeakReference<Student> studentWeakReference = new WeakReference<>(stu);
System.out.println(stu);
System.out.println(studentWeakReference.get());
stu = null;
System.out.println("内存充足 ===========================");
System.gc();
System.out.println(stu);
System.out.println(studentWeakReference.get());
}
}
虚引用需要 java.lang.ref.PhantomReference
类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 ReferenceQueue 联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 gc 回收,用来实现比 finalization 机制更灵活的回收操作。换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java 技术允许使用 finalize 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class PhantomReferenceDemo {
public static void main(String[] args) {
Student stu = new Student("张三");
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Student> studentPhantomReference = new PhantomReference<>(stu, referenceQueue);
System.out.println(stu);
System.out.println(studentPhantomReference.get());
System.out.println(referenceQueue.poll());
stu = null;
System.out.println("内存充足 ===========================");
System.gc();
System.out.println(stu);
System.out.println(studentPhantomReference.get());
System.out.println(referenceQueue.poll());
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class StackOverflowErrorDemo {
public static void main(String[] args) {
method();
}
public static void method() {
method();
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
byte[] bytes = new byte[8 * 1024 * 1024];
}
}
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class MetaspaceDemo {
static class OomTest {
}
public static void main(String[] args) {
// -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=10m
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OomTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
return methodProxy.invokeSuper(obj, objects);
}
});
enhancer.create();
}
} catch (Exception e) {
System.err.println("创建 " + i + " 次后发生异常!");
e.printStackTrace();
}
}
}
这个错误是由于 JVM 花费太长时间执行 GC 且只能回收很少的堆内存时抛出的。根据 Oracle 官方文档,默认情况下,如果 Java 进程花费 98% 以上的时间执行 GC,并且每次只有不到 2% 的堆被恢复,则 JVM 抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class GCOverheadDemoDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new LinkedList<String>();
try {
while (true) {
list.add(String.valueOf(i++).intern());
}
} catch (Throwable throwable) {
System.out.println("********** i = " + i);
throwable.printStackTrace();
throw throwable;
}
}
}
Java 8 出现了 NIO,在写 NIO 程序的时候,经常使用 ByteBuffer 来读或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式。它可以使用 Native 函数库直接分配堆外内存,然后通过一个存做在 Java 里面的 DirectByteBuffer 对作为这块内存的引用进行操作。可以提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
♞ ByteBuffer.allocate(capability)
分配 JVM 堆内存,属于 GC 管辖范围,由于需要拷贝所以速度相对较慢。
♞ ByteBuffer.allocateDirect(capability)
分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象们就不会被回收。这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现 OutOfMemoryError,程序就直接崩溃了。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class DirectBufferMemoryDemo {
public static void main(String[] args) {
// -XX:MaxDirectMemorySize=5m
System.out.println("配置的 maxDirectMemory: " + (sun.misc.VM.maxDirectMemory() / 1024 / 1024) + "MB");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}
高并发请求服务器时,经常出现 java.lang.OutOfMemoryError: unable to create new native thread
,该 native thread 异常与对应的操作系统平台有关。导致原因是创建了太多的线程,一个应用进程创建多个线程,超过系统承载极限。服务器并不允许应用程序创建这么多的线程,linux 系统默认允许非 root 用户单个进程创建的线程数是1024个。应用创建线程超过这个数量,就会报该异常。解决方案要么就是减少线程创建,要么就是扩大上限。
/**
* @author Demo_Null
* @version 1.0
* @date 2021/3/5
* @desc //TODO
*/
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 1;; i++) {
System.out.println(">>>>>>>>>> i = " + i);
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "" + i).start();
}
}
}
☞ 引用计数法 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行,因此最简单的办法就是为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为 0,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加 1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。
☞ 可达性分析 可达性分析基本思路是把所有引用的对象想象成一棵树,通过一系列为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明该对象是不可用的。可作为 GC Roots 的有: ♞ 虚拟机栈中引用的对象,也叫做局部变量。 ♞ 方法区中静态属性引用的对象。 ♞ 方法区中常量引用的对象。 ♞ 本地方法栈中 JNI 引用的对象。
☞ 标记清除 该算法先标记,后清除,将所有需要回收的算法进行标记,然后清除;这种算法的缺点是:效率比较低;标记清除后会出现大量不连续的内存碎片,这些碎片太多可能会使存储大对象会触发 GC 回收,造成内存浪费以及时间的消耗。
☞ 复制 复制算法将可用的内存分成两份,每次使用其中一块,当这块回收之后把未回收的复制到另一块内存中,然后把使用的清除。这种算法运行简单,解决了标记-清除算法的碎片问题,但是这种算法代价过高,需要将可用内存缩小一半,对象存活率较高时,需要持续的复制工作,效率比较低。
☞ 标记整理 标记整理算法是针对复制算法在对象存活率较高时持续复制导致效率较低的缺点进行改进的,该算法是在标记-清除算法基础上,不直接清理,而是使存活对象往一端游走,然后清除一端边界以外的内存,这样既可以避免不连续空间出现,还可以避免对象存活率较高时的持续复制。这种算法可以避免 100% 对象存活的极端状况,因此老年代不能直接使用该算法。
☞ 分代收集 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
类型 | 说明 |
---|---|
串行垃圾收集器(Serial) | 它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境 |
并行垃圾收集器(Parallel) | 多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。 |
并发垃圾收集器(CMS) | 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用于对响应时间有要求的场景。 |
G1 | G1 垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。 |
☞ 约定参数
参数 | 说明 |
---|---|
DefNew | Default New Generation |
Tenured | Serial Old |
ParNew | Parallel New Generation |
PSYoungGen | Parallel Scavenge |
ParOldGen | Parallel Old Generation |
☞ Serial Copying
串行收集器是最古老的,最稳定,效率高的收集器,只使用一个线程去回收但其进行垃圾回收过程中可能会产生较长的停顿。虽然在收集垃圾的过程中需要暂停其他的工作线程,但是简单高效,对于单CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此 Serial 垃圾回收器依然是 Java 虚拟机运行在 Client 模式下默认的新生代垃圾回收器。
开启串行收集器的 JVM 参数是 -XX:+UseSerialGC
。开启后会使用:Serial(Young区) + Serial Old(Old区) 的收集器组合。表示新生代、老年代都会使用串行回收收集器,新生代用复制算法,老年代用标记整理算法。
32 位 Windows 操作系统,不论硬件如何都默认使用 JVM 的 Client 模式;32 位其它操作系统,2 核 2G 以上用 Server 模式,低于该配置还是 Client 模式。
☞ ParNew
使用多线程进行垃圾回收,在垃圾回收时,会暂停所有其他工作线程,直到 GC 结束。ParNew 是 Serial 收集器新生代的并行多线程版本,最常见的应用场景是配合老年代 CMS GC 工作,其余行为和 Seria 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样要暂停所有其他的工作线程。它是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器。
开启串行收集器的 JVM 参数是 -XX:+UseParNewGC
。启用 ParNew 收集器,只影响新生代的收集(新生代 GC 频繁),不影响老年代。开启参数后,会使用 ParNew(Young区) + Serial Old(Old区) 的收集器组合。新生代使用复制算法,老年代使用标记整理算法。Java 8 开始 ParNew + Tenured(Serial Old)不再推荐使用。
☞ Parallel Scavenge
Parallel Scavenge 收集器类似 ParNew,也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。相当于是串行收集器在新生代和老年代的并行化。它重点关注可控制吞吐量,高吞吐量意味着高效利用 CPU 时间,它多用于在后台运算而不需要太多交互的任务。【吞吐量 = 用户代码运行时间/(用户代码运行时间+垃圾回收时间)】
自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。自适应调节策略就是 JVM 会根据当前系统的运行情况看收集性能监控信息,动态调整这些参数以提供最合适的停顿时间 -XX: MaxGCPauseMills
或最大吞吐量。
常用的 JVM 参数:-XX:+UseParallelGC
或者 +UseParallelOldGC
二者可以互相激活,使用 Parallel Scavenge 收集器。开启参数后,新生代用复制算法,老年代用复制标记整理算法。参数 -XX:+ParallelGCThread = K
表示启动 K 个 GC 线程【CPU > 8 K = 5或8 CPU < 8 K = 实际个数】
☞ Serial Old
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程的标记整理算法,在 JDK 1.6 开始提供。JDK 1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配老年代 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在 JDK 1.6 之前,是 Parallel Scavenge + Serial Old。
Parallel Old 是为了老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求较高,JDK 1.8 后优先考虑新生代 Parallel Scavenge 和老年代 Parallel Old 的搭配策列。在 JDK 1.8 及之后,是 Parallel Scavenge + Parallel Old。
JVM 常用参数:-XX:+UseParallelOldGC
使用 Parallel Old 收集器。UseParallelGC 和 UseParallelOldGC 可以互相激活。
☞ Parllel Old Serial Old 收集器是 Serial 垃圾收集器老年代版本,同样是单线程的收集器,使用标记整理算法。主要运行在 Client 默认的 JVM 老年代垃圾回收器。在 Server 模式下,主要有两个用途: ♞ 在 JDK 1.5 之前与新生代 Parallel Scavenge 收集器搭配使用。Parallel Scavenge + Serial Old ♞ 作为老年代版中使用 CMS 收集器的后备垃圾回收方案。
☞ CMS
CMS 收集器是一个以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或 BS 系统的服务器上,因为这类场景重视服务器的响应速度,希望系统的停顿时间尽可能短。CMS 适合堆内存大、CPU 核数多的服务器端应用,也是 G1 出现之前大型应用的首选收集器。
CMS的优势是并发收集停顿少,并发是指与用户线程一起执行。开启收集器的 JVM 参数:-XX:+UseConcMarkSweepGC
开启后会自动开启 -XX:+UseParNewGC
并发标记清除收集器的组合:ParNew + CMS + Serial Old(作为 CMS 出错的后备收集器,增强健壮性)
CMS 内存回收一共有 4 个过程:
♞ 初始标记
:只有标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
♞ 并发标记
:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程,主要标记过程,标记全部对象。
♞ 重新标记
:修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前再做修正。
♞ 并发清除
:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作。所以总体上说 CMS 收集器的内存回收和用户线程是并发执行的(初始标记和重新标记虽然要暂停,但是用时很短)。
CMS 优点是并发收集,停顿次数少。缺点是对 CPU 的压力大,CMS 在收集和应用线程会同时增加对堆内存的占用,也就是说 CMS 必须在老年代堆内存用完之前完成 GC,否则 CMS 会回收失败,将触发担保机制,Serial Old 会以 STW(Stop The World,暂停所有工作线程)的方式进行依次 GC,从而造成较大的停顿时间。而且采用标记清除算法会产生内存碎片。
☞ G1
为解决 CMS 算法产生空间碎片和其它一系列的问题缺陷,HotSpot 提供了另外一种垃圾回收策略,G1(Garbage First)算法,通过参数 -XX:+UseG1GC
来启用,该算法在 JDK 7u4 版本被正式推出。G1 能充分利用多 CPU、多核环境硬件优势,尽量缩短 STW;整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片;宏观上看 G1 之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘;G1 收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分 Region 的集合且不需要 Region 是连续的,也就是说依然会采用不同的 GC 方式来处理不同的区域;G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的 survivor(to space) 堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换。
在 G1 中,还有一种特殊的区域,叫 Humongous(巨大的) 区域如果一个对象占用的空间超过了分区容量 50% 以上,G1 收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放巨型对象。如果一个 H 区装不下一个目型对象,那么 G1 会寻找连续的 H 分区来存储。为了能我到连续的 H 区有时候不得不启动 Full GC。
区域化内存划片 Region,整体编为了一些列不连续的内存区域,避免了全内存区的 GC 操作。核心思想是将整个堆内存区域分成大小相同的子区域(Region),在 JVM 启动时会自动设置这些子区域的大小,在堆的使用上,G1 并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为 2048 个分区,最多能设置 2048 个区域,也即能够支持的最大内存为:32MB * 2048 = 65536MB = 64G 内存
针对 Eden 区进行收集,Eden 区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内存碎片;Eden 区的数据移动到 Survivor 区,假如出现 Survivor 区空间不够,Eden 区数据会部会晋升到 Old 区;Survivor 区的数据移动到新的 Survivor 区,部会数据晋升到 Old 区;最后 Eden 区收拾干净了,GC 结束,用户的应用程序继续执行。
G1 垃圾收集过程
♞ 初始标记
:使用 stop-the-world 模式进行处理,它伴随着一次普通的 Young GC 发生,然后对 Survivor 区(root region)进行标记,因为该区可能存在对老年代的引用。
♞ 扫描根引用区
:因为先进行了一次 YGC,所以当前年轻代只有 Survivor 区有存活对象,它被称为根引用区。扫描 Survivor 到老年代的引用,该阶段必须在下一次 Young GC 发生前结束。因为 Young GC 的话,就会有一些存活的对象进入到 Survivor 区里面了,所以这个阶段不能发生年轻代收集,如果中途 Eden 区真的满了,也要等待这个阶段结束才能进行 Young GC。
♞ 并发标记
:寻找整个堆的存活对象,为了提高速度,这个阶段是并发处理,该阶段可以被 Young GC 中断。
♞ 重新标记
:stop-the-world,完成最后的存活对象标记。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法,这个阶段会回收完全空闲的区块
♞ 清理
:清理阶段真正回收的内存很少。