JVM
类加载器的分类¶
- 类加载器是 JVM 中负责加载类的组件。它将类的字节码从文件系统或网络等位置加载到 JVM 中,并将其解析成 JVM 能够理解的运行时数据结构。
- 主要有以下几种类型的类加载器:
- 启动类加载器(Bootstrap ClassLoader):是最顶层的类加载器,由 C++ 实现,负责加载 Java 核心类库,如
java.lang
包下的类。 - 扩展类加载器(Extension ClassLoader):负责加载 Java 的扩展类库,如
jre/lib/ext
目录下的类。 - 应用程序类加载器(AppClassLoader):也称为系统类加载器,负责加载应用程序的类路径下的类,是大多数 Java 应用中自定义类的默认类加载器。
- 启动类加载器(Bootstrap ClassLoader):是最顶层的类加载器,由 C++ 实现,负责加载 Java 核心类库,如
启动类加载器(Bootstrap Class Loader)¶
- 它是 JVM 中最顶层的类加载器,由 C++ 语言实现,负责加载 Java 核心类库,如
java.lang
、java.util
、java.io
等。这些类库位于 JDK 的jre/lib
目录下,是 Java 运行时必不可少的基础类。 - 启动类加载器比较特殊,它没有父类加载器,因为它是最顶层的类加载器。
扩展类加载器(Extension Class Loader)¶
- 扩展类加载器由 Java 语言实现,父类加载器是启动类加载器。它负责加载 JDK 扩展目录
jre/lib/ext
中的类库,以及java.ext.dirs
系统属性指定的目录中的类。 - 可以通过扩展类加载器来加载一些通用的、可扩展的类库,以便在多个应用程序中共享使用。
应用程序类加载器(Application Class Loader)¶
- 也称为系统类加载器,同样由 Java 语言实现,父类加载器是扩展类加载器。它是 ClassLoader 类中的
getSystemClassLoader()
方法返回的类加载器,负责加载应用程序的类路径(classpath)下的所有类。 - 应用程序中的自定义类和第三方类库通常都是由应用程序类加载器来加载的。
自定义类加载器(Custom Class Loader)¶
- 开发人员可以根据自己的需求自定义类加载器,通过继承
java.lang.ClassLoader
类来实现。自定义类加载器可以实现一些特殊的类加载逻辑,比如从网络、数据库等非传统的存储位置加载类,或者对类进行加密、解密等特殊处理。 - 自定义类加载器的父类加载器通常是应用程序类加载器,当然也可以在创建自定义类加载器时指定其他类加载器作为父类加载器。
不同的类加载器负责不同范围的类加载任务,它们之间形成了一种层次结构,这种结构被称为类加载器的双亲委派模型,保证了类加载的安全性和有序性。
什么是双亲委派模型¶
- 定义:双亲委派模型是一种类加载器的工作模式。当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上,直到顶层的启动类加载器。只有当父类加载器无法完成加载任务时,子类加载器才会尝试自己去加载。
- 作用:保证了 Java 核心类库的安全性和唯一性,避免了不同类加载器对同一核心类的重复加载,也防止了用户自定义的类覆盖核心类库中的类。
如何自己实现一个 String,并让 JVM 加载这个 String¶
- 首先,不能直接实现一个完全等同于 Java 标准库中的
String
类,因为String
类在 Java 中是final
类型,不能被继承和重写。 - 若要自定义一个类似
String
功能的类,比如命名为MyString
,可以通过实现Serializable
接口、Comparable
接口等,来实现字符串的基本功能,如存储字符序列、实现比较、序列化等操作。 - 让 JVM 加载自定义的
MyString
类,需要通过类加载器。可以自定义一个类加载器,继承自ClassLoader
类,重写findClass
方法,在该方法中通过读取字节码文件等方式将MyString
类的字节码加载到 JVM 中。
如何破坏双亲委派机制¶
- 自定义类加载器,重写
loadClass
方法,在方法中不调用父类加载器的loadClass
方法,而是直接自己加载类。例如,在一些框架中,如 Tomcat,为了实现不同 Web 应用之间的类隔离,会自定义类加载器,打破双亲委派机制,使得每个 Web 应用可以有自己独立的类加载体系,避免类冲突。
JVM 内存结构¶
- 线程共享
- Java 堆:是 JVM 所管理的内存中最大的一块,被所有线程共享。堆是存放对象实例以及数组的地方,几乎所有的对象实例和数组都在堆上分配内存。
- 方法区:被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 线程独占
- 程序计数器:是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储。
- Java 虚拟机栈:线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与 Java 虚拟机栈类似,只不过它是为 Native 方法服务的。
- 本地内存
- 直接内存(Direct Memory):由 NIO(New Input/Output)直接分配,不受 JVM 堆的管理,通常用于高性能数据传输,如缓冲区(Buffer)。这个内存结构保证了 JVM 在执行 Java 代码时能够高效管理对象、执行方法调用,并支持多线程并发。
堆内存中新生代和老年代的区别¶
- 新生代:
- 新生的对象首先会分配在新生代。
- 新生代分为 Eden 区和两个 Survivor 区(一般称为 From 区和 To 区)。
- 新生代的特点是对象朝生夕灭,大部分对象的生命周期很短,经过几次垃圾回收后,仍然存活的对象会被晋升到老年代。
- 老年代:
- 存储经过多次垃圾回收后仍然存活的对象。
- 老年代的空间一般比新生代大,垃圾回收的频率相对新生代要低。
内存泄露和垃圾回收¶
- 内存泄漏:程序中不再使用的对象因被错误引用而未释放,导致内存浪费。
- 垃圾回收的概念:JVM 会自动管理内存,通过垃圾回收机制来回收不再被使用的对象所占用的内存空间,以避免内存泄漏和内存溢出,提高内存的利用率。
- 垃圾回收的触发条件:当新生代或老年代的内存空间不足时,会触发垃圾回收。此外,也可以通过调用
System.gc()
方法来建议 JVM 进行垃圾回收,但这只是一个建议,JVM 不一定会立即执行。
常见垃圾回收算法¶
- 标记 - 清除:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。缺点是会产生内存碎片。
- 标记 - 整理:先标记出所有需要回收的对象,然后将存活的对象向一端移动,最后清理掉边界以外的内存。解决了标记 - 清除算法的内存碎片问题。
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完后,将存活的对象复制到另一块空的内存中,然后将原来的内存空间一次性清理掉。适用于新生代,因为新生代中对象存活率低。
- 分代收集:根据对象的生命周期不同,将堆内存分为新生代和老年代,针对不同代采用不同的垃圾回收算法。新生代使用复制算法,老年代使用标记 - 压缩或标记 - 清除算法。
常见的垃圾回收器¶
- CMS(Concurrent Mark Sweep,并发标记清除)
- 特点:以低延迟为目标,大部分工作与用户线程并发执行,STW 时间短。
- 原理:分 4 步:初始标记(STW)→并发标记→重新标记(STW)→并发清除,老年代采用标记 - 清除算法(会产生内存碎片)。
- 适用场景:对响应时间敏感的服务(如 Web 应用),JVM 参数:-XX:+UseConcMarkSweepGC。
- 缺点:CPU 消耗高(需多线程并发)、内存碎片多,JDK9 后逐步废弃。
- G1(Garbage-First)
- 特点:兼顾吞吐量和延迟,面向大内存(堆内存≥4GB),将堆划分为多个大小相等的 Region(区域)。
- 原理:基于 “标记 - 整理” 算法,优先回收垃圾最多的 Region(Garbage-First),通过 Remembered Set 跟踪跨 Region 引用,STW 时间可通过参数控制(-XX:MaxGCPauseMillis)。
- 适用场景:大内存应用(如服务器),JDK9 后成为默认收集器,参数:-XX:+UseG1GC。
- Serial GC(串行收集器)
- 特点:单线程执行 GC,收集时暂停所有用户线程(STW),简单高效但停顿时间长。
- 原理:新生代用复制算法,老年代用标记 - 整理算法,均为单线程操作。
- 适用场景:内存较小的客户端应用(如桌面程序),JVM 参数:-XX:+UseSerialGC。
- Parallel GC(并行收集器)
- 特点:多线程执行 GC,注重吞吐量(用户代码运行时间 /(用户时间 + GC 时间)),仍有 STW。
- 原理:新生代并行复制,老年代并行标记 - 整理,默认线程数与 CPU 核心数相关。
- 适用场景:后台计算等对吞吐量敏感、可接受一定停顿的服务,JVM 参数:-XX:+UseParallelGC(默认新生代)、-XX:+UseParallelOldGC(老年代并行)。
- ZGC(Z Garbage Collector)
- 特点:超低延迟(STW 时间≤10ms),支持 TB 级堆内存,并发执行几乎所有阶段。
- 原理:基于 Region 划分,使用着色指针(Colored Pointers)和读屏障(Load Barrier)跟踪对象引用,避免传统的 Remembered Set,堆大小可动态调整。
- 适用场景:超大内存、低延迟要求的服务(如金融交易),JDK11 引入,参数:-XX:+UseZGC。
低延迟优先:ZGC、Shenandoah、CMS(逐步淘汰)。 吞吐量优先:Parallel GC。 平衡兼顾:G1(默认推荐)。 轻量客户端:Serial GC。
JDK 8 和 JDK 17 中 JVM 默认 GC 的选择与操作系统、CPU 架构及堆内存大小相关,核心规则如下:¶
- JDK 8 默认 GC
- 服务器模式(多 CPU、大内存,默认模式): 采用 Parallel GC(新生代 Parallel Scavenge + 老年代 Parallel Old),注重吞吐量,适合后台计算等对吞吐量敏感的场景。
- 客户端模式(单 CPU、小内存,如 32 位系统): 采用 Serial GC,单线程回收,适合桌面应用等轻量场景。
- JDK 17 默认 GC
- 默认优先选择 G1 GC: 无论服务器还是客户端模式,G1 成为默认 GC(替代 JDK 8 的 Parallel GC),它兼顾吞吐量和延迟,支持大内存(≥4GB),通过动态调整回收区域优化停顿时间。
- 特殊场景降级: 若堆内存极小(如 ≤1792MB),可能自动切换为 Serial GC(资源受限场景下更高效)。
总结:JDK 8 以 Parallel GC 为主,JDK 17 以 G1 为主,均会根据硬件环境动态适配,可通过 -XX:+PrintCommandLineFlags
命令查看当前 JVM 实际使用的 GC 类型。