JVM执行加载结构 #
类加载器 #
用来加载.class字节码文件,加载为Class对象存储到JVM内存中
类加载器的执行过程 #
- 加载:根据全类名查找和导入
.class文件; - 链接(与加载交叉进行)
- 校验:检查载入
.class文件数据的正确性和代码逻辑 - 准备:给类的静态变量分配存储空间,设置默认值(只是赋数据类型的零值)
- 例如
static Integer num = 111:此时num的值只是赋了零值0(初始化阶段才会赋值) - 如果
static final Integer num = 111:那么当前准备阶段就会直接赋值111
- 例如
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程
- 校验:检查载入
- 初始化:对类的静态变量、静态代码块执行初始化工作
- 此阶段不是所有的类都会被初始化,只有在以下五种情况才会初始化(主动使用类才会初始化)
- 当遇到
new、getstatic、putstatic、invokestatic这 4 条直接码指令时,比如 new一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。- 当 jvm 执行
new指令时会初始化类。即当程序创建一个类的实例对象 - 当 jvm 执行
getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - 当 jvm 执行
putstatic指令时会初始化类。即程序给类的静态变量赋值 - 当 jvm 执行
invokestatic指令时会初始化类。即程序调用类的静态方法
- 当 jvm 执行
- 使用
java.lang.reflect包的方法对类进行反射调用时如Class.forname("..."),newInstance()等等。如果类没初始化,触发其初始化。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main方法的那个类),虚拟机会先初始化这个类。 - 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 当遇到
- 此阶段不是所有的类都会被初始化,只有在以下五种情况才会初始化(主动使用类才会初始化)
- 卸载:卸载需要满足要求如下
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
- 综上,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的;但由我们自定义的类加载器加载的类有可能被卸载
类加载器的分层和分类 #
- 启动类加载器(根类加载器、引导类加载器)(BootstrapClassLoader)
- 最顶层的加载类,由 C++实现,主要用来加载 JDK 内部的核心类库(
%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等jar包和类)以及被-Xbootclasspath参数指定的路径下的所有类。rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。
- 最顶层的加载类,由 C++实现,主要用来加载 JDK 内部的核心类库(
- 扩展类加载器(ExtClassLoader)
- 主要负责加载
%JRE_HOME%/lib/ext目录下的 jar 包和类以及被java.ext.dirs系统变量所指定的路径下的所有类。
- 主要负责加载
- 系统类加载器(AppClassLoader)
- 面向用户的加载器,负责加载当前应用
classpath下的所有 jar 包和类。
- 面向用户的加载器,负责加载当前应用
除了BootstrapClassLoader是 JVM 自身的一部分(无需加载)之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
扩展类加载器和系统类加载器由于也是类,也是JDK中提供的类,所以是由引导类加载器进行加载
每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过BootstrapClassLoader加载的。
加载顺序(双亲委派) #
- 类加载器加载存在委托机制,此机制的目的是保证了 Java 程序的稳定运行,可以避免类的重复加载
- 模型要求除顶层的启动类加载器外,其余的类加载器必须有自己的父类加载器。
- 在查找类或资源之前,搜索类和资源的任务会委托给父类加载器。
具体流程如下:
- 系统类加载器:任何类一开始都是由系统类加载器来加载,也就说最下层来加载的。但是由于类加载器具备委托机制。所以,它不会马上去自己加载,而是委托给上一层类加载器来加载,即扩展类加载器来加载。如果扩展类加载器加载到了就加载进内存,如果扩展类加载器没有加载到,就自己再去加载,如果它自己也没有加载到,就会报异常
ClassNotFoundException - 扩展类加载器:如果轮到扩展类加载器来加载的话,由于类加载器具备委托机制,它会让上一层类加载器来加载,即引导类加载器来加载,如果引导类加载器加载到了,就进内存,如果引导类加载器加载不到,再由自己来加载,如果自己也没有加载到,就返回到系统类加载器来加载
- 引导类加载器:如果轮到引导类加载器来加载的话,由于它没有上一层,所以自己来加载。如果加载到,就进内存;如果它自己没有加载到,就会返回到扩展类加载器来加载
自定义类加载器 #
为什么自定义类加载器 #
-
隔离加载类
-
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。
-
两个jar包内都存在相同类名且包名相同,如果没有隔离加载类,则会报错,如:两个版本的jar
-
-
修改类加载方式
- 类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
-
扩展加载源
- 比如从数据库、网络、甚至是电视机机顶盒进行加载
-
防止源码泄露
-
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
-
通常Java系统想增加License(授权),就可以通过自定义类加载器实现。
-
实现自定义类加载器 #
自定义加载器的parent为AppClassLoader
Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
在自定义ClassLoader的子类时候,我们常见的会有两种做法
方式一:重写loadClass()方法
方式二:重写findClass()方法
这两种方法本质上差不多,毕竟
loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。
loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。
MyClassLoader #
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 解析字节码文件路径
String s = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
String filePath = classPath + s + ".class";
// 读取字节码文件
FileInputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
try {
inputStream = new FileInputStream(filePath);
outputStream = new ByteArrayOutputStream();
int len;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
}
byte[] outBytes = outputStream.toByteArray();
// 使用defineClass根据字节码创建Class实例
return defineClass(name,outBytes,0,outBytes.length);
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
if (outputStream != null){
try {
outputStream.flush();
outputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
if (inputStream != null){
try {
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
top.ygang.Student #
package top.ygang;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用命令javac -encoding utf-8 Student.java编译,并且将Student.class拷贝到D:\\下
注意:删除项目中的Student.java!不然由于双亲委派机制,自定义类加载器向上委托,AppClassLoader会在项目classPath中找到该类并执行进行加载
main #
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("D:\\");
Class<?> aClass = myClassLoader.loadClass("top.ygang.Student");
System.out.println(aClass.getClassLoader());
// classloader.MyClassLoader@4dd8dc3
Constructor<?> constructor = aClass.getConstructor(String.class, int.class);
Object lucy = constructor.newInstance("lucy", 12);
System.out.println(lucy);
// Student{name='lucy', age=12}
}
相关API #
获取类加载器对象 #
// 先获取类的字节码文件对象
Class clazz = Person.class;
// 通过字节码文件对象获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();
// 获取类加载器的上一层类加载器
ClassLoader classLoader1 = classLoader.getParent();
使用类加载器来读取配置文件 #
原始方法,通过Properties类API进行读取
// 在src的路径下,有一个jdbc.properties的配置文件
Properties p = new Properties();
p.load(new FileInputStream("src/jdbc.properties"));
String driver = p.getProperty("driver");
System.out.println(driver);
这种方法的弊端是如果配置文件在jar包内,也就是classpath下,则无法读取
使用类加载器,实际上就是AppClassLoader,由于这个类加载器加载的是classpath下的类,所以同样也就可以用来读取classpath下的文件
方式一 #
// 获取类的字节码文件对象
Class clazz = Demo2.class;
// 获取类加载器的对象
ClassLoader classLoader = clazz.getClassLoader();
// 使用类加载器对象读取配置文件
// 注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
InputStream is = classLoader.getResourceAsStream("jdbc.properties");
Properties p = new Properties();
p.load(is);
String driver = p.getProperty("driver");
System.out.println(driver);
方式二 #
// 获取类的字节码文件对象
Class clazz = Demo1.class;
// 获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();
// 使用类加载器对象读取配置文件
// 注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
URL url = classLoader.getResource("jdbc.properties");
String path = url.getPath();
FileInputStream fis = new FileInputStream(path);
Properties p = new Properties();
p.load(fis);
String driver = p.getProperty("driver");
System.out.println(driver);