jvm虚拟机进程何时结束生命周期
结束情形 |
说明 |
main方法或所有非守护线程执行结束 |
最常见,程序自然退出 |
调用 System.exit(int) |
人为强制退出 JVM,立即结束 |
程序抛出未捕获的异常导致主线程终止 |
异常未处理,主线程挂了,其它非守护线程也结束了 |
被操作系统或容器强制杀死 JVM 进程(如 kill) |
外部终止,如 Docker、Linux 系统命令等 |
jvm类的加载机制
Java 虚拟机加载类的整个过程分为 加载、连接(验证 → 准备 → 解析)、初始化 四个阶段。
1. 加载(Loading)
- 工作内容:
- 将类的 .class 文件字节码加载进内存。
- 把类的结构信息存放在 方法区(运行时数据区)。
- 在 堆中创建一个 Class 对象,作为访问方法区中该类数据的入口。
2. 连接(Linking)
连接阶段又分为三个小阶段:
(1)验证(Verification)
- 目的:确保字节码文件的正确性和安全性。
- 检查内容:
- 文件格式检查:类文件头、版本号、字节流完整性(如 md5 校验)。
- 语义检查:父类是否合法、final 类是否被继承、方法重载是否合法、字节码是否合法。
- 引用检查:类中用到的其他类和方法是否存在、是否能被正确访问。
(2)准备(Preparation)
- 目的:为类的 静态变量 分配内存,并设置默认初始值。
- 说明:
- 内存分配在方法区(静态变量)或堆区(实例变量)。
- 基本类型初始值:如 int → 0,long → 0L。
- final 修饰的常量在此阶段会直接赋予编译期确定的值。
(3)解析(Resolution)
- 目的:将 符号引用 转换为 直接引用。
- 说明:
- 符号引用:以类名、方法名、字段名等形式存在于字节码文件中。
- 直接引用:指向方法区具体内存地址的指针或句柄。
3. 初始化(Initialization)
- 工作内容:对类的静态变量、静态代码块执行真正的初始化。
- 执行顺序:
-
- 父类静态变量、静态代码块 →
- 子类静态变量、静态代码块 →
- 父类实例变量、构造方法 →
- 子类实例变量、构造方法。
- 注意事项:
- 如果类还没有加载或连接,则会先进行加载和连接。
- 如果直接父类未初始化,则会先初始化父类。
总结
JVM 类加载机制分为四个主要阶段:
- 加载:读取字节码文件到内存,创建 Class 对象。
- 连接:包括验证(安全性)、准备(分配内存,设置初始值)、解析(符号引用 → 直接引用)。
- 初始化:对静态变量和代码块赋值,遵循父类 → 子类的顺序。
通过这几个阶段,Java 类才能从磁盘上的 .class 文件一步步转化为 JVM 可以运行的类对象。
jvm加载class文件的方式
在 Java 虚拟机中,类的加载方式并不局限于单一的途径,常见的来源方式主要包括以下几种。最常见的方式是从本地系统文件中加载,JVM 直接从磁盘上读取 .class 文件的字节码。其次,类也可以通过网络协议下载并加载,比如早期 Applet 应用、远程类加载器或者某些动态代理框架会通过 HTTP、FTP 等方式获取字节码。另一个常见方式是从 JAR 或 ZIP 归档文件中加载,通常应用程序会将多个类文件打包成 .jar 文件,JVM 或类加载器在运行时解压并读取,例如 Spring Boot 的 fat jar 就属于这一类。
除了这些常规方式,还可以从数据库中提取字节码。某些平台会将类文件以 BLOB 等形式存储在数据库中,运行时通过自定义类加载器读取,再调用 defineClass 将其定义为 JVM 内部可用的类。最后,JVM 还支持动态编译源代码并加载类,借助 Java Compiler API(如javax.tools.JavaCompiler)可以在运行时将 .java 源文件编译成字节码并加载。
无论采用哪种加载方式,当类被成功加载后,JVM 都会在堆内存中生成一个 java.lang.Class 对象。这个对象封装了方法区中类的各种结构信息,包括字段、方法、接口、父类以及注解等元数据,并作为反射 API 的入口,例如通过 clazz.getMethods() 便可以获取类的方法信息。换言之,Class 对象既是 JVM 管理类元数据的桥梁,也是开发者在运行时操作类信息的基础。
JVM 的“懒惰报错”原则
在 JVM 的类加载机制中存在一个“懒惰报错”原则。虽然类并不一定要等到第一次使用时才加载,JVM 允许类加载器根据需要进行预加载,但即便预加载失败,例如类文件缺失、格式错误或类的静态代码块在执行时出现异常,JVM 也不会立刻抛出错误。虚拟机的要求是:只有当类被首次主动使用时,相关的异常才会真正暴露出来。
JVM 的类初始化时机
在 JVM 中,类的初始化时机主要取决于是否发生了主动引用。只要出现主动引用,虚拟机就必须立即对该类进行初始化,即执行静态代码块和静态变量的赋值逻辑。常见的主动引用场景包括:使用 new 创建类的实例;访问类的静态变量或调用静态方法;通过 Class.forName() 主动加载类;当创建子类实例时,父类会优先被初始化;以及 JVM 启动时加载并执行的主类(包含 main() 方法)。这些场景都会直接触发类的初始化。
与此相对,存在一些被动引用场景,它们不会触发类的初始化。例如,当子类访问父类的静态变量时,只会初始化父类而不会初始化子类;定义一个类的数组引用时,不会触发该类的初始化;通过 ClassName.class 获取 Class 对象时,也不会触发初始化;此外,如果访问的是编译期常量(final static 且编译时已确定值),那么该常量会直接被内联到调用处,从而不会触发类的初始化。
这种设计体现了 JVM 的延迟加载策略:只有在类真正需要执行初始化逻辑时才会进行初始化,从而提升性能并避免不必要的资源消耗。
jvm的类加载器
JVM 的类加载器(ClassLoader)是 Java 中用来将 .class 字节码文件加载到 JVM 中的关键组件。类加载器决定了某个类的 .class 文件从哪来、怎么读取,并最终转换成 JVM 中的 Class 对象。
JVM 有四种类加载器:启动类加载器负责加载核心类库,比如java.lang;扩展类加载器加载 JDK 的扩展库;应用类加载器加载我们项目的类和第三方库;还有自定义类加载器,可以让我们按需控制类加载来源,比如网络或加密文件。它们之间采用双亲委派机制,保证核心类优先加载。”
双亲委派机制是什么
当一个类加载器收到类加载请求时,它不会自己先去加载,而是把请求委托给父类加载器,由父类加载器去尝试加载该类。如果父类加载器能够完成加载,就成功返回;如果父类加载器加载失败,子加载器才会自己去加载。
为什么要用双亲委派机制?
- 保证核心类库的安全
只有启动类加载器能加载核心类(java.lang.*),防止用户自定义类覆盖系统类。
- 避免类的重复加载
同一个类由多个加载器加载会导致类型不兼容,双亲委派避免这种情况。
- 规范加载顺序
先由父类加载器尝试加载,层层委托,逐级查找。
如何实现自己的类加载器
在 Java 中,如果需要实现自己的类加载器,可以继承抽象类 java.lang.ClassLoader。用户自定义类加载器通常有两种实现方式:一种是重写 loadClass() 方法,另一种是重写 findClass() 方法。虽然 loadClass() 内部会调用 findClass(),从功能上两者可以实现类似效果,但从设计和安全性角度来看,更推荐只重写 findClass() 方法来实现自定义类的加载逻辑。
重写 findClass() 时,根据传入的类名参数,加载字节码并返回对应的 Class 对象引用即可。这种做法的好处是可以在不破坏原有双亲委派机制的前提下,实现自定义加载逻辑。相比之下,直接修改 loadClass() 方法容易破坏双亲委派模型,可能导致核心类被覆盖或加载异常,同时还需要重复实现委托父加载器的逻辑,增加代码复杂度。因此,在实际开发中,应在双亲委派机制框架内进行小范围改动,尽量保持原有加载结构的稳定性。
如何破坏双亲委派机制
在 Java 中,默认的双亲委派机制保证了父类加载器优先加载类,但可以通过特定方式绕过这一机制。首先,在自定义类加载器中,如果重写 loadClass() 方法并不调用父类加载器的 loadClass(),而直接加载目标类,就会跳过父加载器的优先加载过程,从而破坏双亲委派机制。其次,Java 提供了Thread.currentThread().setContextClassLoader() 方法,可以为当前线程设置“上下文类加载器”。一些框架或服务在加载实现类时,并不使用当前类本身的类加载器,而是通过线程上下文类加载器加载类。通过这种方式,即便父加载器中的代码,也可以加载子加载器中的类,从而实现对双亲委派机制的绕过和灵活的类加载策略。
jvm类缓存机制
Java 虚拟机内部具有类缓存机制,主要目的是提升性能,避免重复加载同一个类。当一个类第一次被加载时,JVM 会通过类加载器将其加载进内存,并缓存对应的 Class 对象。之后,如果再次请求加载同一类,JVM 会优先从类加载器的缓存中获取,而不是重新读取和解析字节码文件。
这种缓存机制实际上由 ClassLoader 内部维护的映射结构实现,通常是一个 Map,键为类的全限定名,值为对应的 Class 对象。为了确保类的唯一性和避免冲突,JVM 结合了双亲委派机制:每个类加载请求会先交由父加载器处理,只有在父加载器加载失败时,当前加载器才会尝试加载,从而进一步避免重复加载。
类加载后,其元数据信息(如字段、方法、常量池等)会被存储到 方法区(JDK 7 及以前)或 Metaspace(JDK 8 之后),这一部分存储区域是类缓存机制的重要组成部分。通常情况下,类缓存不会主动清理,但在一些特殊场景中(如热部署、插件系统等),JVM 可以卸载类。前提是:对应的类加载器以及它加载的所有类对象都不再被引用,JVM 才能回收这些类及其元数据。
jvm的类命名空间
在 JVM 中,类的命名空间由“类加载器 + 类的全限定名”共同决定。也就是说,即便类名相同,如果是由不同的类加载器加载,JVM 会认为它们是两个互不兼容的类,各自属于不同的命名空间。每个类加载器维护自己的命名空间,它可以访问自己加载的类以及父类加载器加载的类,但不能访问其他类加载器加载的类。
这种命名空间机制在插件系统、热部署、容器隔离等场景中非常重要。例如,在 Web 容器中,每个应用都会有独立的类加载器,即便不同应用中存在同名类,它们也互不干扰,实现了类的隔离和版本独立性。这保证了多版本共存和应用运行的安全性。
jvm判断两个对象相等的条件
在 JVM 中,判断两个类是否相等,不仅要求它们的全限定名(包名 + 类名)相同,还要求它们是由同一个类加载器实例加载的。也就是说,即便两个类对象来源于同一个 .class 文件,如果加载它们的类加载器不同,JVM 也会认为它们是两个完全不同的类。这一机制体现了类加载器命名空间的隔离特性,是模块隔离、插件系统、热部署等场景中实现类独立性和安全性的核心基础。
什么是jvm的运行时包
在 JVM 中,运行时包是指由同一个类加载器加载的、具有相同包名的类集合。判断两个类是否属于同一个运行时包,不仅要看它们的包名是否一致,还要看它们的定义类加载器是否相同。只有属于同一运行时包的类,才能互相访问包可见(默认访问级别)的类和类成员。
这一机制可以防止用户自定义类冒充核心类库的类,从而访问核心类库的包可见成员。例如,如果用户自己定义了一个类 java.lang.Spy 并由自定义类加载器加载,由于它与核心类库的 java.lang.* 类由不同加载器加载,因此它们属于不同的运行时包,java.lang.Spy 无法访问核心类库 java.lang 包中的包可见成员。这保证了包访问权限在类加载器隔离下的安全性。
jvm的运行时数据区域
jvm的内存区域主要分为方法区,堆,虚拟机栈,本地方法栈,程序计数器。
程序计数器:
一块较小的内存区域,是当前线程执行字节码的行号指示器。每个线程都有一个独立的程序计数器。是线程私有的。正是因为程序计数器的存在,多个线程来回切换的时候,原来的线程才能找到上次执行到哪里。程序计数器是 JVM 里线程私有的一个小内存区域,负责记录当前线程正在执行的字节码指令地址。它是线程切换时恢复执行的关键,保证多线程环境下每个线程能准确定位执行位置。由于其内存空间很小且简单,程序计数器不会出现内存溢出(OOM)情况。
虚拟机栈:
是描述java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等。每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。栈帧用来存储数据和部分过程结果的数据结构。也被用来处理动态连接,方法返回值,和异常分配。栈帧随着方法调用而创建,随着方法结束而销毁。
本地方法栈:
本地方法栈和虚拟机栈本质一样,不过是只存储本地方法,为本地方法服务。
堆内存:
创建的对象和数组保存在堆内存中,是被线程共享的一块内存区域。是垃圾回收的重要区域,是内存中最大的一块区域。存了类的静态变量和字符常量。
方法区:
用于存储被jvm加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。运行时常量池是方法区的一部分。class文件中除了有类的版本,字段,方法和接口描述等信息外,还有就是常量池,用于存放编译期生成的各种字面量和符号引用。
jvm堆的内部结构
在 JVM 中,堆内存是线程共享的对象和数组存储区域,也是垃圾回收的主要区域。为了提高管理和回收效率,堆通常被划分为几个不同的区域:年轻代(Young Generation)、年老代(Old/ Tenured Generation)和永久代/元空间(Permanent Generation/Metaspace)。
年轻代又进一步划分为 Eden 区 和两个 Survivor 区(S0、S1)。新创建的对象首先分配在 Eden 区,当 Eden 区满时,会触发 Minor GC,将存活对象移动到 Survivor 区。对象在 Survivor 区经过多次垃圾回收仍然存活后,会被晋升到年老代。年老代存放生命周期较长的对象,当年老代满时,会触发 Full GC(Major GC)。
在 JDK 7 及以前,方法区被实现为 永久代(PermGen),存储类元数据和静态变量;从 JDK 8 开始,永久代被 元空间取代,方法区数据存放在本地内存(Native Memory)中,不再受 Java 堆大小限制。堆的划分和分代回收策略,使得垃圾回收器能够针对不同生命周期的对象采用不同的回收算法,提高内存管理效率。
新生代
新生代(Young Generation)用于存放新创建的对象,一般占堆内存的约三分之一。由于新对象创建频繁,新生代会频繁触发 Minor GC 进行垃圾回收。新生代又分为 Eden 区、Survivor From 区 和 Survivor To 区。
- Eden 区:新对象的“出生地”,如果对象过大,会直接进入老年代。当 Eden 空间不足时,会触发 Minor GC。
- Survivor From 区:上一次 GC 中的幸存对象,作为本次 GC 的扫描对象。
- Survivor To 区:本次 GC 中幸存对象的存放区。
Minor GC 采用 复制算法,触发过程如下:
- 将 Eden 区和 Survivor From 区的存活对象复制到 Survivor To 区,并将对象年龄 +1;如果对象年龄达到晋升阈值(16)或 Survivor To 空间不足,则直接晋升到老年代。
- 清空 Eden 区和 Survivor From 区,释放空间。
- 交换 Survivor From 和 Survivor To 区,原 Survivor To 成为下一次 GC 的 Survivor From 区。
这种分代与复制策略能高效管理短生命周期对象,减少垃圾回收开销。
老年代
老年代(Old/Tenured Generation)主要存放应用程序中生命周期较长的对象,这些对象相对稳定,因此 Major GC 不会频繁执行。在触发 Major GC 前,通常会先进行一次 Minor GC,将新生代中晋升的对象移动到老年代。当老年代空间不足或者无法找到足够大的连续内存给新创建的大对象时,也会提前触发 Major GC 进行回收。
Major GC 采用 标记-清除(Mark-Sweep)算法:首先扫描老年代中所有对象并标记存活对象,然后回收未被标记的对象。由于需要扫描和回收整个老年代,Major GC 耗时较长,并可能产生内存碎片。为减少碎片和提升分配效率,通常会对空闲内存进行整理或合并。当老年代空间完全耗尽且无法分配新对象时,JVM 会抛出 OutOfMemoryError(OOM) 异常。
永久代
永久代(PermGen)是 JDK 7 及以前 JVM 中方法区的具体实现,用于存放类元数据(Class 信息)、常量池、静态变量和方法信息等数据。它与堆内存不同,不存放对象实例。永久代在程序运行期间不会像堆那样频繁进行垃圾回收,因此随着加载的类数量增加,永久代可能被填满,导致 OutOfMemoryError异常。
在 JDK 8 之后,永久代被 元空间(Metaspace) 取代,避免了固定大小导致的内存溢出问题,同时仍然承担存储类元数据和静态变量的功能。
元空间
元空间(Metaspace)是 JDK 8 之后取代永久代的 JVM 内存区域,用于存放类的元数据。与永久代不同,元空间不在虚拟机堆内,而是使用 本地内存(Native Memory)。默认情况下,元空间大小仅受系统可用内存限制,不再由 MaxPermSize 控制。类的元数据存放在本地内存中,而字符串池和类的静态变量仍存放在 Java 堆中。
为什么设置eden 、survivor from 和 to 区域
Eden 区和 Survivor 区的设置是为了提高新生代垃圾回收效率,减少对象直接晋升到老年代,从而降低 Full GC 的发生频率。新创建的对象首先分配在 Eden 区,如果没有 Survivor 区,Minor GC 时存活对象会直接进入老年代,容易填满老年代并触发耗时的 Full GC。
Survivor From 和 Survivor To 区的存在有两个作用:
- 减少晋升到老年代的对象:只有经历多次 Minor GC 仍存活的对象才会晋升,延长对象在新生代的生命周期。
- 解决内存碎片问题:Minor GC 时,Eden 和 Survivor From 中的存活对象会复制到 Survivor To,保证对象在 Survivor 区连续存放,Eden 和 Survivor From 清空后,下一次 GC 交换 From 和 To 区继续复制。
这种分区和复制策略有效提高了内存利用率和垃圾回收效率。
触发 Full GC 的几种常见情况
- 老年代空间不足:新生代对象晋升或大对象直接进入老年代,如果老年代空间不足,就会触发 Full GC。频繁创建大对象或大量对象晋升都可能导致这种情况。
- 元空间(或永久代)空间不足:加载大量类或动态生成类(如反射、CGLIB、动态代理)时,元空间/永久代可能溢出,触发 Full GC 或直接抛出 OutOfMemoryError: Metaspace。
- 显式调用 System.gc():调用 System.gc() 或 Runtime.getRuntime().gc() 会建议 JVM 执行 Full GC,许多 JVM 实现会响应该请求,除非显式禁用。
- 对象晋升失败或分配失败:在 Minor GC 时,如果存活对象无法晋升到老年代(老年代空间不足)或分配担保失败,也会触发 Full GC。
- CMS 并发失败:CMS 并发回收老年代时,如果未能及时腾出空间,会触发一次 Stop-the-World 的 Full GC 作为兜底。
- 大对象分配找不到连续内存块:即使老年代总空间够用,但由于内存碎片找不到连续大块,也可能触发 Full GC 进行整理。
Major GC 与 Full GC 的区别
- Major GC:通常只回收 老年代(Tenured Generation),不包括新生代和元空间。主要在老年代空间不足时触发。Major GC 会导致 Stop-the-World(STW),但相比 Full GC 开销较小,停顿时间相对短。
- Full GC:回收 整个 JVM 内存区域,包括新生代、老年代和元空间(永久代)。Full GC 触发频率更低,一般在老年代回收不足、元空间不足、显式调用 System.gc() 或 CMS 并发回收失败时触发。Full GC 停顿时间长,性能开销大。
如何减少fullgc
- 避免老年代频繁溢出:减少不必要的大对象或长生命周期对象,优化业务逻辑释放内存,延迟对象晋升(调整 MaxTenuringThreshold),避免老年代快速填满。
- 禁止显式调用 System.gc():通过 -XX:+DisableExplicitGC 禁用显式 GC,防止代码或第三方库滥用。
- 合理设置堆内存大小:根据业务规模和服务器内存配置合适的堆空间(-Xms 与 -Xmx),减少动态扩容开销。
- 优化垃圾收集器策略:选择合适的 GC,例如 G1 GC、低延迟 GC(ZGC、Shenandoah)来降低 Full GC 发生率,避免使用不适合的老旧收集器。
- 防止元空间溢出:增加元空间大小(-XX:MaxMetaspaceSize)、避免频繁动态生成类或类加载器泄漏,确保类加载器可回收。
- 防止内存泄漏:定期使用内存分析工具检查对象引用链,注意缓存、静态集合、线程本地变量等容易泄漏的场景,合理使用弱引用或软引用。
- 降低老年代碎片化风险:在 CMS 等收集器中可启用内存整理参数(如 XX:+UseCMSCompactAtFullCollection),或使用具备整理能力的 GC(如 G1)以减少碎片导致的 Full GC。
jvm判断对象是否可被回收
JVM 判断对象是否可被回收的两种主要方法:
- 引用计数法:每个对象维护一个引用计数器,当被引用时计数 +1,引用失效时计数 -1。当计数为 0 时,对象可被回收。缺点是无法处理循环引用,例如两个对象互相引用,即使没有外部引用,计数也不为 0,导致无法回收。
- 可达性分析法:JVM 从一组 GC Roots(绝对不会被回收的起点,如线程栈中的引用变量、静态变量、常量池、本地方法栈引用等)出发,沿引用链查找所有可访问对象。只有无法通过 GC Roots 访问的对象才会被回收。可达性分析法可以有效解决循环引用问题,是现代 JVM 垃圾回收判断对象存活的主要机制。
jvm常见垃圾回收器
一、新生代垃圾回收器(Minor GC)
-
Serial(串行收集器)
-
- 特征:单线程回收,Stop-The-World;简单高效,但停顿时间长。
- 算法:复制算法(复制 Eden 区存活对象到 Survivor 区)。
- 适用场景:客户端应用、小内存或单核 CPU 环境,对响应速度要求不高。
-
ParNew
-
- 特征:Serial 的多线程版本,Stop-The-World 仍存在;利用多核 CPU 提高效率。
- 算法:复制算法。
- 适用场景:配合 CMS 收集器使用,适合多核服务器。
-
Parallel Scavenge(吞吐量优先)
-
- 特征:多线程收集,关注整体系统吞吐量而非每次停顿时间。
- 算法:复制算法。
- 适用场景:后台运算或批处理系统,需要高吞吐量。
二、老年代垃圾回收器(Major GC / Full GC)
-
Serial Old
-
- 特征:单线程标记-整理算法,Stop-The-World;效率低。
- 适用场景:Client 模式或 CMS 回退方案。
-
Parallel Old
-
- 特征:多线程标记-整理算法,追求吞吐量。
- 适用场景:服务端高吞吐量环境,多核 CPU。
-
CMS(Concurrent Mark Sweep)
-
- 特征:并发标记清除,降低停顿时间;分阶段(初始标记、并发标记、重新标记、并发清除)。分阶段就是把垃圾回收分成 部分停顿 + 大部分并发执行 的几个步骤,从而降低程序的停顿时间。
- 缺点:易产生内存碎片,可能触发 Concurrent Mode Failure。
- 适用场景:对响应时间敏感的 Web 或在线服务。
三、全区域垃圾回收器(同时管理新生代和老年代)
-
G1(Garbage First)
-
- 特征:G1 通过将堆划分为多个 Region 并按收益优先回收,实现高并发、低停顿的垃圾回收。并行、并发、低停顿;可预测停顿时间。
- 适用场景:大堆内存、延迟敏感的服务端程序。
-
ZGC(Java 11+)
-
- 特征:极低延迟
- 适用场景:延迟敏感、大内存的在线系统(交易平台、游戏服务)。
-
Shenandoah(Java 12+)
-
- 特征:低停顿、并发标记和压缩;适用于 JDK 8/11 OpenJDK。
- 适用场景:延迟敏感系统,堆内存大且要求低停顿。
jvm的垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
- 原理:
-
- 标记阶段:从 GC Roots 出发,沿引用链找到所有可达对象,并标记它们为“活着”。
- 清除阶段:遍历整个堆,把没有被标记的对象(即不可达对象)清除掉,释放内存。
- 优缺点:
- 简单易实现
- 容易产生内存碎片,因为存活对象不连续
- 清理效率低
- 应用:老年代早期收集器,如 Serial Old、CMS 的清除阶段
2. 标记-复制算法(Copying)
- 原理:
-
- 将内存划分为两块等大的区域(如 S0、S1 或 Eden + Survivor)。
- 只使用其中一块存储对象。
- 回收时,把存活对象复制到另一块区域,并一次性清空原区域。
- 优缺点:
- 不产生碎片
- 顺序写内存,速度快
- 空间利用率低,通常浪费一半内存
- 应用:新生代,如 Eden + Survivor 区;收集器:Serial、ParNew
3. 标记-整理算法(Mark-Compact)
- 原理:
-
- 标记所有存活对象(和标记-清除类似)。
- 将存活对象向堆的一端移动,使内存连续。
- 清理边界之外的空间。
- 优缺点:
- 内存紧凑,无碎片
- 移动对象需要停顿(Stop-the-World),比复制算法慢
- 应用:老年代,如 Serial Old、Parallel Old
4. 分代收集算法(Generational Collection)
- 原理:
- 根据对象生命周期长短划分堆:
- 新生代:对象创建多、存活短 → 复制算法
- 老年代:对象存活久 → 标记-整理或标记-清除
- 根据对象生命周期长短划分堆:
- 优点:
- 针对性回收,提高效率
- 避免“一刀切”回收整个堆
- 应用:几乎所有现代 JVM 收集器(ParNew + CMS、Parallel、G1 等)
5. 增量收集算法(Incremental GC)
- 原理:
- 把垃圾回收任务拆分成多个小步骤,每次只处理部分对象
- 减少单次停顿时间
- 应用:响应时间敏感的系统,早期 Train GC
6. 并发标记-清除(CMS)
- 原理:
-
- 初始标记(Stop-the-World):暂停应用线程,标记 GC Roots 直接引用的对象,时间短。
-
- 并发标记(Concurrent Mark):应用线程继续运行,同时标记 GC Roots 间接可达的对象。
-
- 重新标记(Stop-the-World):再次短暂停顿,标记并发阶段新增或修改的对象,保证标记完整。
-
- 并发清除(Concurrent Sweep):应用线程继续运行,同时清理不可达对象,释放内存。
- 优点:低停顿,适合延迟敏感系统
- 缺点:容易产生碎片,需要更多 CPU
- 应用:CMS 收集器
7. G1 垃圾回收(Region-based 混合算法)
- 原理:
-
- 将堆划分为多个大小相等的 Region(新生代 + 老年代混合)
- 分析每个 Region 的垃圾量,优先回收“收益最大”的区域
- 结合复制算法(新生代)和标记-整理算法(老年代)
- 并行、并发执行,控制停顿时间
- 优点:低延迟、可预测停顿、大堆友好
- 应用:G1 收集器,适合大内存、延迟敏感的服务端
JVM 的组成部分及作用
- 类加载器子系统:负责将字节码文件加载到 JVM 中,并完成验证、准备、解析和初始化,确保类能正确执行。
- 运行时数据区:存储程序运行期间的数据,包括:
-
- 程序计数器:记录当前线程执行的字节码地址。
- 虚拟机栈:存储方法调用的栈帧和局部变量。
- 本地方法栈:支持本地方法调用。
- 堆:存放对象实例,是垃圾回收的主要区域。
- 方法区:存放类信息、常量、静态变量等。
- 执行引擎:负责读取字节码并执行,包括解释器(逐条执行)、JIT 编译器(优化热点代码)、垃圾回收器(管理堆内存)。
- 本地接口:支持 Java 调用 C/C++ 等本地方法,实现与底层系统的交互。
- 运行时库:提供 JVM 执行所需的标准类库支持,如 java.lang、java.util 等。
方法区和永久代和元空间有什么关系
方法区是 JVM 规范定义的一块运行时内存区域,用来存储类的结构信息,比如常量池、静态变量和类元数据等。
在 HotSpot 虚拟机中,JDK 1.7 及以前通过“永久代”来实现方法区,但永久代存在大小限制、不易调优等问题,
因此从 JDK 1.8 开始,用“元空间”替代了永久代。元空间使用本地内存,容量更大、更灵活,有效减少了 OOM 问题的发生。
为什么替换永久代为元空间?
- 永久代大小固定(默认较小,容易 OOM)
- 类元数据容易随应用动态变化,永久代限制多
- 不同 JVM 实现间不一致,造成兼容性问题
- 所以 JDK8 后彻底移除永久代,引入更灵活的元空间
jvm中堆和栈的区别
- 所属区域
-
- 堆:属于 JVM 运行时数据区的一部分,所有线程共享。
- 栈:每个线程私有,存放在线程的 JVM 运行时栈中。
- 内存分配
-
- 堆:所有线程共享,由垃圾回收器自动管理。
- 栈:线程独立,方法调用时自动入栈,方法结束后出栈释放。
- 主要作用
-
- 堆:存储对象实例和数组。
- 栈:存储方法调用的栈帧,包括局部变量、操作数栈和返回地址。
- 生命周期
-
- 堆:与 JVM 生命周期一致。
- 栈:与线程生命周期一致。
- 访问速度
-
- 堆:相对较慢。
- 栈:较快。
- 线程安全
-
- 堆:非线程安全,需要同步控制。
- 栈:线程私有,天然线程安全。
- 溢出情况
-
- 堆:可能溢出,抛出 OutOfMemoryError: Java heap space。
- 栈:可能溢出,抛出 StackOverflowError。
总结:堆适合存放长期存在的对象,由 GC 管理;栈适合方法调用和局部变量,生命周期短且访问速度快。
java创建对象的几种方式
方式编号 |
创建方式 |
简要说明 |
1 |
new 关键字 |
最常见方式,编译时类型明确 |
2 |
Class.forName() + newInstance() (已过时) |
通过反射,根据类名字符串创建对象(JDK 9 开始不推荐) |
3 |
Class.getDeclaredConstructor().newInstance() |
推荐的反射方式,JDK 9+ 使用 |
4 |
使用 clone() 方法 |
创建一个当前对象的副本,需实现 Cloneable 接口 |
5 |
使用 ObjectInputStream 反序列化 |
从序列化数据中恢复对象 |
创建对象的流程
Java 对象的创建流程可以简洁总结为五步:
- 类加载检查:JVM 执行 new 时,先检查常量池对应的类是否已加载,未加载则先进行类加载。
- 内存分配:在堆中为对象分配内存。堆规整时用 指针碰撞,不规整时用 空闲列表。并发分配可通过 CAS 或 TLAB 完成。
- 内存初始化:为对象分配的内存设置默认值。
- 对象元信息设置:初始化对象头,包括类型指针、哈希码、锁状态等。
- 执行构造方法:调用对象的 方法完成进一步初始化。
总结:整个过程是 类加载 → 内存分配 → 初始化 → 元信息设置 → 构造方法执行,涉及堆管理和线程安全机制。
Jvm内存分配的指针碰撞与空闲列表是什么?
指针碰撞:假设为Java堆中内存是绝对完整的,所有用过的内存放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,所分配的内存就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。
空闲列表:假设Java堆中的内存并不是完整的,已使用的内存和空闲内存都混在一起了,这时虚拟机需要维护一个列表,用来记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。
选择哪种分配方式由Java堆是否完整决定,Java堆是否完整由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等垃圾收集器时系统采用的是指针碰撞,在使用CMS等基于标记擦除算法的收集器时,采用的是空闲列表。
Jvm创建对象内存分配时,如何进行并发处理
在 JVM 中创建对象时,为了保证多线程环境下的内存分配安全,需要处理 并发分配问题。主要方法有两种:
- CAS(Compare-And-Swap)同步
-
- 原理:使用原子操作更新堆内存指针。
- 流程:线程尝试将指针从当前位置移动到新位置,如果在移动前指针被其他线程修改,则操作失败并重试。
- 优点:不使用锁,避免阻塞;保证线程安全。
- 缺点:高并发时可能频繁失败重试,开销较大。
- 本地线程分配缓冲(TLAB, Thread Local Allocation Buffer)
-
- 原理:为每个线程在堆中预先分配一小块私有内存,线程在自己的 TLAB 中直接分配对象。
- 优点:线程无需竞争,分配速度快,天然线程安全。
- 限制:当 TLAB 空间不足或对象过大时,才需要回退到全局堆,使用 CAS 进行分配。
总结:
- TLAB 优先,大部分对象分配都在线程私有缓冲完成,无竞争;
- CAS 回退,处理大对象或 TLAB 用尽情况,保证全局堆分配的线程安全。
jvm对象的访问方式
1. 句柄访问方式(Handle Access)
在这种方式下,每个对象在堆中都有一个句柄,句柄中存储了对象实际地址和类型信息的地址。对象引用指向句柄地址,访问对象时需要经过两次跳转:先从引用找到句柄,再从句柄找到对象数据和类型信息。
- 优点:对象在堆中移动时无需修改引用,只需更新句柄的对象地址,适合频繁移动对象的 GC(如 CMS)。
- 缺点:访问时多一次间接跳转,性能略低。
2. 直接指针访问方式(Direct Pointer Access)
对象引用直接保存对象在堆中的地址,访问对象时通过引用即可直接访问对象数据和类型信息,只需一次跳转。
- 优点:访问速度快,性能高,适合性能敏感场景。
- 缺点:对象移动时引用必须同步更新,否则可能造成野指针错误,因此依赖压缩堆或整理内存的 GC(如 Serial、ParNew)。
jvm类何时可以卸载
在 JVM 中,类只有在它的类加载器被回收的前提下才可能被卸载。具体来说,需要满足三个条件:1)类是由自定义的 ClassLoader 加载的;2)该 ClassLoader 不再被引用;3)该类本身及其实例、方法栈、静态变量等都不再被引用。只有在满足这些条件时,Full GC 才可能回收这个类。常见的应用场景是热部署或插件式架构中的类卸载。
jvm的直接内存
JVM 的直接内存(Direct Memory)是指 Java 程序绕过堆内存,直接向操作系统申请的一块内存区域。它不受 JVM 垃圾回收管理,但总量有限,由 JVM 参数 -XX:MaxDirectMemorySize 控制。直接内存主要用于高性能 IO(如 NIO、Netty),因为数据读写可以直接在操作系统内存中进行,减少了堆内存与本地内存之间的复制,提高效率。如果直接内存分配过多,超过限制,就会抛出 OutOfMemoryError: Direct buffer memory。
jvm三色标记法
JVM 的三色标记法是一种垃圾回收可达性分析算法。它将对象分为白色(未访问)、灰色(已访问但引用未完全扫描)、黑色(自身及引用已处理),通过从 GC Roots 出发逐步扫描灰色对象,将不可达的白色对象回收。该方法保证并发 GC 时不漏标、不误回收,是现代垃圾回收器(如 CMS、G1)实现并发回收的基础。
jvm三色标记法的缺点
- 浮动垃圾(Floating Garbage)
在并发标记过程中,用户线程可能产生新的垃圾对象,这些对象不会被本次 GC 回收,只能等待下一次 GC 清理。浮动垃圾不会影响程序正确性,只是回收延迟,是并发标记的正常现象。
- 漏标问题(误回收)
并发标记时,如果用户线程修改对象引用关系,可能导致某些仍然可达的对象未被标记,从而被错误回收。漏标会引发严重问题,如空指针异常或程序崩溃,因此必须通过写屏障等机制加以防范。
增量更新
增量更新(Incremental Update)是一种写屏障策略,主要用于 CMS GC 解决并发标记阶段可能出现的漏标问题。
核心思想
在并发标记时,如果一个已标记为黑色的对象新增了指向白色对象的引用,该白色对象可能被漏掉。增量更新会在这种情况下,把新引用的白色对象重新加入标记队列(标记为灰色),确保它会被扫描和回收正确处理。
示例流程
- GC 并发标记中,对象 A 已标记为黑色;
- 用户线程执行 A.setField(C),新增了对白色对象 C 的引用;
- 写屏障记录该操作;
- GC 将 C 标记为灰色,加入标记队列,确保 C 不被误回收。
特点
- 监控对象:主要关注黑色对象新增引用;
- 适用场景:CMS 等增量标记为主的 GC;
- 优点:实现逻辑简单,能有效避免漏标;
- 缺点:可能重复标记或扫描对象,增加一些额外开销。
原始快照
SATB(Snapshot-At-The-Beginning,原始快照)是一种现代写屏障策略,主要用于 G1、Shenandoah、ZGC 等并发 GC,用于解决并发标记时的漏标问题。
核心思想
SATB 假设 GC 在并发标记阶段处理的是标记开始时的对象引用图快照。在标记期间,如果程序删除了某个对象的引用(将字段置 null 或改向其他对象),该对象可能还未被标记。SATB 的做法是:在引用被删除时,将原本被引用的对象记录下来,并加入标记队列,确保其不会被误回收。
示例流程
- GC 并发标记开始,A → B 存在;
- 用户线程执行 A.setField(null),断开 A → B 的引用;
- 写屏障记录被删除引用的对象 B;
- GC 将 B 加入标记队列,保证其仍会被标记,不被误回收。
特点
- 主要监控对象:引用被删除的对象;
- 保持标记阶段的引用快照一致性;
- 适合现代并发 GC;
- 实现比增量更新复杂,但标记更精确。
jvm的强引用弱引用软引用虚引用
在 Java 中,对象的可达性决定其是否可以被垃圾回收。为了细粒度控制对象生命周期,JVM 提供了四种引用类型,强度依次递减,回收条件依次宽松:
- 强引用(Strong Reference)
默认引用方式,例如 Object obj = new Object();。只要存在强引用,垃圾回收器绝不会回收对象,即使内存紧张。适用于大多数普通对象。
- 软引用(Soft Reference)
用于描述可有可无的对象,通常用于内存敏感的缓存。只有在内存不足时才会被回收。适用于图片缓存、内存缓存等,创建方式:SoftReference ref = new SoftReference<>(obj);
- 弱引用(Weak Reference)
描述非必须对象,一旦发生 GC,无论内存是否充足都会被回收。适用于 WeakHashMap、ThreadLocal 的 key 等,创建方式:WeakReference ref = new WeakReference<>(obj);
- 虚引用(Phantom Reference)
虚引用是 Java 中最弱的一种引用,它不会影响对象的生命周期。也就是说,无论对象有多少虚引用,它随时都可能被垃圾回收。虚引用的主要作用不是防止对象被回收,而是在对象被回收前得到一个通知,方便开发者在对象销毁前做一些清理工作,比如释放底层资源(文件句柄、直接内存等)。
java的osgi是什么
OSGi 的核心就是把一个大型应用拆解成多个“小模块”,每个模块叫 Bundle。
- 模块独立:每个 Bundle 都有自己的类加载器和私有类空间,不会直接影响其他模块的类。
- 模块间联系:模块之间不直接依赖类,而是通过 服务注册表(Service Registry) 来交互。一个模块可以 注册服务,其他模块通过查询服务来使用功能。
- 动态特性:模块可以在应用运行时 动态加载、卸载或更新,不会影响整个系统的稳定性。
简单理解就是:模块独立管理,但通过服务接口相互合作,实现灵活的组合和解耦。
什么情况下会发生栈溢出
- 递归调用没有终止条件:方法无限制地调用自身,导致栈帧不断叠加。
- 递归调用深度过大:即使有终止条件,如果递归深度超过栈容量,也会导致溢出。
- 方法调用层级过多:大量连续的方法调用(即使非递归)也可能导致栈空间耗尽。
- 线程栈设置过小:通过 JVM 参数(如 -Xss128k)设置过小的栈空间会加剧溢出风险。
java会存在内存泄漏吗
即使 Java 有自动垃圾回收机制(GC),仍可能发生内存泄漏。内存泄漏是指:程序中某些不再使用的对象因为仍然被其他对象引用,导致无法被垃圾回收,从而一直占用内存空间。
- 静态集合类的滥用:如 HashMap、List 等被定义为静态变量,未及时清理元素;
- ThreadLocal 使用不当:未调用 remove() 方法清除线程局部变量;
- 缓存未清理:程序中自定义缓存没有淘汰策略,导致对象长期存在;
- 对象间引用关系复杂:如容器类循环引用,导致不再使用的对象依然被持有。