面试官最爱问的 JVM 核心知识点,全在这了
目录
String s = new String(“abc”)执行过程中分别对应哪些内存区域?
minorGC、majorGC、fullGC的区别,什么场景触发full GC
内存模型
JVM的内存模型介绍一下
JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分

- 程序计数器:用于记录当前线程执行到哪一行字节码。
-
- 线程私有
- 唯一 不会 OOM 的区域
- 线程切换时恢复执行位置
- 执行 native 方法时,计数器值为 undefined
- Java 虚拟机栈:执行 Java 方法,存储局部变量、方法出口、操作数栈、动态链接。
-
- 每个线程都有自己独立的 Java 虚拟机栈,生命周期与线程相同。
- 每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
-
-
- 可能会抛出 StackOverflowError(栈帧太多) 和 OutOfMemoryError(栈帧太大) 异常。
-
- 本地方法栈:执行 native 方法。
-
- 虚拟机栈 → Java 方法
- 本地方法栈 → native 方法
- 本地方法执行时也会创建栈帧,同样可能出现 StackOverflowError 和 OutOfMemoryError 两种错误。
- Java 堆:存放所有对象实例、数组。
-
- 是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建
- GC 唯一管理的区域
- 最容易 OOM
- 从内存回收角度,堆被划分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出 OutOfMemoryError 异常。
- 方法区(元空间):存储类信息、常量池、静态变量、即时编译器编译后的代码。
-
- 方法区可以选择不实现垃圾收集,内存不足时会抛出 OutOfMemoryError 异常。
- 在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存。(为了解决OOM)
- 运行时常量池:类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
-
- 是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。当无法申请到足够内存时,会抛出 OutOfMemoryError 异常。
- 直接内存:不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致 OutOfMemoryError 异常。
JVM内存模型里的堆和栈有什么区别?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。
- 生命周期:
-
- 栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁;
- 堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用时才被回收。
- 存取速度:
-
- 栈的存取速度通常比堆快,因为栈遵循先进后出(LIFO, Last In First Out)的原则,操作简单快速;
- 堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间,
- 存储空间:
-
- 栈的空间相对较小,栈溢出时,通常是因为递归过深或局部变量过大
- 堆的空间较大,堆溢出是创建大对象过多和没有及时回收对象
- 栈内存是线程私有的,而堆内存是线程共有的。
- 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
-
- 栈空间不足:java.lang.StackOverFlowError。
- 堆空间不足:java.lang.OutOfMemoryError。
栈中存的到底是指针还是对象?
栈中存储的不是对象,而是对象的引用
- 比如MyObject obj = new MyObject();这里的obj实际上是一个存储在栈上的引用,指向堆中实际的对象实例。
堆分为哪几部分呢?

- 新生代(Young Generation):新生代分为Eden Space和Survivor Space。当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
- 老年代(Old Generation/Tenured Generation):存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
- 元空间(Metaspace):从Java 8开始,永久代(Permanent Generation)被元空间取代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。
-
- 元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的OOM问题。
- 大对象区(Large Object Space / Humongous Objects):在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。
如果有个大对象一般是在哪个区域?
直接分配到老年代。
- 避免频繁 GC:新生代空间小,大对象易占满空间,导致频繁 Minor GC,复制开销大。
- 减少内存碎片:大对象需要连续内存,新生代复制回收易产生碎片;老年代空间大、更适合存放。
程序计数器的作用,为什么是私有的?
如果在线程切换的过程中,下一条指令执行到哪里了,还是会用到我们的程序计数器啊。每个线程都有自己的程序计数器,因为它们各自执行的代码的指令地址是不一样的呀,所以每个线程都应该有自己的程序计数器。
方法区中的方法的执行过程?
- 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)。
- 栈帧创建:在调用一个方法前,当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
- 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境
方法区中还有哪些东西?
类型信息、常量、静态变量、即时编译器编译后的代码缓存
String保存在哪里呢?
String 保存在字符串常量池中,不同于其他对象,它的值是不可变的,且可以被多个引用共享。
String s = new String(“abc”)执行过程中分别对应哪些内存区域?
如果abc这个字符串常量不存在,则创建两个对象,分别是abc这个字符串常量,以及new String这个实例对象。如果abc这字符串常量存在,则只会创建一个对象。
引用类型有哪些?有什么区别?
- 强引用:指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
- 软引用:可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
- 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
- •虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。
弱引用了解吗?举例说明在哪里可以用?
弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
- 缓存系统:弱引用常用于实现缓存,可以让JVM在需要更多内存时自动清理这些缓存对象。
- 对象池:在对象池中,弱引用可以用来管理那些暂时不使用的对象。当对象不再被强引用时,它们可以被垃圾回收,释放内存。
- 避免内存泄露:当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外地保留,从而避免潜在的内存泄露。
内存泄漏和内存溢出的理解?
内存泄露:内存泄漏是指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。
内存泄露常见原因:
- 静态集合:使用静态数据结构(如HashMap或ArrayList)存储对象,且未清理。
- 事件监听:未取消对事件源的监听,导致对象持续被引用。
- 线程:未停止的线程可能持有对象引用,无法被回收。
内存溢出:内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。
内存溢出常见原因:
- 大量对象创建:程序中不断创建大量对象,超出JVM堆的限制。
- 持久引用:大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存累积。
- 递归调用:深度递归导致栈溢出。
jvm 内存结构有哪几种内存溢出的情况?
- 堆内存溢出:原因是代码中可能存在大对象分配,或者发生了内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
- 栈溢出:如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
- 元空间溢出:出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。
- 直接内存内存溢出:在使用ByteBuffer中的allocateDirect()的时候会用到,很多JavaNIO(像netty)的框架中被封装为其他的方法,出现该问题时会抛出Java.lang.OutOfMemoryError: Direct buffer memory异常。
有具体的内存泄漏和内存溢出的例子么请举例及解决方案?
- 静态属性导致内存泄露
大量使用static静态变量
public class StaticTest {
public static List<Double> list = new ArrayList<>();
public void populateList() {
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
Log.info("Debug Point 2");
}
public static void main(String[] args) {
Log.info("Debug Point 1");
new StaticTest().populateList();
Log.info("Debug Point 3");
}
}
解决方法:第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。
- 未关闭的资源
JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。
解决方法:
- 始终记得在finally中进行资源的关闭;
- 关闭连接的自身代码不能发生异常;
- Java7以上版本可使用try-with-resources代码方式进行资源关闭
- 使用ThreadLocal
ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。
ThreadLocalMap使用ThreadLocal的弱引用作为key,value 为强引用,value永远无法回收,造成内存泄漏。
解决:
- 使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;
- 不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。
- 最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
类初始化和类加载
创建对象的过程?

- 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 进行必要设置,比如对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始——构造函数,即class文件中的方法还没有执行,所有的字段都还为零,对象需要的其他资源和状态信息还没有按照预定的意图构造好。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来
对象的生命周期
- 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
- 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
类加载器有哪些?

- 启动类加载器(Bootstrap Class Loader):这是最顶层的类加载器,负责加载Java的核心库(如位于jre/lib/rt.jar中的类),它是用C++编写的,是JVM的一部分。启动类加载器无法被Java程序直接引用。
- 扩展类加载器(Extension Class Loader):它是Java语言实现的,继承自ClassLoader类,负责加载Java扩展目录(jre/lib/ext或由系统变量Java.ext.dirs指定的目录)下的jar包和类库。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器.
- 系统类加载器(System Class Loader)/ 应用程序类加载器(Application Class Loader):这也是Java语言实现的,负责加载用户类路径(ClassPath)上的指定类库,是我们平时编写Java程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。它可以通过ClassLoader.getSystemClassLoader()方法获取到。
- 自定义类加载器(Custom Class Loader):开发者可以根据需求定制类的加载方式,比如从网络加载class文件、数据库、甚至是加密的文件中加载类等。自定义类加载器可以用来扩展Java应用程序的灵活性和安全性,是Java动态性的一个重要体现。
什么是双亲委派模型?
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
双亲委派模型的作用
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
- 为了安全,保证类库API不会被修改
- 支持隔离和层次划分:双亲委派模型支持不同层次的类加载器服务于不同的类加载需求,如应用程序类加载器加载用户代码,扩展类加载器加载扩展框架,启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和扩展。
- 简化了加载流程:通过委派,大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率
讲一下类加载过程?

- 加载:通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 连接:验证、准备、解析 3 个阶段统称为连接。
-
- 验证:确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证
- 准备:为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了
- 解析:解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用, 那引用的目标必定已经存在在内存中了。
- 初始化:初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法(() ),要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。
- 使用:使用类或者创建对象
- 卸载:一个类要被JVM卸载,条件非常苛刻,需要同时满足以下三点:
-
- 该类所有的实例都已经被回收:这是最显而易见的前提。如果堆中还存在这个类的任何一个实例对象,那么定义这个对象的Class对象肯定不能被卸载。
- 加载该类的ClassLoader已经被回收:这是最关键也是最难满足的条件。类与其加载器是双向绑定的共生关系。一个类由哪个类加载器加载,这个信息是存储在Class对象里的。要卸载一个类,必须先卸载加载它的类加载器。
- 类对应的Java.lang.Class对象没有任何地方被引用:不能在任何地方通过反射(如静态字段、全局变量)、静态变量、JNI等途径引用到这个Class对象。一旦这个Class对象还存在强引用,GC就不会回收它,那么这个类也就不会被卸载。
讲一下类的加载和双亲委派原则
我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为Class 对象,这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
• 验证(Verification):JVM 需要核验class文件中的字节流包含的信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
• 准备(Preparation):为类中的静态字段分配内存,并设置默认的初始值,
• 解析(Resolution):将常量池中的符号引用(symbolic reference)替换为直接引用。
• 初始化:执行类的构造器方法
- 使用:使用类或者创建对象
- 卸载:一个类要被JVM卸载,条件非常苛刻,需要同时满足以下三点:
-
- 该类所有的实例都已经被回收:这是最显而易见的前提。如果堆中还存在这个类的任何一个实例对象,那么定义这个对象的Class对象肯定不能被卸载。
- 加载该类的ClassLoader已经被回收:这是最关键也是最难满足的条件。类与其加载器是双向绑定的共生关系。一个类由哪个类加载器加载,这个信息是存储在Class对象里的。要卸载一个类,必须先卸载加载它的类加载器。
- 类对应的Java.lang.Class对象没有任何地方被引用:不能在任何地方通过反射(如静态字段、全局变量)、静态变量、JNI等途径引用到这个Class对象。一旦这个Class对象还存在强引用,GC就不会回收它,那么这个类也就不会被卸载。
双亲委派模型:
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
垃圾回收
什么是Java里的垃圾回收?如何触发垃圾回收
GC是一种自动内存管理机制,负责释放不再使用的对象所占用的内存,避免内存泄漏,提高程序运行效率。
- 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
- 手动请求:手动调用 System.gc() 或 Runtime.getRuntime().gc() 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
- JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。
- 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。
判断垃圾的方法有哪些?
引用计数法和可达性分析算法。
引用计数法(Reference Counting)
- 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
- 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。
可达性分析算法(Reachability Analysis)
定义:会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。

GC Roots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象、
- 方法区中类静态属性引用的对象、
- 静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈中JNI(Java Native Interface)引用的对象、
- 活跃线程的引用等。
注意点:可达性分析
X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。
finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。
垃圾回收算法是什么,是为了解决了什么问题?
垃圾回收机制来自动管理内存。
垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。
为了解决这两个问题:
- 内存泄漏:程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。
- 内存溢出:是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。
通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。
垃圾回收算法有哪些?
- 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,清除阶段 统一回收所有被标记的对象。
-
- 缺点:
-
-
- 标记和清除两个动作都需要遍历所有的对象,效率低;在GC时,需要停止应用程序
- 清理出来的内存碎片化严重
-
- 复制算法:将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。
-
- 优点:解决了空间碎片的问题。
- 缺点:内存利用率严重不足。
- 标记-整理算法:标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
-
- 优点:存活对象较少的情况下效率比较高,
- 缺点:但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。
- 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

垃圾回收器有哪些?

- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
-
- 特点是:G1回收的范围是整个Java堆(包括新生代,老年代)
-
-
- 前六种收集器回收的范围仅限于新生代或老年代
-
串行垃圾收集器:
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
-
-
- Serial 作用于新生代,采用复制算法
- Serial Old 作用于老年代,采用标记-整理算法
-
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
并行垃圾收集器:
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
-
-
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记-整理算法
-
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器:
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
G1(Garbage First)收集器 (标记-整理算法):
Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
特点是:G1回收的范围是整个Java堆(包括新生代,老年代)
前六种收集器回收的范围仅限于新生代或老年代
注意:新生代一般用复制算法,老年代一般用标记整理算法(cms采用标记清除算法)
标记清除算法的缺点是什么?
- 一个是效率问题,标记和清除过程的效率都不高;
- 另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多
垃圾回收算法哪些阶段会stop the world?
下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:

G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。
标记阶段停顿分析
• 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
• 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
• 再标记阶段:重新标记那些在并发标记阶段漏标的对象。该阶段是STW的。
清理阶段停顿分析
• 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
复制阶段停顿分析(这里就是混合回收阶段)
• 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。
因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。(也就是最后混合回收新生代+老年代的阶段)
minorGC、majorGC、fullGC的区别,什么场景触发full GC
- Minor GC (Young GC)
-
- 作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。
- 触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。
-
-
- 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
-
- Major GC
-
- 作用范围:主要针对老年代进行回收,但不一定只回收老年代。
- 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。
-
-
- 特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
-
- Full GC
-
- 作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
- 触发条件:
-
-
- 直接调用System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。
- Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
- 当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。
-
-
- 特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。
垃圾回收器 CMS 和 G1的区别?
- 区别一:使用的范围不一样:
-
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
- G1收集器收集范围是新生代和老年代。不需要结合其他收集器使用
- 区别二:STW的时间:
CMS:以低停顿为目标,但停顿时间不可预测、不可控。
G1:不仅停顿短,还能指定最大停顿时间,停顿可预测、可控。
- 区别三: 垃圾碎片
-
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-整理”算法,进行了空间整合,没有内存空间碎片。
- 区别四: 垃圾回收的过程不一样
注意这两个收集器第四阶段得不同

- 区别五: CMS会产生浮动垃圾
浮动垃圾:在 GC 运行过程中新产生的垃圾,这次 GC 来不及标记、来不及收,只能留到下一次 GC 才收的垃圾。
-
- CMS 并发清除时,用户线程还在跑,产生浮动垃圾必须预留内存,否则触发 Concurrent Mode Failure,退化为 Serial Old,停顿极长。
- G1 也会产生新垃圾,但不叫浮动垃圾。新垃圾留到下次回收,且 G1 回收更可控,更不容易并发失败,触发 Concurrent Mode Failure,会退化到 Full GC
两者都可能内存不够、回收赶不上分配( Concurrent Mode Failure )并退化,但 CMS 非常容易发生,G1 很难发生。
什么情况下使用CMS,什么情况使用G1?
CMS适用场景:
• 低延迟需求:适用于对停顿时间要求敏感的应用程序。
• 老生代收集:主要针对老年代的垃圾回收。
• 碎片化管理:容易出现内存碎片,可能需要定期进行Full GC来压缩内存空间。
G1适用场景:
• 大堆内存:适用于需要管理大内存堆的场景,能够有效处理数GB以上的堆内存。
• 对内存碎片敏感:减少内存碎片,降低了碎片化对性能的影响。
• 比较平衡的性能:G1在提供较低停顿时间的同时,也保持了相对较高的吞吐量。
G1回收器的特色是什么?
G1 的特点:
• G1最大的特点是引入分区的思路,弱化了分代的概念。
• 合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷
G1 相比较 CMS 的改进:
- G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。
- G1 停顿时间可控
- G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。
分区:
- 传统 GC(Serial/Parallel/CMS):物理上严格分块一块固定是年轻代,一块固定是老年代,界限分明。
- G1:不固定划分,把整个堆切成很多个小 Region(分区)每个 Region 逻辑上可以是 Eden、Survivor、Old,但物理上没有固定边界,想用哪个当哪个。
G1 逻辑上仍分代,但物理上不分块,结构更灵活。
好处:
- 可以只挑部分垃圾多的 Region 回收,不用每次都收整个老年代,停顿可控。
- 内存利用率更高,不会像 CMS 那样有大量内存碎片。
- 方便控制停顿时间,每次少收一点,时间就短,符合
MaxGCPauseMillis。
GC只会对堆进行GC吗?
JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)