java中Class类装载流程是怎样的

发布时间:2021-09-27 09:50:56 作者:柒染
来源:亿速云 阅读:306
# Java中Class类装载流程是怎样的

## 前言

在Java虚拟机(JVM)执行Java程序的过程中,类的加载是一个至关重要的环节。理解Class类装载机制不仅有助于我们深入理解Java的运行原理,还能帮助开发者解决实际开发中遇到的类加载问题。本文将全面剖析Java中Class类的装载流程,从基本概念到具体实现细节,为读者呈现一个完整的类加载知识体系。

## 一、类加载的基本概念

### 1.1 什么是类加载

类加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类加载的最终产品是位于堆中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

### 1.2 类加载的时机

Java虚拟机规范并没有明确规定类加载的时机,但规定了以下几种情况必须立即对类进行"初始化"(而加载、验证、准备自然需要在此之前开始):

1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时
2. 使用java.lang.reflect包的方法对类进行反射调用时
3. 当初始化一个类时,发现其父类还未初始化时
4. 虚拟机启动时,用户指定的主类(包含main()方法的那个类)
5. 当使用JDK1.7的动态语言支持时

### 1.3 类加载器的层次结构

Java中的类加载器采用双亲委派模型,主要分为以下几类:

1. **启动类加载器(Bootstrap ClassLoader)**: 由C++实现,负责加载JAVA_HOME/lib目录下的核心类库
2. **扩展类加载器(Extension ClassLoader)**: 负责加载JAVA_HOME/lib/ext目录下的扩展类
3. **应用程序类加载器(Application ClassLoader)**: 负责加载用户类路径(ClassPath)上的类库
4. **自定义类加载器**: 用户自定义的类加载器

```java
// 获取类加载器的示例代码
public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 获取当前类的类加载器
        ClassLoader loader = ClassLoaderDemo.class.getClassLoader();
        System.out.println(loader); // AppClassLoader
        
        // 获取系统类加载器
        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemLoader); // AppClassLoader
        
        // 获取扩展类加载器
        ClassLoader extLoader = systemLoader.getParent();
        System.out.println(extLoader); // ExtClassLoader
        
        // 获取启动类加载器
        ClassLoader bootLoader = extLoader.getParent();
        System.out.println(bootLoader); // null (由C++实现,Java中无法直接获取)
    }
}

二、类加载的详细流程

类加载的全过程可以分为加载、验证、准备、解析和初始化五个阶段。其中验证、准备、解析三个部分统称为连接(Linking)。

2.1 加载阶段

加载阶段是类加载过程的第一个阶段,主要完成以下工作:

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2.1.1 获取二进制字节流的方式

虚拟机规范并没有规定从哪里获取二进制流,因此可以有以下多种实现方式:

2.1.2 数组类的特殊性

数组类本身不通过类加载器创建,而是由Java虚拟机直接创建。但数组类的元素类型最终还是要靠类加载器来完成加载。

2.2 验证阶段

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成以下四个检验动作:

2.2.1 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理:

2.2.2 元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范:

2.2.3 字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的:

2.2.4 符号引用验证

在解析阶段发生,检查符号引用中通过字符串描述的全限定名是否能找到对应的类:

2.3 准备阶段

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。需要注意几点:

  1. 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量
  2. 初始值通常是数据类型的零值,如int类型的0,boolean类型的false等
  3. 如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量值就会被初始化为ConstantValue属性所指定的值
// 准备阶段示例
public class PreparationDemo {
    public static int value = 123; // 准备阶段后value的初始值为0
    public static final int CONST_VALUE = 456; // 准备阶段后CONST_VALUE的值为456
}

2.4 解析阶段

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

2.4.1 类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机需要完成以下步骤:

  1. 如果C不是一个数组类型,虚拟机会将代表N的全限定名传递给D的类加载器去加载这个类C
  2. 如果C是一个数组类型,并且数组的元素类型为对象类型,将会按照第1点的规则加载数组元素类型
  3. 最后检查访问权限,确保D对C有访问权限

2.4.2 字段解析

解析一个未被解析过的字段符号引用时,首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果解析成功,将对这个类或接口进行字段的搜索:

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
  2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口
  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类
  4. 否则,查找失败

2.4.3 类方法解析

类方法解析的第一个步骤与字段解析一样,也需要先解析出方法表的class_index项中索引的方法所属的类或接口的符号引用。如果解析成功,将对这个类或接口进行方法的搜索:

  1. 类方法和接口方法符号引用的常量类型定义是分开的
  2. 如果在类方法表中发现class_index中索引的C是个接口,直接抛出java.lang.IncompatibleClassChangeError异常
  3. 如果通过第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法
  4. 否则,在类C的父类中递归查找
  5. 否则,在类C实现的接口列表及它们的父接口中递归查找
  6. 否则,查找失败

2.5 初始化阶段

初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。初始化阶段就是执行类构造器<clinit>()方法的过程。

2.5.1 <clinit>()方法的特点

  1. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的
  2. <clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕
  3. 由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  4. <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
  5. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法
  6. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步
// 初始化阶段示例
public class InitializationDemo {
    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }
    
    static class Sub extends Parent {
        public static int B = A;
    }
    
    public static void main(String[] args) {
        System.out.println(Sub.B); // 输出2
    }
}

三、类加载器的实现机制

3.1 双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型的重要特性:

  1. 委派性:子类加载器先委托父类加载器加载
  2. 可见性:子类加载器可以访问父类加载器加载的类,而父类加载器不能访问子类加载器加载的类
  3. 唯一性:保证一个类只会被加载一次,避免重复加载

3.2 破坏双亲委派模型

在某些特殊情况下,双亲委派模型会被破坏:

  1. SPI机制:JDBC等SPI服务使用线程上下文类加载器加载实现类
  2. OSGi:OSGi实现模块化热部署,采用网状结构的类加载器架构
  3. 热替换:如Tomcat实现JSP文件的热替换

3.3 自定义类加载器

自定义类加载器通常需要继承ClassLoader类,并重写findClass方法:

public class CustomClassLoader extends ClassLoader {
    private String classPath;
    
    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classData = getClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            }
            return defineClass(name, classData, 0, classData.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
    
    private byte[] getClassData(String className) throws IOException {
        String path = classPath + File.separatorChar + 
            className.replace('.', File.separatorChar) + ".class";
        try (InputStream ins = new FileInputStream(path);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        }
    }
}

四、类加载的常见问题与解决方案

4.1 ClassNotFoundException与NoClassDefFoundError

4.2 类初始化死锁

在多线程环境下,类的初始化可能会导致死锁:

public class InitializationDeadlock {
    static class A {
        static {
            System.out.println("A init");
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            B.test();
        }
        
        public static void test() {
            System.out.println("A test");
        }
    }
    
    static class B {
        static {
            System.out.println("B init");
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            A.test();
        }
        
        public static void test() {
            System.out.println("B test");
        }
    }
    
    public static void main(String[] args) {
        new Thread(() -> A.test()).start();
        new Thread(() -> B.test()).start();
    }
}

解决方案:避免在类的初始化过程中调用其他可能还未初始化的类的方法

4.3 类加载器内存泄漏

在长时间运行的应用程序中(如应用服务器),不正确的类加载器使用可能导致内存泄漏。常见场景:

  1. 线程局部变量持有类加载器加载的类的引用
  2. 静态集合持有类加载器加载的类的实例
  3. 缓存未正确清理

解决方案:谨慎使用静态集合,确保动态加载的类有适当的生命周期管理

五、类加载的优化技巧

5.1 类加载性能监控

可以通过JVM参数监控类加载情况:

5.2 类预加载

对于已知需要使用的类,可以在应用启动时预先加载:

public class PreLoader {
    public static void preloadClasses() {
        String[] classesToPreload = {
            "java.util.ArrayList",
            "java.util.HashMap",
            // 其他常用类
        };
        
        for (String className : classesToPreload) {
            try {
                Class.forName(className);
            } catch (ClassNotFoundException e) {
                // 处理异常
            }
        }
    }
}

5.3 类共享

在多个应用服务器实例间共享已加载的类,减少重复加载开销:

  1. 使用共享类目录
  2. 使用Java EE应用服务器的共享库功能
  3. 使用OSGi等模块化技术

六、总结

Java的类加载机制是Java语言灵活性和安全性的重要基石。理解类加载的完整流程,从加载、验证、准备、解析到初始化,有助于开发者编写更健壮的Java应用程序。双亲委派模型虽然有其局限性,但在大多数情况下提供了良好的类隔离和安全保障。在实际开发中,合理使用自定义类加载器可以实现热部署、模块隔离等高级特性,但同时也需要注意避免内存泄漏和性能问题。

随着Java模块化系统(JPMS)的引入,类加载机制也在不断演进。掌握类加载的核心原理,将帮助开发者更好地适应Java平台的未来发展。

参考资料

  1. 《深入理解Java虚拟机》- 周志明
  2. 《Java虚拟机规范》(Java SE 8版)
  3. Oracle官方文档 - Class Loading in Java
  4. IBM DeveloperWorks - Java类加载原理解析
  5. OpenJDK源码

本文共计约5900字,全面介绍了Java中Class类装载的流程、机制、问题及优化技巧,适合中高级Java开发者阅读参考。 “`

推荐阅读:
  1. 反编译java class文件的流程
  2. java如何判断class是否是某个类的子类或父类

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java class

上一篇:Linux系统的服务器上出现故障常见的解决命令有哪些

下一篇:有哪些使用SSH的技巧

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》