什么是Stop The World?

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。这是Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。 不管选择哪种GC算法,stop-the-world都是不能彻底避免的,只能尽量降低STW的时长。 为什么需要STW呢? 首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。 其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和多标。 多标:其实就是这个对象原本应该被回收掉的垃圾对象,但是被错误的标记成了存活对象。从而导致这个对象没有被GC回收掉。 这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。 垃圾收集在标记的过程中,其实标记的并不是垃圾对象,而是通过可达性分析标记可达对象。所以当一个对象本来是垃圾对象(不可达对象),但是错误的标记成非垃圾对象(可达对象)时,就是多标了。就会导致浮动垃圾。 漏标:一个对象本来应该是存活对象,但是没有被正确的标记上,导致被错误的垃圾回收掉了。

March 22, 2026 · 1 min · santu

JVM如何判断对象是否存活?

典型回答 当JVM判断对象不再存活的时候,便会在下一次GC时候将该对象回收掉,为堆腾出空间,那么JVM如何判断对象是否存活呢? JVM有两种算法来判断对象是否存活,分别是引用计数法和可达性分析算法 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 循环引用会导致对象无法被回收,最终会导致内存泄漏及内存溢出 可达性分析算法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 但是,并不是说当进行完可达性分析算法后,即可证明某对象可以被GC。对象是否存活,需要两次标记: 第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记 如果对象的finalize()方法被覆盖并且没有执行过,则放在F-Queue队列中等待执行_(不一定会执行)_,如果一段时间后该对象的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收 不过现在都不提倡覆盖finalize方法,它的本意是像Cpp一样在对象销毁前执行,但是它影响了JAVA的安全和GC的性能,所以第二种判断会越来越少 知识扩展 哪些内容可以作为GC roots? GC roots是作为可达性分析算法的起点的。要实现语义正确的可达性分析,就必须要能完整枚举出所有的GC Roots,否则就可能会漏扫描应该存活的对象,导致GC错误回收了这些被漏扫的活对象。那么,所谓“GC Roots”,就是一组必须活跃的引用。 那么,有哪些引用是一定活跃的呢?看下下面这些是不是都符合这个条件: Class - 由系统类加载器(system class loader)加载的类,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。 Thread - 活着的线程 Stack Local - Java方法的local变量或参数 JNI Local - JNI方法的local变量或参数 JNI Global - 全局JNI引用 Monitor Used - 被同步锁(synchronized)持有的对象 Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。 以上,比如系统类加载器加载的类、活着的线程、方法中的本地变量、被synchronized锁定的对象这些,都是符合活跃的引用这个条件的! ...

March 22, 2026 · 1 min · santu

什么是强引用、软引用、弱引用和虚引用?

在Java中,强引用、软引用、弱引用和虚引用是用于管理对象生命周期的不同类型的引用。它们的主要作用是帮助垃圾回收器(GC)决定何时回收对象,从而更高效地管理内存。 强软弱虚,按照这个顺序,对象的引用的强度越来越弱。 强引用 强引用是Java的默认引用形式,使用时不需要显示定义。如果一个对象具有强引用,那垃圾回收器绝不会回收它(可达时)。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 1 String[] arr = new String[]{"a", "b", "c"}; 案例 强引用是默认的引用方式,我们创建的new 一个对象,就是用的强引用,即如果引用一直在,就不会被 GC 回收掉。 在集合中, 如我们常见的HashMap使用的引用就是强引用,也就是说垃圾收集的时候,Map中引用的对象不会被GC掉。 软引用 软引用是一种相对较弱的引用类型,用于表示对对象的“非强”引用。 软引用是使用SoftReference来创建对象的方式,如SoftReference<Object> softRef = new SoftReference<>(new Object());就是软引用。 当内存不足时,垃圾回收器会优先回收软引用指向的对象,以释放内存。软引用可以在系统内存充足时保留对象,以提高性能(缓存机制)。 1 SoftReference<String[]> softBean = new SoftReference<String[]>(new String[]{"a", "b", "c"}); 案例 在 Guava Cache 中,有关于软引用的使用: 1 2 3 4 5 6 7 8 9 10 class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { static class SoftValueReference<K, V> extends SoftReference<V> implements ValueReference<K, V> { final ReferenceEntry<K, V> entry; SoftValueReference(ReferenceQueue<V> queue, V referent, ReferenceEntry<K, V> entry) { super(referent, queue); this.entry = entry; } } } LocalCache 类用到了 SoftReference 来实现软引用缓存。通过软引用,来方便在内存不足时能自动回收缓存对象,来避免 OOM 的发生。 ...

March 22, 2026 · 2 min · santu

G1和CMS有什么区别?

典型回答 G1 是 JDK 1.9中默认的垃圾收集器,他代替了Java 8 中的默认的Parallel Scavenge GC+Parallel Old GC,并且也代替了CMS。 G1 和 CMS相比,他们都是基于三色标记法实现的,替代了原有的传统的可达性分析(三色标记也是可达性分析的一种,只不过特殊一点),可以大大的降低STW的时长。但是,他们之间还是有很大的不同的: 特性 CMS G1 回收位置 老年代 整堆 GC算法 标记-清除算法 标记-复制算法回收年轻代标记-整理算法回收老年代 垃圾识别算法 三色标记法——增量更新解决漏标 三色标记法——原始快照解决漏标 碎片产生 存在内存碎片 可防止内存碎片产生 可预测性 无法预测 G1的STW时长可预测 堆内存基本要求 一般要求不高 4G以上 自适应调优 不支持 支持 JDK版本 1.8及以前,(JDK14中被移除) 1.7+ (1.9+的默认GC) 总结一下就是,G1会把Java的堆分为多个大小相等的Region(每个Region的大小为1M-32M),他在年轻代回收的时候采用标记-复制算法,而在老年代回收的时候,采用的是标记-整理算法,这两种算法都可以避免内存碎片的产生。 G1在回收的过程中,标记和清理的过程是并行的,可以充分利用多个CPU来缩短STW的时长,在复制的过程中是并发的,可以让复制线程和用户线程并发执行,不需要STW。并且G1还可以在运行时动态的做区域内存大小的调整。

March 22, 2026 · 1 min · santu

新生代和老年代的GC算法

三种垃圾回收算法,复制、标记清除、标记整理中,比较适合新生代的算法是标记复制算法。 ✅JVM有哪些垃圾回收算法? 因为对于新生代来说,一般来说GC的次数是要比老年代高很多的,所以需要一个效率更高的算法,而且最好不要有碎片,因为很多对象都是需要先在新生代分配空间的,如果碎片太多的话,那么就会导致很多对象无法正常分配了。 所以,新生代选择了复制算法进行垃圾回收,但是标记复制算法存在一个缺点就是会浪费空间,新生代为了解决这个问题,把区域进一步细分成一个Eden区和两个Survivor区,同时工作的只有一个Eden区+一个Survivor区,这样,另外一个Survivor主要用来复制就可以了。只需要动态的调整Eden区和Survivor区的比例就可以降低空间浪费的问题。 对于老年代来说,通常会采用标记整理算法,虽然效率低了一点,但是可以减少空间的浪费并且不会有空间碎片等问题。在有些回收器上面,如CMS,为了降低STW的时长,也会采用标记清除算法。 ✅Java的堆是如何分代的?为什么分代? ✅新生代如果只有一个Eden+一个Survivor可以吗?

March 22, 2026 · 1 min · santu

Java 8 和 Java 11 的GC有什么区别?

典型回答 Java 8 和 Java 11都是LTS版本的JDK,所以会有人经常问他们之间的区别。特别是在GC上面的差别。 首先,在垃圾收集器上面,Java 8 中默认的Parallel Scavenge GC+Parallel Old GC的,分别用来做新生代和老年代的垃圾回收。而在Java 11中默认采用的是G1进行整堆回收的(Java 9中就是默认的了)。 另外,Java 11中还新增了一种垃圾收集器,那就是ZGC,他可以在保证高吞吐量的同时保证最短的暂停时间。 ✅JDK 11中新出的ZGC有什么特点? 在知道了垃圾收集器上面的区别之后,就可以基于Parallel Scavenge GC+Parallel Old GC 和 G1的区别进一步说一下GC上面的区别了。 在垃圾识别及回收上面,Java 8基于的是单纯地可达性分析,而Java 11中的G1采用的是三色标记法,可以大大降低STW的时长。 ✅什么是三色标记算法? 另外,G1的内存划分是自适应的,它会根据堆的大小和使用情况来动态调整各个区域的大小和比例。而Parallel Scavenge GC+Parallel Old GC都是固定分配的策略。

March 22, 2026 · 1 min · santu

JDK 11中新出的ZGC有什么特点?

典型回答 ZGC(Z Garbage Collector)是Java 11中引入的一种新的垃圾回收器,他是一个为了实现低延迟而设计的垃圾收集器,具有以下几个特点: 低停顿:ZGC的目标是保证暂停时间非常短,ZGC 的目标是保持最大暂停时间在亚毫秒级,且这个暂停时间不会随着堆、live-set 或 root-set 的大小而增加。 高吞吐量:ZGC 是一个并发垃圾收集器,意味着大部分垃圾收集工作都是在 Java 线程继续执行的同时完成的。这极大地减少了垃圾收集对应用程序响应时间的影响。 兼容性:ZGC与现有的Java应用程序完全兼容,并且无需更改代码即可使用。但是也有一定的限制,仅支持 Linux 64位系统,不支持 32位平台。不支持使用压缩指针,采用内存分区管理。 简单性:ZGC设计简单,代码库较小,因此它更容易维护和扩展。 支持大堆:ZGC 能处理从 8MB 到 16TB 大小的堆,适用于大规模内存需求的应用程序 不分代回收:ZGC在垃圾回收时对全量内存进行标记,但是回收时仅针对分内存回收,优先回收垃圾比较多的页面。(JDK 21中已经支持分代ZGC了,JDK 24中删除了非分代ZGC) 因此,ZGC是一种新的、高效的、低停顿的垃圾回收器,适用于内存大小从几GB到数TB的应用程序。它的设计目标是在保证高吞吐量的同时保证最短的暂停时间,并且易于使用和维护。 ✅ZGC和CMS和G1的区别对比?

March 22, 2026 · 1 min · santu

为什么G1从JDK 9之后成为默认的垃圾回收器?

典型回答 G1,Garbage First,是CMS的改进版,解决了CMS内存碎片、更多的内存空间等问题。总之,G1是一个先进的垃圾收集器,它可以提高系统的吞吐量,降低停顿的频率,并且可以有效管理大型堆。在JDK 9之后,G1成为了默认的垃圾回收器,主要是因为他有以下优势: 并发回收:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The World的停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。 ✅什么是三色标记算法? 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于标记-整理算法实现收集,从局部(两个Region)上来看是基于标记-复制算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 ✅G1如何精确控制 STW的时间的? 支持热插拔:G1可以在运行时动态调整堆的大小,以适应不同的内存需求。 与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。 同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。 Garbage First (G1) 是一种面向大型内存环境的垃圾回收算法。因此,G1适合在以下场景中使用: 大型内存环境:G1针对大型内存环境进行了优化,因此对于使用了大量内存的应用程序来说(超过4G),G1是一个更好的选择。 对应用程序响应时间敏感的场景:G1通过分配多线程来进行垃圾回收,以最大限度地减少回收时应用程序的暂停时间。 对内存使用效率敏感的场景:G1可以更好地评估哪些内存空间可以释放,以此来提高内存的利用率。 动态内存需求的场景:G1支持热插拔,可以在运行时动态调整堆的大小,以适应不同的内存需求。 要求回收时间具有可预测性的场景:G1使用固定的内存分配块来管理堆内存,这使得其在回收时间上具有更高的可预测性。 所以,如果应用程序需要在大型内存环境下运行,同时对内存使用效率和应用程序的响应时间敏感,那么G1是一个更好的选择。

March 22, 2026 · 1 min · santu

Java中的类什么时候会被加载?

典型回答 Java中的类在以下几种情况中会被加载: **当创建类的实例时,如果该类还没有被加载,则会触发类的加载。**例如,通过关键字new创建一个类的对象时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。 **当使用类的静态变量或静态方法时,如果该类还没有被加载,则会触发类的加载。**例如,当调用某个类的静态方法时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。 **当使用反射机制访问类时,如果该类还没有被加载,则会触发类的加载。**例如,当使用Class.forName()方法加载某个类时,JVM会检查该类是否已经加载,如果没有加载,则会调用类加载器进行加载。 当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。 总之,**Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加载。**同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为Java程序带来了更大的灵活性。 扩展知识 Java中的类什么时候会被卸载 Java中类的卸载是由Java虚拟机(JVM)自动进行的,JVM会在满足以下3个条件时对一个类进行卸载: 该类所有的实例都已被GC回收。 该类的ClassLoader已经被GC回收。 该类对应的Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 在Java中,每一个类都会由ClassLoader加载到内存中,并在运行期间一直存在。当一个类不再被使用时,它的实例对象被GC回收后,如果ClassLoader也被GC回收,那么这个类就可以被卸载了。 需要注意的是,Java虚拟机并不会在程序运行过程中频繁地卸载类,因为类卸载是一个比较耗时的操作,会影响程序的性能。通常情况下,Java虚拟机会在需要释放内存空间时才会对不再使用的类进行卸载。 另外,Java SE 9引入了一个新的特性,即“模块化”,通过模块化可以对Java类进行更加精细的控制,包括对类的卸载。在模块化环境下,如果一个模块中的类不再被引用,那么这个模块就可以被卸载。模块化可以使Java应用程序更加安全、可靠和可维护。 Java在类的加载步骤中做了哪些操作 对于Java自带的类加载器来说,当一个类被加载的时候,需要用到类加载器将类从外部加载到Jvm的内存当中,如下代码所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 查询该类是否被加载过 Class<?> c = findLoadedClass(name); // 如果没有 if (c == null) { long t0 = System.nanoTime(); try { // 委派父类加载器加载 if (parent != null) { c = parent.loadClass(name, false); // 对于bootstap类加载器来说,他是没有父加载器的,所以用bootstrap加载该类 } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // 如果还没加载到,说明此类的二进制文件还没有定位到,需要使用自己的类加载器 if (c == null) { // 使用自定义的加载方式 c = findClass(name); // 省略... } } // 省略... return c; } } 那么最后一步的findClass是什么意思呢?我们可以通过JDK8的classLoader源码的注释中发现这么一个例子: ...

March 22, 2026 · 2 min · santu

如何判断JVM中类和其他类是不是同一个类?

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。 简单点说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

March 22, 2026 · 1 min · santu

留言给博主