【JVM】类加载
概述
负责从硬盘上加载字节码文件到JVM中。
类加载器子系统负责从文件系统或者网络中加载 class 文件。ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。加载的类信息存放于一块称为方法区的内存空间。
class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中的,根据这个模板实例化出 n 个实例 .class file 加载到 JVM 中,被称为 DNA 元数据模板。此过程就需要一个运输工具(类加载器 ClassLoader)来扮演一个快递员的角色。
类加载过程
类的生命周期
加载
把类从硬盘读到内存的过程,通过类名(地址)获取此类的二进制字节流,以二进制字节流的方式加载字节码,在内存中为此类生成一个Class对象,作为此类各种数据的访问入口,这是一个将静态存储转为运行时存储的过程。
链接
验证
1.验证字节码格式是否正确。
2.验证语法是否正确。
大概知道它是干啥的就可以了,如果想深入可以去看《深入理解Java虚拟机:JVM高级特性与最佳实践》(第3版)这本书的第七章第2节。
准备
准备阶段负责为类的静态属性分配内存,并设置默认初始值。
比如我们在类中写了一个 static int value = 10; 这个10是在初始化阶段才赋给 value 的,而准备阶段 value 的初始值是0。
但不包含用 final 修饰的 static 常量,因为其在编译时就进行了初始化。
解析
将静态文件中指令的符号引用替换成内存中指向方法区某一地址的直接引用。
初始化
对类变量(静态变量)进行赋值。
类什么时候会被初始化?
package com.mdy.javaPro.jvm.classloader_demo;
public class User {
// 准备阶段
// static int value = 0;
// 初始化阶段
// static int value = 10;
static final int value = 10; // 静态常量在编译期间就被初始化了
/*static {
value = 20;
}*/
static {
System.out.println("User类被加载了");
}
}
package com.mdy.javaPro.jvm.classloader_demo;
public class TestUser {
public static void main(String[] args) {
System.out.println(User.value);
}
}
1.使用类中的静态变量、静态方法。
2.在一个类中运行 main 方法。
3.创建对象。
4.使用反射加载一个类。
5.加载一个类的子类(优先加载其父类)。
注意:当只使用某个类中静态常量时,类不会被初始化,因为静态常量在编译阶段就被初始化了。
类在加载阶段初始化完成,才是类的整个加载过程结束。
类加载器
真正实施类加载的具体实现者。
分类
宏观上分为两类:
1.引导类加载器(启动类加载器),不是用Java语言写的,使用C/C++嵌套在JVM内部。
2.其他类加载器,用Java语言写的实现类,都继承 java.lang.ClassLoader。
站在Java开发人员角度可细分为四类:
1.引导类加载器(启动类加载器),Java中系统提供的类,都是由启动类加载器加载,例如 String。
2.扩展类加载器,Java语言编写的,由 sum.misc.Launcher$ExtClassLoader 实现,派生于 ClassLoader 类。从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。
3.应用程序类加载器(Application ClassLoader),Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现,派生于 ClassLoader 类。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4.自定义类加载器,例如我们自己写一个类继承 ClassLoader;再例如 tomcat 这种容器,都有自己加载类的加载器。
package com.mdy.javaPro.jvm.classloader_demo;
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // 返回 null,说明是引导类加载器加载的
// 返回 sun.misc.Launcher$AppClassLoader@18b4aac2,说明我们自己的类是由应用程序类加载器加载的
System.out.println(ClassLoaderDemo.class.getClassLoader());
// 返回 sun.misc.Launcher$ExtClassLoader@1b6d3586,应用程序类加载器是由扩展类加载器加载的
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent());
// 返回 null,扩展类加载器是由引导类加载器加载的
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
双亲委派机制
我们自己伪造一个 java.lang.String,然后进行测试:
package java.lang;
public class String {
public String() {
System.out.println("自定义的String构造方法");
}
}
package com.mdy.javaPro.jvm.classloader_demo;
public class StringTest {
public static void main(String[] args) {
new java.lang.String(); // 运行后,控制台没有任何输出
try {
// 给一个不存在的类名(地址)
Class.forName("com.mdy.Demo");
} catch (ClassNotFoundException e) {
System.out.println("找不到");
}
}
}
如果一个类加载器收到了类加载请求,它会将请求委托给父类加载器,父类加载器也是向上委托,直到找到引导类加载器。如果上级类加载器找到了类,就使用上级类加载器加载的类;如果上级找不到,就逐级向下委托,使用子级类加载器加载的类;如果都找不到,那就抛出 ClassNotFoundException 异常。这样做的好处就是避免我们自己开发的类替换掉系统中的类。
如何打破双亲委派机制?
通过自定义类加载器,重写 ClassLoader 类中的 findClass(),从而打破双亲委派机制。再例如 tomcat 等都有自己定义的类加载器。
package com.mdy.javaPro.jvm.classloader_demo;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
// 类的路径
private String classPath;
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.classPath = codePath;
}
public MyClassLoader(String codePath) {
this.classPath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
// 完整的类名
String file = classPath + name + ".class"; // D:/Hello.class
try {
// 初始化输入流
bis = new BufferedInputStream(new FileInputStream(file));
// 获取输出流
baos = new ByteArrayOutputStream();
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
// 获取内存中的字节数组
byte[] bytes = baos.toByteArray();
// 调用 defineClass 将字节数组转换成 class 实例
Class<?> cls = defineClass(null, bytes, 0, bytes.length);
return cls;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("D:/");
try {
// Class<?> cls = myClassLoader.loadClass("Hello"); // 用系统的类加载流程
Class<?> cls = myClassLoader.findClass("Hello"); // 用我们自己重写的类加载方式
// 打印具体的类加载器,验证是否由我们自定义的类加载器加载
System.out.println("测试字节码是由" + cls.getClassLoader().getClass().getName() + "加载的");
Object o = cls.newInstance();
System.out.println(o.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}