JVM-类加载及类加载器
本文是JVM系列第二篇
类加载器的作用和分类
类加载器(Class Loader)是Java虚拟机的一个重要组成部分,它负责将Java类加载到内存中,并将其转换成Java对象。类加载器的作用是将Java程序的字节码加载到内存中,并将其转换成Java对象,以便在Java虚拟机中执行。
类加载器的分类如下:
- 启动类加载器(Bootstrap Class Loader):负责加载Java平台核心库,如rt.jar等。启动类加载器是Java虚拟机的一部分,是用C++编写的,不是Java类,因此无法在Java程序中直接访问。
- 扩展类加载器(Extension Class Loader):负责加载Java平台扩展库,如jce.jar等。扩展类加载器是由Java语言编写的,它是Java虚拟机类加载器的子类。
- 应用程序类加载器(Application Class Loader):负责加载应用程序的类,如自己编写的类和第三方类库。应用程序类加载器也是由Java语言编写的,它是Java虚拟机类加载器的子类。
- 自定义类加载器:可以根据需要自定义类加载器,用于加载一些特殊的类,如加密过的类等。
类加载器的作用主要包括:
- 加载类:将Java程序的字节码文件加载到内存中,转换成Java对象并初始化。
- 搜索类:在类路径中搜索指定的类,找到后将其加载到内存中。
- 命名空间隔离:每个类加载器都有独立的命名空间,可以避免不同类之间的冲突。
总之,类加载器是Java虚拟机的重要组成部分,负责将Java类加载到内存中,以便在Java虚拟机中执行。类加载器可以根据需要自定义,也可以根据功能和权限进行分类。
类加载过程及原理
类加载是Java程序运行的重要过程之一,它将Java类加载到内存中,并将其转换成Java对象以便在Java虚拟机中运行。类加载过程分为三个阶段:加载、链接和初始化。
- 加载:在加载阶段,类加载器会从指定的位置(例如本地文件系统、网络等)加载类的字节码文件到内存中,并生成相应的Class对象。在加载阶段,Java虚拟机不会对类进行任何的链接和初始化操作。
- 链接:在链接阶段,Java虚拟机会对类进行验证、准备和解析等操作。
- 验证:验证阶段主要是对类的字节码进行验证,以确保它符合Java虚拟机规范,不会出现安全问题和运行时异常。
- 准备:准备阶段主要是为类的静态变量分配内存,并初始化为默认值,如int类型的变量初始化为0,对象类型的变量初始化为null等。
- 解析:解析阶段主要是将类中的符号引用转换成直接引用,以便在运行时可以直接访问类中的方法和字段。
- 初始化:在初始化阶段,Java虚拟机会执行类的初始化代码,包括静态代码块和静态变量的赋值等操作。
在Java虚拟机规范中,只有在以下情况下才会执行类的初始化代码:
- 创建类的实例;
- 访问类的静态变量或者静态方法;
- 使用反射机制访问类的方法或者字段;
- 初始化类的子类;
- 启动Java虚拟机时,指定的类。
代码执行顺序举例
public class MethodOrder {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("-----------------");
dog.instanceMethod();
System.out.println("-----------------");
Dog.staticMethod();
System.out.println("-----------------");
Dog dog2 = new Dog("Jerry");
dog2.instanceMethod();
System.out.println("-----------------");
}
}
class Animal {
static {
System.out.println("Animal 静态代码块");
}
{
System.out.println("Animal 构造代码块");
}
public Animal() {
System.out.println("Animal 无参构造函数");
}
public Animal(String name) {
System.out.println("Animal 有参构造函数");
}
public static void staticMethod() {
System.out.println("Animal 静态方法");
}
public void instanceMethod() {
System.out.println("Animal 实例方法");
}
}
class Dog extends Animal {
private static String name = "default";
static {
System.out.println("Dog 静态代码块");
}
{
System.out.println("Dog 构造代码块");
}
public Dog() {
super("Tom");
System.out.println("Dog 无参构造函数");
}
public Dog(String name) {
super(name);
System.out.println("Dog 有参构造函数");
}
public static void staticMethod() {
System.out.println("Dog 静态方法");
}
public void instanceMethod() {
System.out.println("Dog 实例方法");
}
}
执行顺序如下
Animal 静态代码块
Dog 静态代码块
Animal 构造代码块
Animal 有参构造函数
Dog 构造代码块
Dog 无参构造函数
-----------------
Dog 实例方法
-----------------
Dog 静态方法
-----------------
Animal 构造代码块
Animal 有参构造函数
Dog 构造代码块
Dog 有参构造函数
Dog 实例方法
-----------------
类加载器的双亲委派模型
类加载器的双亲委派模型是一种类加载机制,它是Java中默认的类加载器行为。
在该模型中,当一个类加载器接收到类加载请求时,它会优先将该请求委派给它的父类加载器来尝试加载这个类。只有当父类加载器无法加载该类时,子类加载器才会尝试加载该类。
这样的设计有以下几个好处:
- 避免重复加载:如果父类加载器已经加载了某个类,那么子类加载器就没有必要再次加载该类,从而避免了重复加载,减少了内存占用和冲突问题。
- 安全性保障:通过双亲委派模型,可以确保核心Java API类由引导类加载器加载,而不是被自定义的类加载器所替代,从而避免了恶意代码替换核心API类的安全隐患。
- 代码隔离:每个类加载器都有自己的命名空间,从而实现了代码的隔离,不同类加载器加载的类互相之间不会产生影响,避免了类冲突和版本不兼容问题。
类加载器的自定义
public class CustomClassLoader extends ClassLoader {
private final Map<String, Class<?>> classes = new HashMap<>(); // 缓存已加载的类
private final String classPath; // 类文件路径
private final Set<String> allowedPackages; // 允许加载的包名列表
public CustomClassLoader(String classPath, Set<String> allowedPackages) {
this.classPath = classPath;
this.allowedPackages = allowedPackages;
}
public synchronized void addClass(String name, byte[] bytes) {
classes.put(name, defineClass(name, bytes, 0, bytes.length));
}
@Override
public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = classes.get(name);
if (clazz != null) {
return clazz;
}
if (!isAllowed(name)) {
throw new ClassNotFoundException("Class " + name + " not allowed");
}
try {
// 使用双亲委派模型加载类
return super.loadClass(name);
} catch (ClassNotFoundException e) {
// 如果双亲委派模型加载失败,则调用自定义逻辑加载类
byte[] bytes = getClassData(name);
if (bytes == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, bytes, 0, bytes.length);
}
}
private boolean isAllowed(String name) {
for (String allowedPackage : allowedPackages) {
if (name.startsWith(allowedPackage)) {
return true;
}
}
return false;
}
private byte[] getClassData(String name) {
String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try (InputStream inputStream = new FileInputStream(path)) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
return null;
}
}
}
在上述示例中,我们定义了一个CustomClassLoader类,继承自ClassLoader,并重写了loadClass()方法来实现类的加载逻辑。在加载类的过程中,我们先尝试使用双亲委派模型加载类,如果无法加载,则调用自定义逻辑从指定路径加载。
此外,我们还添加了缓存已加载的类、允许加载的包名列表等功能,以提高类加载的性能和安全性。
使用此自定义类加载器,我们可以加载特定目录下的类,并且只允许加载指定的包名,从而实现类隔离和安全性控制。
例如,我们可以使用以下代码加载特定目录下的类,并只允许加载指定的包名:
Set<String> allowedPackages = new HashSet<>();
allowedPackages.add("com.example.package1");
allowedPackages.add("com.example.package2");
CustomClassLoader loader = new CustomClassLoader("path/to/classes", allowedPackages);
Class<?> clazz = loader.loadClass("com.example.package1.MyClass");
在上述代码中,我们创建了一个CustomClassLoader类加载器,指定了类文件所在的路径以及允许加载的包名列表。然后,通过调用loadClass()方法加载指定的类。如果类不在允许加载的包名列表中,则会抛出ClassNotFoundException异常。
转载自:https://juejin.cn/post/7246777406386782265