介绍下CMS的垃圾回收过程

CMS,Concurrent Mark Sweep,同样是老年代的收集器。他是一个并发执行的垃圾收集器,他更加关注垃圾回收的停顿时间,通过他的名字Concurrent Mark Sweep就可以知道,他采用的是耗时更短的**<font style="color:#5C8D07;">标记-清除算法</font>**。 ✅说一说JVM的并发回收和并行回收 CMS收集器的工作流程主要有下面4个步骤: 初始标记:标记所有从GCRoot直接可达的对象。这一步骤需要STW,即暂停所有应用线程,但由于只标记直接可达的对象,因此这个阶段通常很快。 并发标记:从初始标记阶段标记的对象出发,遍历整个对象图,标记所有可达的对象。在此阶段,GC线程与应用线程同时运行,不需要STW。(预清理:这一阶段也是并发执行的,目的是在实际清理前,处理并发标记阶段结束后和重新标记阶段开始前这段时间内发生的变化。目的是减少重新标记阶段的工作量。) 重新标记:这一阶段是为了修正并发标记期间因应用线程继续运行而产生的更改。这是另一个需要STW的阶段。 并发清理:在此阶段,GC线程清除不可达的对象,并回收它们占用的内存空间。这个阶段与应用线程并发执行,不需要STW。 以上这个过程其实就是三色标记法的过程:https://www.yuque.com/hollis666/ec96i7/lva8a9gfhagbrw2g 从上面的四个步骤中可以看出,CMS的过程中,只有初始标记和重新标记这两个步骤是STW的,所以,相比其他的收集器整个回收过程都STW来说,他导致的应用停顿时间更短。 优点: 并发 低停顿 缺点: 对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢 无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾。 CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了 扩展知识 ✅为什么初始标记和重新标记需要STW,而并发标记不需要? ✅什么是STW?有什么影响?

March 22, 2026 · 1 min · santu

新生代和老年代的垃圾回收器有何区别?

典型回答 常见的垃圾回收器如下: 串行垃圾回收器(Serial Garbage Collector) 如:Serial GC, Serial Old 并行垃圾回收器(Parallel Garbage Collector) 如:Parallel Scavenge,Parallel Old,ParNew 并发标记扫描垃圾回收器(CMS Garbage Collector) G1垃圾回收器(G1 Garbage Collector,JDK 7中推出,JDK 9中设置为默认) ZGC垃圾回收器(The Z Garbage Collector,JDK 11 推出) 新生代收集器有Serial、ParNew、Parallel Scavenge; 老年代收集器有Serial Old、Parallel Old、CMS。 整堆收集器有G1、ZGC 扩展知识 串行垃圾收集器 Serial GC Serial是单线程的串行垃圾回收器,主要采用**<font style="color:#213BC0;">标记-复制算法</font>**进行垃圾回收。 单线程地好处就是减少上下文切换,减少系统资源的开销。但这种方式的缺点也很明显,在GC的过程中,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(Stop The World)。若GC不是频繁发生,这或许是一个不错的选择,否则将会影响程序的执行性能。 Serial Old Serial Old是Serial的老年代版本,也是个单线程收集器,适用于老年代,使用的是**<font style="color:#AE146E;">标记-整理算法</font>**。 优缺点基本和Serial差不多,二者主要是回收算法不一样。 并行垃圾收集器 ParNew ParNew其实就是Serial的多线程版本,在参数、回收算法上,和**Serial是完全一样的,所以他也是采用**<font style="color:#213BC0;">标记-复制算法</font>**进行垃圾回收的。** ...

March 22, 2026 · 1 min · santu

新生代如果只有一个Eden+一个Survivor可以吗?

答案是不行,如果只有两个区域,也能实现复制算法,但是会大大浪费空间。 我们知道,新生代进一步区分了一个Eden区和2个Survivor区,一共有Eden、Survivor From、Survivor To这三个区域,那么,为什么需要三个区域呢?2个行不行呢? 这其实涉及到新生代的垃圾回收算法了: 新生代和老年代的GC算法 根据默认配置,新生代有一个 Eden区,两个survivor区,eden区占80%内存空间,每一块survivor区占 10% 因为新生代主要使用的是标记-复制算法进行垃圾回收的。 刚开始对象都分配在Eden区,如果Eden区快满了就触发垃圾回收,把Eden区中的存活对象转移到一块空着的survivor区,eden区清空,然后再次分配新对象到eden区,再触发垃圾回收,就把eden区存活的和survivor区存活的转移到另一块空着的survivor。 那么也就是说,在平常的时候,新生代的区域中是只有一块eden和一块survivor区在被使用的,而另一块Survivor区是空着的,所以内存使用率大约 90%。 如果没有三个区域,只有两个,比如只有一个Eden和一个Survivor: 如果此时Eden区进行YoungGC之后,会如下图所示: 那么,接下来继续创建对象的时候,如果继续向Eden分配: 如果之后进行第二次YoungGC的时候,就不能只扫描Eden区,还要扫描Survivor区。那么,就不能使用标记复制算法了,因为标记复制算法的要求是必须有一块区域是空着的。 而如果使用标记-清除算法或者标记-整理算法的话,就会存在碎片和效率等问题。 那么,如果改一下,从Eden复制到Survivor之后,再次分配新对象的时候分配到Survivor呢?然后Survivor满了再把对象复制到Eden,这样循环往复? 这样做,或许可以实现复制算法了,但是带来的问题就是两个区域都会承担新对象的分配工作,那么他的内存就都得足够大,那么就要分配成1:1,这样的话,整个新生代的同一时刻只能有1/2的空间被使用,利用率很低。 扩展知识 Survivor不够怎么办? 在YoungGC之后,如果存活的对象所需要的空间比Survivor区域的空间大怎么办呢?毕竟一块Survivor区域的比例只是年轻代的10%而已。 这时候就需要把对象移动到老年代。 空间分配担保机制 如果Survivor区域的空间不够,就要分配给老年代,也就是说,老年代起到了一个兜底的作用。但是,老年代也是可能空间不足的。所以,在这个过程中就需要做一次空间分配担保(CMS): 在每一次执行YoungGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 如果大于,那么说明本次Young GC是安全的。 如果小于,那么虚拟机会查看HandlePromotionFailure 参数设置的值判断是否允许担保失败。如果值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小(一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考)。如果大于,则尝试进行一次YoungGC,但这次YoungGC依然是有风险的;如果小于,或者HandlePromotionFailure=false,则会直接触发一次Full GC。 但是,需要注意的是**HandlePromotionFailure**这个参数,在JDK 7中就不再支持了: 在JDK代码中,移除了这个参数的判断(https://github.com/openjdk/jdk/commit/cbc7f8756a7e9569bbe1a38ce7cab0c0c6002bf7 ),也就是说,在后续的版本中, 只要检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则认为担保成功。 但是需要注意的是,担保的结果可能成功,也可能失败。所以,在YoungGC的复制阶段执行之后,会发生以下三种情况: 剩余的存活对象大小,小于Survivor区,那就直接进入Survivor区。 剩余的存活对象大小,大于Survivor区,小于老年代可用内存,那就直接去老年代。 剩余的存活对象大小,大于Survivor并且大于老年代,触发"FullGC"。

March 22, 2026 · 1 min · santu

有哪些常用的JVM启动参数?

典型回答 JVM的启动参数有很多,但是我们平常能用上的并不是特别多,这里介绍几个我们常用的: 堆设置: -Xms:设置堆的初始大小。 -Xmx:设置堆的最大大小。 栈设置: -Xss:设置每个线程的栈大小。 垃圾回收器设置: -XX:+UseG1GC:使用 G1 垃圾回收器。 -XX:+UseParallelGC:使用并行垃圾回收器。 性能调优: -XX:PermSize 和 -XX:MaxPermSize:在 Java 8 之前设置永久代的初始大小和最大大小。 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:在 Java 8 及以上版本设置 Metaspace 的初始大小和最大大小。 -XX:+PrintGCDetails:打印垃圾回收的详细信息。 调试和分析: -verbose:gc:输出垃圾回收的详细信息。 -XX:+HeapDumpOnOutOfMemoryError:在内存溢出时生成堆转储。 扩展知识 如何使用 要使用这些 JVM 启动参数,你需要在启动 Java 应用程序时在命令行中指定它们。 例如,如果你想设置最大堆大小为 512MB 并启用 G1 垃圾回收器,你可以这样启动你的 Java 应用程序: 1 java -Xmx512m -XX:+UseG1GC -jar your-application.jar 另外,如果是在IDEA中,需要再run配置中增加上对应的参数,如: (图片来源于网络)

March 22, 2026 · 1 min · santu

简单介绍一下JIT优化技术?

典型回答 我们知道,想要把高级语言转变成计算机认识的机器语言有两种方式,分别是编译和解释,虽然Java转成机器语言的过程中有一个步骤是要编译成字节码,但是,这里的字节码并不能在机器上直接执行。 所以,JVM中内置了解释器(interpreter),在运行时对字节码进行解释翻译成机器码,然后再执行。 解释器的执行方式是一边翻译,一边执行,因此执行效率很低。为了解决这样的低效问题,HotSpot引入了JIT技术(Just-In-Time)。 有了JIT技术之后,JVM还是通过解释器进行解释执行。但是,当JVM发现某个方法或代码块运行时执行的特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。 扩展知识 HotSpot虚拟机中内置了两个JIT编译器:Client Complier和Server Complier,分别用在客户端和服务端,目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。 当 JVM 执行代码时,它并不立即开始编译代码(因为Java默认是解释执行的)。首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力。因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多。第二个原因是最优化,当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。 ✅Java是编译型还是解释型? 在机器上,执行java -version命令就可以看到自己安装的JDK中JIT是哪种模式: 上图是我的机器上安装的jdk1.8,可以看到,他是Server Compile,但是,需要说明的是,无论是Client Complier还是Server Complier,解释器与编译器的搭配使用方式都是混合模式,即上图中的mixed mode。 热点检测 上面我们说过,要想触发JIT,首先需要识别出热点代码。目前主要的热点代码识别方式是热点探测(Hot Spot Detection),有以下两种: **1、基于采样的方式探测(Sample Based Hot Spot Detection) **:周期性检测各个线程的栈顶,发现某个方法经常出现在栈顶,就认为是热点方法。好处就是简单,缺点就是无法精确确认一个方法的热度。容易受线程阻塞或别的原因干扰热点探测。 2、基于计数器的热点探测(Counter Based Hot Spot Detection)。采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,某个方法超过阀值就认为是热点方法,触发JIT编译。 在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。 方法计数器。顾名思义,就是记录一个方法被调用次数的计数器。 回边计数器。是记录方法中的for或者while的运行次数的计数器。 编译优化 前面提到过,JIT除了具有缓存的功能外,还会对代码做各种优化。说到这里,不得不佩服HotSpot的开发者,他们在JIT中对于代码优化真的算是面面俱到了。 主要的优化有:逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除。接下来挑几个重点的介绍一下。 逃逸分析 ✅什么是逃逸分析? 锁消除 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。 如以下代码: 1 2 3 4 5 6 public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } 代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成: ...

March 22, 2026 · 1 min · santu

类的生命周期是怎么样的?

典型回答 一个类从诞生到卸载,大体分为如下几步: 大的阶段可以分为类的加载、类的使用、以及类的卸载。 其中类的加载阶段又分为加载、链接、初始化。其中连接过程又包含了验证、准备和解析。 加载阶段 Java中类加载的过程是怎么样的? 类使用过程 类的使用,即是类在加载完毕后,会有代码段来引用该类,如初始化该类的对象,或者通过反射获取该类的元数据。 类卸载过程 假如说该类满足下面2个条件: 该类所有的实例都已被GC回收。 该类的ClassLoader已经被GC回收。 那么该类会在FULLGC期间从方法区被回收掉。 这个时候,我们需要明白一个问题,我们知道,JVM自带的类加载器因为需要一直加载基础对象,所以JDK自带的基础类是一定不会被回收掉的,那么会有哪些类会被回收掉呢? 答案就是那些自定义类加载器一些场景的类会被回收掉,如tomcat,SPI,JSP等临时类,是存活不久的,所以需要来不断回收

March 22, 2026 · 1 min · santu

项目中如何选择垃圾回收器?为啥选择这个?

典型回答 ✅新生代和老年代的垃圾回收器有何区别? 上文中介绍常见的垃圾回收器,其中包括: 串行垃圾回收器(Serial Garbage Collector) 如:Serial GC, Serial Old 单线程垃圾收集器,适用于单核机器。 暂停时间较长,不适用于多核环境和大内存应用。 并行垃圾回收器(Parallel Garbage Collector) 如:Parallel Scavenge,Parallel Old,ParNew 多线程垃圾收集器,适用于多核机器。适合批处理、后台作业等暂停时间不敏感的应用。 暂停时间较长,不适合需要低延迟的应用。 并发标记扫描垃圾回收器(CMS Garbage Collector) 低延迟垃圾收集器,适合需要响应时间较短、高响应性要求的应用。 容易产生碎片,可能需要Full GC进行碎片整理,Full GC时会有较长暂停。 G1垃圾回收器(G1 Garbage Collector,JDK 7中推出,JDK 9中设置为默认) 设计用于替代CMS,适合大内存、多核环境。较低的暂停时间,能够预测性地控制暂停时间,适合大数据量应用。 ZGC垃圾回收器(The Z Garbage Collector,JDK 11 推出)、Shenandoah GC(JDK 12 推出) 超低延迟垃圾收集器,适用于超大堆内存。暂停时间通常在10ms以下,适合对响应时间有极高要求的应用。 目前只支持较新的JDK版本,可能存在一些不成熟的特性。 综上,在进行选择的时候,按照以下步骤: 1、根据机器情况判断,如果是单核机器,或者内存较小的机器,则选择Serial GC。 2、根据业务类型判断,看你的应用更在意的是吞吐量还是 STW 的时长。比如批处理任务的应用,更在意的就是吞吐量,而实时交易系统,更在意的就是 STW 的时长。 3、根据机器分配的堆内存大小进行判断,一把来说,我们认为至少达到4G 以上才可以用 G1、ZGC 等,通常要比如超过8G、16G 这样效果才更好。 4、根据 JDK 版本进行判断,不同的版本支持的垃圾收集器不一样。 可以参考以下的选择方式(但是,并不绝对,尤其是 ZGC 和Shenandoah GC 的选择,其实还是要慎重,毕竟他们的稳定性各方面还有待验证):

March 22, 2026 · 1 min · santu

对JDK进程执行kill -9有什么影响?

典型回答 kill -9 命令会立刻关闭Jvm进程。但是kill -9的语意是强制关闭,会导致在Jvm中执行的服务立刻关闭,来不及收尾。如导致RPC服务没有从注册中心取消注册导致服务不可用,如导致事务执行一半直接终止等等 kill 命令 我们都知道,想要在Linux中终止一个进程有两种方式,如果是前台进程可以使用Ctrl+C键进行终止;如果是后台进程,那么需要使用kill命令来终止。(其实Ctrl+C也是kill命令) kill命令的格式是: kill[参数] [进程号] ,如: kill 21121 kill -9 21121 其中[参数]是可选的,进程号可以通过jps/ps/pidof/pstree/top等工具获取。 kill的命令参数有以下几种: -l 信号,如果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称 -a 当处理当前进程时,不限制命令名和进程号的对应关系 -p 指定kill 命令只打印相关进程的进程号,而不发送任何信号 -s 指定发送信号 -u 指定用户 通常情况下,我们使用的-l(信号)的时候比较多,如我们前文提到的kill -9中的9就是信号。 信号如果没有指定的话,默认会发出终止信号(15)。常用的信号如下: HUP 1 终端断线 INT 2 中断(同 Ctrl + C) QUIT 3 退出(同 Ctrl + \) TERM 15 终止 KILL 9 强制终止 CONT 18 继续(与STOP相反, fg/bg命令) STOP 19 暂停(同 Ctrl + Z) 比较常用的就是强制终止信号:9和终止信号:15。 另外,中断信号:2其实就是我们前文提到的Ctrl + C结束前台进程。 ...

March 22, 2026 · 2 min · santu

Java中的对象一定在堆上分配内存吗?

典型回答 不一定,在HotSpot虚拟机中,存在JIT优化的机制,JIT优化中可能会进行逃逸分析,当经过逃逸分析发现某一个局部对象没有逃逸到线程和方法外的话,那么这个对象就可能不会在堆上分配内存,而是进行栈上分配。 扩展知识 逃逸分析 ✅简单介绍一下JIT优化技术? 标量替换 标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。 1 2 3 4 5 6 7 8 9 10 11 12 public static void main(String[] args) { alloc(); } private static void alloc() { Point point = new Point(1,2); System.out.println("point.x="+point.x+"; point.y="+point.y); } class Point{ private int x; private int y; } 以上代码中,point对象并没有逃逸出alloc方法,并且point对象是可以拆解成标量的。那么,JIT就不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。 以上代码,经过标量替换后,就会变成: ...

March 22, 2026 · 2 min · santu

YoungGC和FullGC的触发条件是什么?

YoungGC的触发条件比较简单,那就是**当年轻代中的eden区分配满的时候就会触发。** FullGC的触发条件比较复杂也比较多,主要以下几种: 老年代空间不足 创建一个大对象,超过指定阈值会直接保存在老年代当中,如果老年代空间也不足,会触发Full GC。 YoungGC之后,发现要移到老年代的对象,老年代存不下的时候,会触发一次FullGC 空间分配担保失败(空间分配担保详见:https://www.yuque.com/hollis666/ec96i7/eigm8iqgpwmd2eg8#l3Gjz) 当准备要触发一次YoungGC时,会进行空间分配担保,在担保过程中,发现虚拟机会检查老年代最大可用的连续空间小于新生代所有对象的总空间,但是HandlePromotionFailure=false,那么就会触发一次FullGC(HandlePromotionFailure 这个配置,在JDK 7中并不在支持了,这一步骤在该版本已取消) 当准备要触发一次YoungGC时,会进行空间分配担保,在担保过程中,发现虚拟机会检查老年代最大可用的连续空间小于新生代所有对象的总空间,但是HandlePromotionFailure=true,继续检查发现老年代最大可用连续空间小于历次晋升到老年代的对象的平均大小时,会触发一次FullGC 永久代空间不足 如果有永久代的话,当在永久代分配空间时没有足够空间的时候,会触发FullGC 代码中执行System.gc() 代码中执行System.gc()的时候,会触发FullGC,但是并不保证一定会立即触发。

March 22, 2026 · 1 min · santu

留言给博主