1 jvm主要模块
方法区 | 存储了由类加载器从.class文件中解析的类的元数据(类型信息、域信息、方法信息)及运行时常量池(引用符号及字面量)。 所有线程共享;内存不要求连续,可扩展,可能发生垃圾回收(如卸载类)。 |
堆 | 存储内容:1)所有new的对象及数组。2)静态变量及字符串常量。 一个jvm实例只有一个堆,所有线程共享,包含了新生代和老生代。 |
栈区 | 线程私有。在线程创建时创建,线程销毁时销毁。 记录了局部变量引用、操作数栈等。每调用一个方法生成一个栈帧,后进先出。 |
PC寄存器 | 指令计数器,存储指向下一条指令的地址。 |
本地方法栈 | 和栈区功能一样,但执行的是本地的方法。 |
表 jvm的主要模块
1.1 常量池
经过编译后生成的.class文件,包含了元数据(类型信息、域信息及方法信息的原始表示)及类文件常量池(符号引用及字面量)。
而方法区存储的是运行时,当类被加载后解析的元数据及常量池(运行时常量池)。
1.1.1 永久代到元空间的演变
jdk1.8之前方法区也成为永久代。存储于堆中。jdk1.8及之后,被元空间取代。元空间不在jvm的内存中,而在本地内存。
元空间替代永久代,有以下优势:
- 内存空间不受jvm的限制,而是由物理内存决定。用户可以配置元空间大小。
- 类的元数据在类卸载时由GC回收,效率更高效。
2 类加载器
负责将.class 文件加载到JVM内存,并生成对应的Class<?>对象。职责包括:
- 加载字节码:从文件系统、网络或其他来源读取.class文件。
- 定义类结构:将字节码转换成JVM内部的类元数据。
- 维护类隔离:通过不同类加载器实现类的作用域隔离。
2.1 类加载器层级
启动类加载器 | Bootstrap ClassLoader,加载JAVA_HOME\lib 目录下的核心类库。 有JVM自身实现,无法在代码中直接引用。 |
扩展类加载器 | Extension ClassLoader,加载JAVA_HOME\lib\ext目录下的扩展类库。 |
应用程序类加载器 | Application ClassLoader,加载classpath目录下的用户类(项目代码、第三方库) |
图 系统自带的类加载器
图 类加载器的层级
2.2 双亲委派模型
类加载器采用了“双亲委派模型”,即加载类时,先委托给父类加载器加载,如果加载不到,则由自己加载。这里的父类,并不是指具有继承关系,而是通过合成复用,来设置父类加载器。
要求,除顶层的启动类加载器外,其他的加载器都应有自己的父加载器。
在实现自定义类加载器时,需要继承抽象类ClassLoader,其伪代码如下:
public abstract class ClassLoader {
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this.parent = parent;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先确定这个类是否被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name);
} else { // 如果父加载器不存在,则将启动类加载器作为父加载器
c = findBootstrapClass(name);
}
if (c == null) {
c = findClass(name); // 自定义实现加载类
}
}
return c;
}
/**
* 确定这个类是否已加载
*/
private Class<?> findLoadedClass(String name) {
return null;
}
/**
* 通过启动类加载器查找
*/
private native Class<?> findBootstrapClass(String name);
/**
* 自定义实现加载类。在实现自定义加载器时,推荐重写这个方法,而不要动loadClass方法
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
2.2.1 优缺点
优点:
- 保护核心类库安全,防止用户自定义同名类覆盖核心类,避免安全漏洞(如恶意代码注入)。
- 保证类的全局唯一性,无论通过哪个类加载器加载,最终得到的都是同一个类。
缺点:
- 灵活性受限,例如在模块化开发时,如果不同模块要求加载的类库版本不一致,双亲委派模式难以实现。(Tomcat打破了双亲委派模式)
- SPI服务加载矛盾,例如核心接口(如java.sql.Driver)y由启动类加载器加载,但实现类(如Mysql驱动)需要由应用类加载器加载。
- 热部署困难,类一旦被加载后,无法通过同一类加载器重新加载修改后的类,需要重启部署整个项目(即所有类需要重新被加载)。
2.3 打破双亲委派模型
Java默认采用双亲委派模型来加载类,但是在某些场景下,我们需要打破这种方式,来实现特定的需求。
2.3.1 SPI服务—java.sql.Driver
SPI(Service Provide Interface),服务供给接口。Java 1.5新添加的一个内置标注。Java核心库提供特定的服务接口,用户或第三方服务来实现这个接口。同时在META-INFA/services文件夹下,编写以该接口全限定名命名的文档,内容为实现类的全限定名。然后Java根据该全限定名来加载这个实现类。
下面以java.sql.Driver 为例,介绍SPI。
java.sql.Driver 接口主要职责是建立数据库连接。
图 mysql-connector-java 中Driver的实现类及服务提供文件
JDK 核心库中的java.sql.DriverManager,负责管理Driver类,包括Driver实现类的加载。其同时支持两种加载驱动类的方式:
1)加载系统属性jdbc.drivers指定的驱动类。
Class.forName(“Driver实现类全限定名”, true, ClassLoader.getSystemClassLoader());
ClassLoader.getSystemClassLoader()方法获取系统类加载器(即默认的应用程序加载器Application ClassLoader)
2)加载SPI机制提供的驱动类。
ServiceLoader.load(Driver.class);
// ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
SPI机制用的类加载器是通过Thread.currentThread().getContextClassLoader();获取。该方法获取的类加载器默认为应用程序加载器。
2.3.2 模块化部署—Tomcat容器
假如在Tomcat同时部署两个应用web1和web2。其中web1依赖spring-core-5.3.0.jar。而web2依赖spring-core-6.0.0.jar。Tomcat默认是单JVM运行多个Web应用。如果采用双亲委派策略,假如web1的某个父加载器加载了spring-core-5.3.0.jar,而到web2时,由于该类已被加载,就不会加载spring-core-6.0.0.jar,那么此时,web2 会报异常NoSuchMethodError或版本冲突。
Common | 加载Tomcat通用类。 |
Catalina | 加载Tocmat自身类。 |
Shared | 加载所有Web应用共享的类库(可配置)。 |
WebAppClassLoader | 每个Web应用独立的类加载器,优先从自身类路径(WEB-INF/class和WEB-INF/lib)加载。 |
图 Tomcat的类加载器
图 Tomcat的类加载器层级
WebAppClassLoader打破了双亲委派模型,会优先自己加载,而非直接委托父类加载器。