JAVA虚拟机面试总结

JAVA虚拟机面试总结

JVM的内存模型介绍一下
  • **程序计数器:**JVM 里的程序计数器(Program Counter Register)是一块较小的内存空间,其作用是存储当前线程正在执行的字节码指令地址。它是线程私有的,每个线程都有独立的程序计数器,生命周期与线程相同。若线程执行的是 Java 方法,计数器记录的是正在执行的字节码指令地址;若执行的是本地(Native)方法,计数器值为undefined。程序计数器是唯一在 JVM 规范中没有规定任何OutOfMemoryError情况的区域,其主要目的是为了线程切换后能恢复到正确的执行位置,保证程序的有序执行。·
  • **虚拟机栈:**JVM中的虚拟机栈(VM Stack)是线程私有的内存区域,用于存储方法执行时的栈帧(Stack Frame)。每个栈帧包含局部变量表、操作数栈、动态链接和方法出口等信息,方法调用时创建栈帧并压入栈顶,方法返回时栈帧出栈。虚拟机栈的生命周期与线程一致,支持方法调用的先进后出(FILO)特性。若线程请求的栈深度超过限制(如无限递归),会抛出StackOverflowError;若栈动态扩展失败,会抛出OutOfMemoryError。它是Java方法执行的核心数据结构,确保线程内的方法调用链能正确恢复执行上下文。
  • **本地方法栈:**JVM中的本地方法栈(Native Method Stack)是线程私有的内存区域,用于支持本地(Native)方法的执行。与虚拟机栈类似,它在方法调用时创建栈帧,存储局部变量、操作数栈和动态链接等信息,但服务对象是由C/C++实现的本地方法。当线程调用本地方法时,JVM会切换到本地方法栈执行本地代码,方法返回后恢复Java字节码执行。不同JVM实现对本地方法栈的处理有所差异,例如HotSpot虚拟机将其与虚拟机栈合并。本地方法栈同样可能抛出StackOverflowError(栈深度超限)和OutOfMemoryError(扩展失败)。它为Java提供了与操作系统、硬件交互的桥梁,常用于JNI(Java Native Interface)、反射等场景。
  • **堆:**JVM中的堆(Heap)是所有线程共享的内存区域,用于存储对象实例和数组。作为垃圾回收(GC)的主要管理区域,堆被划分为新生代(Young Generation)和老年代(Old Generation):新生代包含Eden区和两个Survivor区(S0、S1),新创建的对象优先在Eden区分配,经过多次GC仍存活的对象会被移至老年代。堆的大小可通过-Xms(初始大小)和-Xmx(最大大小)参数配置,若对象分配时堆空间不足,会触发GC,若GC后仍无法满足需求则抛出OutOfMemoryError
  • **方法区:**JVM 中的方法区(Method Area)是线程共享的内存区域,用于存储已被虚拟机加载的类信息(如类结构、方法字节码)、常量、静态变量以及编译器编译后的代码等数据。它逻辑上属于堆的一部分,但 HotSpot 虚拟机在 JDK 7 及以前通过永久代(PermGen)实现,而 JDK 8 及以后改用元空间(Metaspace),后者使用本地内存而非堆内存,避免了永久代的内存溢出问题。方法区的大小可通过-XX:MetaspaceSize-XX:MaxMetaspaceSize等参数配置,若类加载过多导致空间不足,会抛出OutOfMemoryError: Metaspace
  • 运行时常量池:运行时常量池(Runtime Constant Pool)是方法区(在JDK 8及以后为元空间)的一部分,用于存储类加载后解析的编译期常量符号引用(如类名、方法名、字段名)。它不仅包含编译器生成的常量池(如字符串字面量、基本数据类型常量),还支持运行时动态生成常量(如通过String.intern()将字符串添加到常量池)。运行时常量池的特点是全局共享具备动态性,例如两个不同类的相同字符串常量会指向同一个运行时常量池实例。若常量池无法申请到足够内存,会抛出OutOfMemoryError: Metaspace(JDK 8+)。它是Java实现字符串驻留(String Interning)和符号引用解析的核心机制,确保了常量的高效复用和引用的准确解析。
  • **直接内存:**直接内存(Direct Memory)并非 JVM 运行时数据区的一部分,而是指 Java 堆外的、直接向操作系统申请的内存区域。它通过java.nio.DirectByteBuffer等类进行操作,避开了 Java 堆与 native 堆之间的数据拷贝,能显著提升 NIO(非阻塞 IO)操作的性能,尤其适用于频繁的网络通信、文件读写等场景。直接内存的分配不受 JVM 堆大小限制,但受限于系统总内存,其回收机制依赖于垃圾回收(通过 Cleaner 或 PhantomReference 触发),若分配过多可能导致OutOfMemoryError。由于它不属于 JVM 管理的内存,-Xmx等堆参数无法直接控制其大小,通常需通过-XX:MaxDirectMemorySize指定上限。
JVM内存模型里的堆和栈有什么区别?
  • **存储内容不同:**栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据。每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除。堆用于存储对象的实例(包括类的实例和数组)。当你使用new关键字创建一个对象时,对象的实例就会在堆上分配空间。
  • **生命周期:**栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用时才被回收。
  • 可见性:堆是所有线程共享的内存区域,一个 JVM 实例只有一个堆;虚拟机栈则是线程私有的,每个线程创建时都会分配独立的栈,线程间的栈数据互不干扰。
  • **存取速度:**栈的存取速度远快于堆,这是因为栈采用连续内存布局,遵循先进后出原则,分配与释放仅需移动栈指针,操作简单且无多线程竞争,加之连续空间易被 CPU 缓存高效命中;而堆中对象多为离散存储,分配需复杂算法寻找空间且可能涉及同步,访问需通过引用间接寻址,易导致缓存失效,再加上垃圾回收机制运行时的额外开销,使得堆的存取效率远低于栈。
  • **存储空间:**栈的空间相对较小且线程私有,每个线程的栈容量默认通常为 1MB(可通过 -Xss 参数调整),由 JVM 在创建线程时动态分配和管理。当栈溢出时,通常是因为递归调用过深导致栈帧数量过多,或局部变量占用空间过大(如定义超大数组)。堆的空间较大且线程共享,默认大小通常为物理内存的 1/4(可通过 -Xmx 参数调整),支持动态扩展,由 JVM 通过垃圾回收机制(GC)自动管理。堆溢出通常是由于持续创建大量大对象,或存在内存泄漏(如静态集合持有对象引用导致无法被 GC 回收)。
栈中存的到底是指针还是对象?
  • 基本数据类型(如 int、float、bool 等):在栈中直接存储值本身

  • 引用类型(对象、数组等):栈中通常存储的是指向堆中对象的指针(或引用地址),而对象的实际数据存放在堆中。比如 Java 中String s = new String("test");,栈中存储的是s这个引用(可理解为指向堆中字符串对象的指针),真正的 “test” 字符数据则在堆中。

堆分为几部分?
  • 年轻代(Young Generation):用于存放新创建的对象,分为 Eden 区(伊甸园)和两个 Survivor 区(幸存者区,通常称为 From 区和 To 区)。新对象优先在 Eden 区分配,当 Eden 区满时触发 Minor GC,存活对象会被复制到其中一个 Survivor 区;经过多次 GC 仍存活的对象,会逐渐晋升到年老代。
  • 年老代(Old Generation/Tenured Generation):用于存放存活时间较长的对象,当年轻代中经过多次 Minor GC 仍存活的对象(达到一定年龄阈值)会被移至此区域。年老代的 GC(Major GC/Full GC)频率较低,但回收耗时更长,因为区域更大且对象存活时间久。
  • 元空间(Metaspace,JDK 8 及以后):替代了之前的永久代(PermGen),用于存储类信息、方法元数据、常量池等与类结构相关的数据。元空间不属于堆的传统范畴,但与堆紧密关联,其内存直接分配在本地内存中,大小受系统内存限制,避免了永久代因内存不足导致的 OOM 问题。
  • **大对象区:**在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。
程序计数器的作用,为什么是私有的?

程序计数器是 JVM 中用于记录当前线程正在执行的字节码指令地址的内存区域,其核心作用是支持指令的顺序执行、分支跳转及线程切换时的上下文恢复;而它被设计为线程私有的原因,在于多线程环境下需确保各线程执行路径独立隔离,避免指令执行混乱,同时保障线程切换和异常处理时的上下文正确性,是线程并发执行的基础保障。

方法区的方法执行流程
  1. 加载字节码:从方法区获取被调用方法的字节码指令。
  2. 创建栈帧并压栈:在当前线程的虚拟机栈中为该方法创建栈帧,并压入栈顶。
  3. 参数传递:将实际参数复制到栈帧的局部变量表中。
  4. 执行字节码指令:
    • 通过程序计数器(PC 寄存器)依次读取字节码指令。
    • 使用操作数栈进行计算(如iadd指令将两个整数相加)。
    • 局部变量表读取 / 存储数据。
  5. 方法返回:
    • 返回值存入调用者栈帧的操作数栈。
    • 当前栈帧出栈,释放局部变量表和操作数栈。
    • 程序计数器恢复为调用者方法的下一条指令地址。
String保存在哪里?
创建方式存储位置示例
字面量赋值("xxx"字符串常量池String s = "hello";
new String("xxx")String s = new String("hello");
intern()方法入池字符串常量池String s = new String("java").intern();
运行时动态拼接(变量 + 变量)String s = a + b;(a、b 为变量)
String s = new String(“abc”)执行过程中分别对应哪些内存区域?

字符串常量池和堆内存。

  • 第一个对象:字符串常量池中的 “abc”
    当 JVM 首次遇到字面量 “abc” 时,会先检查常量池。由于不存在,会在常量池中创建一个 “abc” 字符串对象(存储字符数据)。这是因为字面量本身是编译期确定的常量,JVM 会自动将其放入常量池以实现复用。
  • 第二个对象:堆中的String实例
    new String(...)关键字的语义是强制在堆中创建一个新的String对象,该对象会引用常量池中 “abc” 的字符数据(JDK 7 + 为引用共享,而非拷贝)。即使常量池已有相同内容,new也会保证生成一个新的堆对象。
引用类型有哪些,有什么区别?
引用类型回收条件典型用途强度等级(从强到弱)代码示例
强引用无强引用指向对象时(如 obj=null常规对象存储(如成员变量、局部变量)最强Object obj = new Object();
软引用内存不足(即将 OOM)时内存敏感的缓存(如图片缓存)较强SoftReference<Object> ref = new SoftReference<>(new Object());
弱引用发生 GC 时(无论内存是否充足)临时关联数据(如 WeakHashMap较弱WeakReference<Object> ref = new WeakReference<>(new Object());
虚引用对象被 GC 回收时(仅用于接收通知)跟踪对象回收状态(如释放 Native 资源)最弱PhantomReference<Object> ref = new PhantomReference<>(obj, queue);
字符串常量池引用无强引用且被 JVM 卸载时(极罕见)字符串复用(如字面量 "abc"同强引用String s = "abc";(若常量池已有 "abc",则 s 直接引用池中的对象)
类引用类加载器被回收且无其他引用时反射操作(如 Class.forName()较强Class<?> cls = String.class;
弱引用的使用场景
  • **缓存系统:**弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。如果缓存的大小不受控制,可能会导致内存溢出。使用弱引用来维护缓存,可以让JVM在需要更多内存时自动清理这些缓存对象。
  • **对象池:**在对象池中,弱引用可以用来管理那些暂时不使用的对象。当对象不再被强引用时,它们可以被垃圾回收,释放内存。
  • **避免内存泄漏:**当一个对象不应被长期引用时,使用弱引用可以防止该对象被意外保留,避免内存泄露。
内存泄露和内存溢出的理解?
  • **内存泄露:**程序中某些对象不再被使用,但由于仍然被强引用持有,导致 GC 无法回收它们,最终造成内存占用持续增长。
    • 核心原因:
      • 长生命周期对象持有短生命周期对象的引用:例如静态集合(如 static List)存储临时对象,或单例模式持有外部资源(如数据库连接)。
      • 资源未正确释放:如 InputStreamConnection 等资源未关闭,导致对象无法被回收。
      • 内部类持有外部类的引用:非静态内部类会隐式持有外部类的引用,可能导致外部类无法被回收。
  • **内存溢出:**内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError
    • 核心原因:
      • 内存泄漏的累积:长期的内存泄漏最终耗尽可用内存。
      • 大对象分配:如创建超大数组(new byte[1024*1024*100])或加载大文件。
      • 内存配置不合理:JVM 堆内存(-Xmx)设置过小,无法满足程序运行需求。
      • 无限循环或递归:如无终止条件的递归调用,不断创建新对象。
jvm 内存结构有哪几种内存溢出的情况
内存区域溢出类型触发条件典型错误信息常见场景
堆内存(Heap)堆溢出(最常见)1. 创建的对象过多且未被回收(如无限循环创建对象) 2. 内存泄漏累积导致堆空间耗尽 3. 堆内存配置过小(-Xmx 不足)java.lang.OutOfMemoryError: Java heap space- 批量处理大量数据时未分页 - 内存泄漏(如静态集合持有大量对象) - 大对象分配(如超大数组 new byte[1024*1024*1000]
方法区 / 元空间方法区 / 元空间溢出1. 加载的类过多(如动态生成类的框架:CGLib、反射) 2. 常量池过大(如大量字符串 intern 操作) 3. 元空间配置过小(-XX:MaxMetaspaceSize 不足)JDK 7 及以前:java.lang.OutOfMemoryError: PermGen space JDK 8+:java.lang.OutOfMemoryError: Metaspace- 频繁使用动态代理生成类 - 应用服务器热部署类未卸载(类加载器泄漏) - 常量池中存储大量字符串且未被回收
虚拟机栈 / 本地方法栈栈溢出(Stack Overflow)1. 方法调用层级过深(如无限递归) 2. 单个线程栈空间过小(-Xss 配置不足)java.lang.StackOverflowError- 无终止条件的递归调用(如 public void f() { f(); }) - 复杂算法导致调用栈过深
虚拟机栈 / 本地方法栈栈内存溢出(线程创建过多)1. 创建大量线程,每个线程占用栈空间,总栈内存超过系统限制 2. 栈空间配置过大(-Xss 过大)导致总内存超限java.lang.OutOfMemoryError: unable to create new native thread- 短时间内创建 thousands 级线程(如循环创建线程且不销毁) - 32 位系统中进程总内存有限,线程栈过多易触发
直接内存(Direct Memory)直接内存溢出1. NIO 中 ByteBuffer.allocateDirect() 分配的直接内存过多 2. 直接内存未被释放(如未调用 cleaner()) 3. 直接内存配置过小(-XX:MaxDirectMemorySize 不足)java.lang.OutOfMemoryError: Direct buffer memory- 频繁使用 NIO 操作大文件,未及时释放直接内存 - 框架(如 Netty)误用直接内存导致泄漏
有具体的内存泄漏请举例及解决方案?
  • 静态属性导致内存泄露

    静态属性的生命周期与 JVM 一致,添加的对象永远不会被 GC 回收,导致内存持续占用,最终可能引发OutOfMemoryError: Java heap space

    **如何优化?**第一,进来减少静态变量;第二,如果使用单例,尽量采用懒加载。

  • 未关闭的资源

    无论什么时候当我们创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如,数据库链接、输入流和session对象。忘记关闭这些资源,会阻塞内存,从而导致GC无法进行清理。

    **如何优化?**第一,始终记得在finally中进行资源的关闭;第二,关闭连接的自身代码不能发生异常;第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。

  • 使用ThreadLocal

    ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

    **如何优化?**第一,使用ThreadLocal提供的remove方法,可对当前线程中的value值进行移除;第二,不要使用ThreadLocal.set(null) 的方式清除value,它实际上并没有清除值,而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。第三,最好将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。

创建对象的过程
  1. 类加载检查(Class Loading Check)

当执行new Person(...)时,JVM 首先会检查:

  • 这个类(Person)是否已被加载到方法区(Method Area)中。
  • 如果未加载,JVM 会通过类的全限定名(如com.example.Person)执行类加载过程(加载→验证→准备→解析→初始化),将类的信息(属性、方法、常量等)存入方法区。
  1. 为对象分配内存

类加载完成后,JVM 会为新对象在堆内存(Heap) 中分配空间,空间大小由类的属性(成员变量)决定(包括继承自父类的属性)。

  • 分配方式:
    • 指针碰撞:若堆内存是连续的(如使用 Serial、ParNew 等收集器),JVM 通过一个指针划分已用和未用内存,分配时移动指针即可。
    • 空闲列表:若堆内存碎片化(如 CMS 收集器),JVM 需维护一张 “空闲内存块列表”,从中找到合适的块分配给对象。
  • 线程安全:为避免多线程同时分配内存导致的冲突,JVM 会采用TLAB(Thread Local Allocation Buffer,本地线程分配缓冲),即每个线程在堆中预留一块私有内存,优先在 TLAB 中分配,减少锁竞争。
  1. 初始化对象内存(零值与设置)

内存分配后,JVM 会先将分配的内存空间初始化为零值(如int为 0,Stringnull),这一步保证了对象即使未显式初始化,也能访问到默认值。
随后,JVM 会设置对象的对象头(Object Header) 信息,包括:

  • 类的元数据指针(指向方法区中该类的信息);
  • 哈希码(后期懒加载,首次调用hashCode()时计算);
  • GC 分代年龄、锁状态等标记信息。
  1. 执行构造方法(初始化对象)

零值初始化后,JVM 会调用类的构造方法(Constructor),对对象进行显式初始化(如为nameage赋值)。

  • 构造方法的执行顺序:先调用父类的构造方法(通过super()),再执行当前类构造方法中的代码。
  • 若类未定义构造方法,Java 会自动生成一个无参默认构造方法。

5. 将对象引用赋值给变量

最后,JVM 将堆中对象的内存地址(引用)赋值给栈内存中的变量(如Person p = new Person(...)中的p),此后通过该引用即可操作对象。

对象的生命周期
  • **创建:**对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
  • **使用:**对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
  • **销毁:**当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
类加载器有哪些?
  1. 启动类加载器(Bootstrap ClassLoader)
    • 核心职责:加载 Java 的核心类库,如java.lang.*java.util.*等。
    • 加载路径JRE/lib目录下的类库(如rt.jarcharsets.jar)。
    • 实现方式:由 C++ 实现,属于 JVM 的一部分,在 Java 代码中无法直接引用。
    • 示例类java.lang.Objectjava.util.ArrayList
  2. 扩展类加载器(Extension ClassLoader)
  • 核心职责:加载 Java 的扩展类库,提供标准核心类之外的功能。
  • 加载路径JRE/lib/ext目录下的类库(如 XML 解析器、加密算法等)。
  • 实现方式:由 Java 代码实现,继承自java.net.URLClassLoader
  • 示例类javax.crypto.*包中的类。
  1. 应用类加载器(Application ClassLoader)
  • 核心职责:加载应用程序classpath下的类库,即开发者编写的代码及依赖。
  • 加载路径:环境变量CLASSPATHmaven/gradle等构建工具配置的依赖路径。
  • 实现方式:由 Java 代码实现,继承自URLClassLoader,是ClassLoader.getSystemClassLoader()的返回值。
  • 示例类:项目中自定义的类(如com.example.MyClass)。
  1. 自定义类加载器(Custom ClassLoader)
  • 核心职责:开发者通过继承java.lang.ClassLoaderURLClassLoader实现特殊加载需求。
  • 常见场景:
    • 从网络、数据库或加密文件加载类;
    • 实现热部署(如 Web 容器中的类加载);
    • 隔离不同模块的类(如 OSGi 框架)。
  • 实现关键点:
    • 重写findClass()方法(推荐)或loadClass()方法(需谨慎,避免破坏双亲委派)。
父加载器
父加载器
父加载器
加载路径
加载路径
加载路径
加载路径
启动类加载器
Bootstrap
扩展类加载器
Extension
应用类加载器
Application
自定义类加载器
Custom
JRE/lib/*
JRE/lib/ext/*
classpath
自定义路径
讲一下双亲委派模型

双亲委派模型的工作原理是:当一个类加载器收到加载类的请求时,它不会先自己尝试加载,而是先将请求委派给父类加载器处理;父类加载器同样会把请求向上委派,直到传递到最顶层的启动类加载器;之后从启动类加载器开始,逐层向下尝试加载该类,若某个类加载器能成功加载就返回结果,若所有父类加载器都无法加载,最终才由最初发起请求的类加载器自行加载。这一过程通过“向上委派请求、向下查找加载”的方式,确保了类加载的安全性和唯一性。

如果在向下查找时刚好遇到最初请求的类加载器才能成功加载,此时属于什么情况?

这种场景属于 “所有父类加载器都无法加载,回到最初请求的类加载器加载”。因为 “向下查找” 的过程本身就包含了对 “所有父类是否能加载” 的验证 —— 只有当上层父类全部失败后,才会轮到最初的加载器尝试,而它的成功正是 “父类均失败” 的直接结果。两者并非矛盾,而是同一过程的前后逻辑:“向下查找” 是流程形式,“父类均失败后自身加载” 是该流程在终点处的具体结果

双亲委派模型的作用
  1. 保证类的唯一性与安全性
    通过 “向上委派、向下查找” 的机制,确保同一个类(全限定名相同)只会被最顶层的类加载器加载一次。例如,java.lang.Object类只会由启动类加载器加载,任何自定义类加载器都无法自行加载同名类,避免了 “恶意类篡改核心类” 的风险(如伪造java.lang.String类替换系统类),保障了 Java 核心库的安全性。
  2. 避免类的重复加载
    由于类加载请求会优先委派给父类加载器,若父类已加载过该类,则直接返回结果,无需子类加载器重复加载。这减少了内存中类的冗余,保证了类在 JVM 中的唯一性(类的唯一性由 “类加载器 + 全限定名” 共同决定,同一类被不同加载器加载会视为不同类)。
  3. 维护类加载的层级关系
    双亲委派模型明确了不同类加载器的职责边界:启动类加载器负责加载核心库(如rt.jar),扩展类加载器加载扩展库(如ext目录),应用类加载器加载应用程序类,自定义类加载器则处理特殊需求(如加密类、网络加载类)。这种层级划分使得类加载流程清晰可控,符合 Java “沙箱安全” 和 “模块化设计” 的理念。
讲一下类加载的过程

**加载:**通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

**连接:**验证、准备、解析 3 个阶段统称为连接。

**验证:**确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证

**准备:**为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了

**解析:**解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用, 那引用的目标必定已经存在在内存中了。

初始化:初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法,要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。

**使用:**使用类或者创建对象

**卸载:**如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。 3. 类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

什么是Java里的垃圾回收?如何触发垃圾回收?

在 Java 中,垃圾回收(Garbage Collection,简称 GC) 是 JVM(Java 虚拟机)自动管理内存的机制,用于识别并回收不再被程序使用的对象所占用的内存空间,避免内存泄漏和溢出,简化开发者的内存管理工作。

  • 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求:虽然垃圾回收是自动的,开发者可以通过调用 System.gc() 或 Runtime.getRuntime().gc() 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的时机,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。
判断垃圾的方法有哪些?
  • 引用计数法:

    原理
    为每个对象维护一个引用计数器,当有新的引用指向该对象时,计数器加 1;当引用失效(如引用被赋值为null或超出作用域)时,计数器减 1。当计数器为 0 时,对象被视为垃圾。

    缺陷
    无法解决循环引用问题。例如:

    class A { public B b; }
    class B { public A a; }
    
    A objA = new A();
    B objB = new B();
    objA.b = objB; // A引用B
    objB.a = objA; // B引用A
    objA = null;
    objB = null
    

    此时,objAobjB相互引用,各自的引用计数器为 1,但外部已无引用指向它们,导致它们无法被回收,造成内存泄漏。

  • 可达性算法:

    原理
    以一组称为GC Roots的对象为起点,通过引用链(Reference Chain)遍历所有可达对象(即 “活对象”),未被遍历到的对象则被视为不可达(即垃圾)。

    GC Roots 的类型(以下对象永远不会被回收):

    1. 虚拟机栈(栈帧中的本地变量表)引用的对象:

      例如方法中的局部变量。

      public void method() {
          Object obj = new Object(); // obj是GC Root,指向的对象不可回收
      } // 方法结束后,obj出栈,其指向的对象可能被回收
      
    2. 方法区中类静态属性引用的对象:

      例如static变量。

      public class Demo {
          static Object staticObj = new Object(); // staticObj是GC Root
      }
      
    3. 方法区中常量引用的对象:

      例如final常量。

      public class Demo {
          final static Object CONST_OBJ = new Object(); // CONST_OBJ是GC Root
      }
      
    4. 本地方法栈中 JNI(Native 方法)引用的对象
      例如 Java 调用 C/C++ 代码时,Native 方法引用的 Java 对象。

    5. JVM 内部的引用
      如类加载器、基本数据类型的 Class 对象、常驻的异常对象(如NullPointerException)等。

垃圾回收算法有哪些?
一、标记 - 清除算法(Mark-Sweep)

核心思想:分为 “标记” 和 “清除” 两个阶段,是最基础的垃圾回收算法。

  1. 标记阶段
    通过可达性分析(见前文)标记所有可回收的对象(垃圾)。
  2. 清除阶段
    遍历堆内存,清除所有被标记的垃圾对象,释放其占用的内存空间。

示例
堆中存在 A(存活)、B(垃圾)、C(存活)、D(垃圾)四个对象,标记后清除 B 和 D,剩余 A 和 C。

优点
实现简单,无需移动对象。

缺点

  • 内存碎片:清除后会产生大量不连续的内存碎片,当需要分配大对象时,可能因找不到足够大的连续内存而触发额外的 GC。
  • 效率低:标记和清除过程都需要遍历所有对象,耗时较长,尤其在对象数量多时。
二、复制算法(Copying)

核心思想:将堆内存分为两块大小相等的区域(如 From 区和 To 区),每次只使用其中一块(From 区)。当 From 区满时,将存活对象复制到另一块未使用的区域(To 区),然后清空 From 区,交换两者的角色(From 变 To,To 变 From)。

示例
From 区有 A(存活)、B(垃圾)、C(存活),复制时仅将 A 和 C 复制到 To 区,然后清空 From 区,后续新对象分配到原 To 区(现在的 From 区)。

优点

  • 无内存碎片,因为存活对象被连续复制到新区域。
  • 清除效率高,只需直接清空整个区域,无需逐个判断对象。

缺点

  • 内存利用率低:始终有一半内存处于空闲状态(如 100MB 堆内存,实际可用仅 50MB)。
  • 复制成本高:若存活对象较多(如老年代),复制操作会消耗大量资源。
三、标记 - 整理算法(Mark-Compact)

核心思想:结合 “标记 - 清除” 和 “复制” 的优点,分为 “标记”“整理” 两个阶段。

  1. 标记阶段:与 “标记 - 清除” 相同,标记所有可回收对象。
  2. 整理阶段:将所有存活对象向内存空间的一端移动,然后直接清除边界以外的所有垃圾对象。

示例
堆中标记出 B 和 D 为垃圾,整理时将 A 和 C 移动到内存起始位置,然后清除 A、C 之后的所有内存(包括 B 和 D)。

优点

  • 解决了 “标记 - 清除” 的内存碎片问题。
  • 解决了 “复制” 的内存利用率低问题(无需预留一半空间)。

缺点

  • 整理阶段需要移动对象,会消耗额外的时间(尤其对象较多时),且需要更新所有引用该对象的指针。
四、分代收集算法(Generational Collection)

核心思想:根据对象的存活周期,将堆内存划分为不同区域(新生代、老年代、永久代 / 元空间),针对不同区域采用不同的回收算法。
这是当前所有商用 JVM 的默认垃圾回收策略(非独立算法,而是对上述算法的组合应用)。

  1. 新生代(Young Generation)
    • 特点:对象存活时间短,存活率低(大部分对象创建后很快被回收)。
    • 算法:采用复制算法(通常细分为 Eden 区、From Survivor 区、To Survivor 区,比例为 8:1:1)。
    • 回收过程:
      • 对象分配
        新对象优先分配到 Eden 区。若对象过大(超过 -XX:PretenureSizeThreshold),则直接进入 老年代
      • 首次 Minor GC 触发
        Eden 区满 时,触发第一次 Minor GC:
        • 标记阶段:通过可达性分析标记 Eden 区中的存活对象。
        • 复制阶段:
          • 存活对象被复制到 To 区(初始时 To 区为空),对象年龄 +1(初始为 1)。
          • From 区保持为空(首次 GC 前 Survivor 区无对象)。
        • 角色切换:GC 结束后,To 区变为下次的 From 区,原 From 区变为下次的 To 区。
      • 后续 Minor GC 触发
        当 Eden 区再次满时,触发后续 Minor GC:
        • 标记阶段:标记 Eden 区当前 From 区(上次的 To 区)中的存活对象。
        • 复制阶段:
          • 将存活对象复制到 当前 To 区(上次的 From 区,此时为空),年龄 +1
          • 清空 Eden 区和当前 From 区。
        • 角色切换:GC 结束后,To 区变为下次的 From 区,原 From 区变为下次的 To 区。
      • 对象晋升老年代
        对象满足以下条件时会晋升至老年代:
        • 年龄阈值:对象年龄达到 -XX:MaxTenuringThreshold(默认 15)。
        • 动态年龄判定:若 To 区中相同年龄的对象总大小超过 To 区的一半,年龄≥该年龄的对象直接晋升。
        • 空间不足:若 To 区无法容纳所有存活对象,部分对象会直接进入老年代。
  2. 老年代(Old Generation)
    • 特点:对象存活时间长,存活率高(如缓存对象、长期存在的单例对象)。
    • 算法:采用标记 - 清除标记 - 整理算法(因对象存活率高,复制算法不适用)。
    • 回收过程:当老年代内存不足时,触发 Major GC(老年代回收),通常会伴随 Minor GC,耗时较长。
  3. 永久代 / 元空间(Permanent Generation/Metaspace)
    • 存储类信息、常量、静态变量等,一般不触发回收,除非发生类卸载(如类加载器被回收)。
垃圾回收算法哪些阶段会stop the world?

1. 标记 - 清除算法(Mark-Sweep)

  • 标记阶段
    必须暂停用户线程,遍历所有可达对象并标记,避免用户线程在标记过程中修改对象引用关系(导致标记结果错误)。
  • 清除阶段
    通常不需要 STW(仅回收未标记对象的内存空间,不涉及对象移动),但部分实现中可能短暂 STW 以整理空闲内存链表。

2. 标记 - 复制算法(Mark-Copy)

  • 标记阶段
    暂停用户线程,标记存活对象(与标记 - 清除的标记逻辑一致)。
  • 复制阶段
    必须 STW,将存活对象从 “From 区” 复制到 “To 区”,并更新引用地址(避免用户线程访问到旧地址)。
  • 切换指针阶段
    复制完成后,交换 “From 区” 和 “To 区” 的指针,此过程需 STW(确保指针切换的原子性)。

3. 标记 - 整理算法(Mark-Compact)

  • 标记阶段
    同前两种算法,需 STW 以保证标记准确性。
  • 整理阶段
    必须 STW,将存活对象向内存一端移动并更新引用(避免用户线程访问到移动过程中的无效地址)。

4. 分代回收算法(基于上述算法组合)

分代回收(如新生代用复制算法,老年代用标记 - 整理 / 清除)中,各代的回收阶段均可能 STW

  • 新生代 Minor GC:复制算法的标记和复制阶段 STW。
  • 老年代 Major GC/Full GC:标记 - 清除 / 整理的标记和整理阶段 STW。
minorGC、majorGC、fullGC的区别,什么场景触发full GC?
维度Minor GC(新生代 GC)Major GC(老年代 GC)Full GC(全局 GC)
回收区域仅新生代(Eden 区 + Survivor 区)仅老年代新生代 + 老年代(有时包含永久代 / 元空间)
触发频率高(新对象频繁分配,Eden 区易满)低(老年代对象存活时间长,空间增长慢)低(通常是 Major GC 的 “附带产物” 或特殊场景触发)
STW 时间短(新生代对象存活少,复制算法高效)较长(老年代对象多,标记 - 整理 / 清除算法耗时)最长(回收区域大,涉及对象多)
使用的回收算法标记 - 复制算法(新生代特点:对象存活率低)标记 - 清除 / 标记 - 整理算法(老年代特点:存活率高)同 Major GC(覆盖所有区域,算法组合使用)

触发Full Gc的场景

  • 直接调用System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。

  • Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。

  • 当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。

GC只会对堆进行GC吗?

堆内存的 GC

堆是 Java 垃圾回收的核心区域,所有通过new创建的对象都存储在这里。GC 会频繁针对堆的新生代(Minor GC)和老年代(Major GC/Full GC)进行操作,通过标记 - 清除、复制、标记 - 整理等算法,识别并回收不再被引用的对象,释放内存空间,这是 GC 最主要、最活跃的工作范围。

方法区 / 元空间的 GC

方法区(Java 8 前)或元空间(Java 8 及后)存储类元信息、常量池等数据,其 GC 并非主要目标,回收频率较低。当加载的类过多、常量池过大导致空间不足时,GC 会尝试回收无用的类(需满足类实例全被回收、类加载器被回收等严格条件)或未被引用的常量,以此释放空间,这一过程通常伴随 Full GC 发生。

直接内存的 GC

直接内存是通过ByteBuffer.allocateDirect()分配的堆外内存,本身不由 JVM 直接管理,但它的引用对象(DirectByteBuffer)存于堆中。当堆中的DirectByteBuffer被回收时,关联的直接内存会通过虚引用触发的 “Cleaner 机制” 释放;若直接内存不足,可能间接触发 Full GC,通过回收堆内存间接释放对应的堆外空间。

什么情况下使用CMS,什么情况使用G1?

选 CMS

  • 堆内存小(<4GB)、对象分配稳定、需极致低延迟(如 Web 服务器)、无法升级 JDK。

选 G1

  • 堆内存大(>8GB)、对象分配率高 / 波动大、需预测性停顿(如数据平台)、JDK 9+。

默认优先 G1,除非明确符合 CMS 场景。

垃圾回收器 CMS 和 G1的区别?
维度CMSG1
算法基础标记 - 清除标记 - 整理 + 复制
内存布局分代(新生代 + 老年代)分区(Region)
STW 控制依赖并发阶段减少 STW通过 Region 筛选和停顿预测模型
碎片问题标记 - 清除导致内存碎片标记 - 整理避免碎片
大内存处理老年代连续空间,大对象分配易失败Region 分散存储,支持更大内存
Full GC 频率高(并发失败或碎片导致)低(通过整理减少碎片)
适用场景中小堆内存(<4GB)、低延迟需求大内存(>8GB)、混合负载

各自的GC流程

  • CMS:

    1. 初始标记(STW,短暂):标记 GC Roots 直接关联的对象。
    2. 并发标记:与用户线程并发执行,遍历所有可达对象。
    3. 重新标记(STW,较长):修正并发期间因用户线程修改导致的标记偏差。
    4. 并发清除:与用户线程并发清除未标记对象。
  • G1:

    1. 初始标记(STW,短暂):同 CMS。
    2. 并发标记:同 CMS,但引入SATB(Snapshot At The Beginning) 减少重新标记的停顿。
    3. 最终标记(STW,短暂):处理并发阶段的遗留引用更新。
    4. 筛选回收(STW):根据停顿预测模型,选择回收价值最高的 Region(采用复制算法,将存活对象移至新 Region)。
G1回收器的特色是什么?

G1 的特点:

G1最大的特点是引入分区的思路,弱化了分代的概念。

合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷

相比较 CMS 的改进:

算法: G1 基于标记–整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。

停顿时间可控: G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。

**并行与并发:**G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值