Java 高并发编程详解 3.0 类加载过程

测试类加载过程程序


/**
 * 测试类加载过程
 */
public class Singleton {

    // 2
    private static Singleton singleton = new Singleton();
    // 1
    private static int x = 4;
    private static int y;
    
    private Singleton(){
        x++;
        y++;
    }
    public static Singleton getSingleton(){
        return singleton;
    }

    public static void main(String[] args) {
        Singleton test = Singleton.getSingleton();
        System.out.println(test.x+","+test.y);
    }

    // x = 4 在上面时输出
    // 5,1

    // x = 4 在下面时输出
    // 4,1 x被重新赋值
}

类的主动使用和被动使用

主要是知道什么时候类主动使用

  1. 使用new 关键字生成对象会初始化,如果数组,集合等容器对象不会初始化
  2. 访问类的静态变量,静态方法会初始化,但是静态变量还用final修改饰(private final static int x = 4;),直接有类.的方式是不会初始化,这个一定不能经过复杂运算(取随机数 private final static int y = new Random().nextInt()😉
  3. 使用反射机制生成对象
  4. 初始化子类也会导致父类初始化,但是只是子类访问父类的静态方法或变量时,只有父类会初始化,子类不会。
  5. 启动类,执行main函数所在的类会初始化

类加载过程

一般分为三个比较大的阶段,加载过程,连接过程,初始化过程。

加载过程

查找并加载类的二进制数据文件(class文件)
一般来源 为堆区的class对象,还有其他地方 只要是二进制就可以进入加载过程。

  1. 动态代理生成的二进制信息
  2. 通过网络获取的二进制信息
  3. 通过读取zip压缩文件获取的二进制信息
  4. 读取数据库存储的二进制信息
  5. 运行时才生成的class二进制信息

连接过程

连接过程为重点 主要分为 验证二进制信息是否正确 为类的静态变量分配内存和初始值(初始值不是代码设置而是类型固定值例如int为0) 解析类,接口,字段,方法的符号引用是否正确

验证阶段

  1. 验证文件格式是否正确
查看二进制文件头部魔术因子是不是class的魔术因子
查看java版本,低版本的java不能兼容高版本的class
通过class的MD5加密信息来查看二进制流信息是否被修改
查看常量的引用和值不能为不支持的类型
  1. 验证元数据信息
查看 类是否有父类或接口,有的话看其是否存在和合法
查看 final修饰的类是不能继承 final的方法也不能被重写
查看 该类是不是抽象类的实现 其对象的接口是否实现全部
查看 该类的重载的方法是否正确 方法名和参数类型顺序都一致,只是返回值不一样 这种是不正确的
  1. 字节码验证
主要验证程序的控制流程是否正确,如循环,分支等
  1. 符合引用是否正确
主要验证各个类的引用是否正确,是否存在.

准备阶段

为类的静态变量分配内存和初始值(初始值不是代码设置而是类型固定值例如int为0)

解析阶段

主要是针对类接口,字段,类方法,接口方法这四个类进行,从其对应的常量池取值

  1. 类接口解析
    查看类是否存在,是否正确,不正确就抛出异常
  2. 字段解析
    查看类接口中的字段 是否存在,是否正确,不正确也抛出异常
  3. 类方法解析
    类的静态方法可以直接通过类名获取,而接口静态方法,必须要有实现类才可以通过类名获取。
  4. 接口方法解析
    和类方法一样 检查是否存在,是否正确,不正确则抛出异常。

初始化阶段

给所有的类变量重新赋予正确的值,设置为代码中的值而不是默认初始值。
class initialize方法是编译阶段生成的,是包含在class文件中的。包含类变量的赋值动作和静态预计块的执行代码。当类既没有静态变量也没有静态方法,那么class initialize方法则没有数据。

以上为类加载过程,后面同JVM加载器来保证类在JVM中的唯一性。

JVM类加载器

  1. 根类加载器:BootstrapClassLoader 用来加载虚拟机的核心类库,用C++编写实现
  2. 扩展类加载器:ExtClassLoader 用来加载JAVA_HOME目录下的jre/lb/ext目录下的类库,用java编写实现
  3. 系统类加载器:ApplicationClassLoader 用来加载CLASS_PATH目录下的类库和jar包,用java编写实现,一般为自定义加载器的父类加载器(这个父类不是指extends关键字来实现,而是使用组合方式实现即类中有变量保持父类加载器)
  4. 自定义加载器:一定为ClassLoader类的子类,通过重写findClass方法来实现加载其他的二进制class类,或者通过重写loaderClass方法来破坏双亲委派机制(先判断自己找到没有,找到直接返回,而不去父类加载器再找),加载指定的class文件。

自定义类加载器实现

  1. 自定义类加载器实现类
/**
 * 自定义 类加载器
 */
// 自定义类加载器必须为ClassLoader的子类
public class MyClassLoader extends ClassLoader {
    // 设置class存放路径
    private final static Path DEFAULT_CALSS_DIR = Paths.get("C:/Java/classloader");
    private final Path classDir;

    public MyClassLoader() {
        super();
        this.classDir = DEFAULT_CALSS_DIR;
    }
    // 可以设置class存放路径的构造
    public MyClassLoader(String classDir){
        super();
        this.classDir = Paths.get(classDir);
    }
    // 可以设置class存放路径和 指定父类加载器的构造
    public MyClassLoader(String classDir,ClassLoader parent){
        super(parent);
        this.classDir = Paths.get(classDir);
    }

    // 重写父类的findClass方法 通过这个方法查找class 二进制信息
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 读取二进制信息
        byte[] bytes = this.readClassBytes(name);
        // 如果为null 或者没有信息时 抛出异常
        if(bytes == null || bytes.length == 0){
            throw new ClassNotFoundException(name+" 这个class文件不存在或为空白");
        }
        // 加载class
        return this.defineClass(name,bytes,0,bytes.length);
    }

    // 将class文件读进内存
    private byte[] readClassBytes(String name) throws ClassNotFoundException{
        // 将包名分隔符 . 换成文件分分隔符 /
        String classPath = name.replace(".","/");
        // 测试数据时 直接拷贝HelloWorld.class 到类加载目录 所以要截取最后类的信息 包名信息忽略
        int index = classPath.lastIndexOf("/");
        String className= classPath.substring(index+1);
        Path classFullPath = classDir.resolve(Paths.get(className + ".class"));
        if(!classFullPath.toFile().exists()){
            throw new ClassNotFoundException("类名为:"+name+" 不存在");
        }
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
            Files.copy(classFullPath,outputStream);
            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException("读取class二进制信息报错 名称:"+name,e);
        }
    }

    @Override
    public String toString() {
        return "My ClassLoader";
    }
}
  1. 生成测试的HelloWorld.class文件信息
public class HelloWorld {
    static {
        System.out.println("Hello World Class is Initialized");
    }
    public String welcome(){
        return "Hello World";
    }
}
  1. 将生成的class文件拷贝到自定义类加载器的目录中
    在这里插入图片描述
  2. 将HelloWorld类的打印信息改下 后面查看自定义类加载器加载的系统类加载器的class还是其文件目录的class
public class HelloWorld {
    static {
        System.out.println("测试是否会被加载");
    }
    public String welcome(){
        return "测试方法";
    }
}
  1. 编写自定义类加载器测试类
// 测试自定义加载器是否可以加载helloWorld
class MyClassLoaderTest{
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // 情况1 使用自定义类加载器 没有重写loaderClass方法
        MyClassLoader myClassLoader = new MyClassLoader();
        // 当前类的加载器 为系统类加载器(ApplicationClassLoader)即 系统配置的ClassPath路径都可以加载到 jar包等 默认是自定义加载器的父类加载器
        ClassLoader appClassLoader = MyClassLoader.class.getClassLoader();
        // 情况2 可以指定扩展类加载器(ExtClassLoader)做为父类加载器 由纯java代码编写 加载JAVA_HOME下的jre/lb/ext的类库
        ClassLoader ExtClassLoader = appClassLoader.getParent();
        myClassLoader = new MyClassLoader("C:/Java/classloader",ExtClassLoader);
        // 情况3 也可以指定根类加载器(BootstrapClassLoader)为父类加载器  设置为null 就是指定为根类加载器  有C++编写 是用来加载虚拟机核心类库
        myClassLoader = new MyClassLoader("C:/Java/classloader",null);
        // 情况4 使用破坏双亲委派机制的自定义类加载器
        myClassLoader = new BrokerClassLoader();

        // 加载HelloWorld.class 并不会执行static块代码
        Class<?> aClass = myClassLoader.loadClass("com.fenggf.simple.classLoader.HelloWorld");
        System.out.println("------------1-------------");
        System.out.println(aClass.getClassLoader());
        // 初始化对象 才会执行static块代码
        System.out.println("------------2-------------");
        Object helloWorld = aClass.newInstance();
        System.out.println(helloWorld);
        // 使用对象的方法
        System.out.println("------------3-------------");
        Method method = aClass.getMethod("welcome");
        Object result = method.invoke(helloWorld);
        System.out.println("输出结果:"+result);
    }
}

执行情况

情况1 使用自定义类加载器 但没有破坏双亲委派规则 执行系统类加载器找到的class
------------1-------------
sun.misc.Launcher$AppClassLoader@18b4aac2
------------2-------------
测试是否会被加载
com.fenggf.simple.classLoader.HelloWorld@677327b6
------------3-------------
输出结果:测试方法

情况2 指定父类加载器为扩展类加载器  执行自定义加载器目录的class
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@7f31245a
------------3-------------
输出结果:Hello World

情况3 指定父类加载器为根类加载器 执行自定义加载器目录的class
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@7f31245a
------------3-------------
输出结果:Hello World

情况4 使用破坏双亲委派机制的自定义加载器 执行自定义加载器目录的class 
------------1-------------
My ClassLoader
------------2-------------
Hello World Class is Initialized
com.fenggf.simple.classLoader.HelloWorld@6d6f6e28
------------3-------------
输出结果:Hello World

注意:每个类加载器找到class文件时,自己都会存放一份数据,即使相同类加载的不同对象实例,他们都各自存放一份数据(不同class对象)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值