java虚拟机学习-03 | Java虚拟机是如何加载Java类的?

       https://time.geekbang.org/column/article/11523

       java的语言类型分为基本类型(primitive types)和引用类型(reference types),其中引用类型还细分为接口,类,数组类和泛型参数. 其中泛型会在编译过程中被擦除. 因此java虚拟机中实际上只有三种,在类,接口,数组类中,数组类由虚拟机直接生成,其他两种都具有对应的字节流.

        字节流:最常见的是由编译器直接生成的,除此还可以在程序内部直接生成或者从网页中获取(例如网页中内嵌的小程序 Java applet)字节流

以下是类加载的步骤:

加载

        加载指查找字节流,并根据这创建类的过程. 对于除了数组类的其他类,虚拟机需要通过类加载器来完成查找字节流的过程

        虚拟机中除了启动类加载器(boot class loader)是由C++实现的,其余的类加载器都是java.lang.classloader的子类,需要由其他的类加载器加载到虚拟机中,比如启动类加载器.

        类加载器加载字节流时有一个双亲委派模型,指每个类加载器接收到加载请求时,它会将请求转给父-类加载器,在父-类加载器没找到所请求的类,该加载类才会去进行加载,当该类加载器还找不到的情况下会直接报ClassNotFountExption,不会再往下寻找子-类加载器

      在java9之前:

      启动类加载器(boot class loader)负责加载最基础,最重要的类,比如存放在JRE下lib目录下jar包的类(以及有虚拟机参数Xbootclasspath指定的类).

     扩展类加载器(ext class loader)的父-类加载器是启动类加载器,负责加载相对次要但又通用的类,比如放在JRE的lib/ext下的jar包中的类(以及由系统变量java.ext.dirs指定的类)

     应用类加载器(app class loader)的父-类加载器是扩展类加载器.负责加载应用程序路径下的类(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的.

     在java9的时候引入模块系统,并且扩展类加载器改名为平台类加载器(platform class loader),除了少数的几个关键模块,例如java.base下的类是有启动类加载器加载,其余模块都由平台类加载器加载.

     在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

链接

       链接,是值将创建成的类合并到虚拟机中,使之能够执行的过程,可以分为验证,准备,以及解析三个阶段

       验证阶段的目的在于确保被加载类能够Java虚拟机的约束,通常Java编译器生成的Class文件必然符合Java虚拟机规范

       准备阶段的目的在于为被加载类的静态字段分配内存. Java代码中队静态字段的具体初始化在初始化阶段完成

       解析阶段的目的在于将这些符号引用解析成为实际引用.如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化.)

      Java 虚拟机规范并没有要求在链接过程中完成解析,它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析.

初始化

      在Java代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对它赋值

     如果直接赋值的静态代码块被final修饰,并且它的类型时基本类型或者字符串时,该字段就会被Java编译器标记为常量值(ConstantValue), 其初始化由Java虚拟机完成. 除此之外的直接复制操作以及所有的静态代码块中的代码都会被Java编译器归到同一个方法中,命名为<clinit>.

     类加载的最后一步是初始化,便是为标记为常量值得字段赋值,以及执行<clinit>方法的过程,Java虚拟机会通过加锁来保证<clinit>只被执行一次.

     只有在初始化完成后,类才真正成为可执行状态

     JVM规范枚举了以下多种触发类初始化的情况:

  1. 当虚拟机启动时,初始化用户指定的主类
  2. 当遇到用以新建目标类实例时new指令时,初始化new指令的目标类
  3. 当遇到调用静态方法时,初始化该静态方法所在的类
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类
  5. 子类的初始化会触发父类的初始化
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化都会触发接口的初始化
  7. 用反射API对某个类进行反射调用时,初始化这个类
  8. 当初次调用MethodHandle(句柄)实例时,初始化该MethodHandle(句柄)指向方法所在的类

自定义类加载器

package com.chenkx.baize_server.StreamStudy;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class ClassLoadStu extends ClassLoader {
    public ClassLoadStu(){}
    public ClassLoadStu(ClassLoader parent){
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("自定义类加载器");
        File file = new File("D://study//"+name+".class");
        try {
            byte[] bytes = getBytes(file);
            return this.defineClass(name,bytes,0,bytes.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private byte[] getBytes(File file) throws IOException {
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true){
            int i = fc.read(by);
            if(i==0 || i==-1){
                break;
            }
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_34332035/article/details/86717820