Java基础 (20) 类加载

1)类加载机制谈谈对ClassLoader(类加载器)的理解
2)JVM加载时机与加载过程
3)对象创建过程

一. ClassLoader 类加载原理

它主要工作在 Class装在的加载阶段,其主要作用是从系统外部获得 Class 二进制数据流。它是Java的核心组件,所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader 负责通过将 Class 文件里的二进制数据流装载进系统,然后交给 Java 虚拟机进行连接、初始化等操作。

1.1 ClassLoader的种类
  • BootStarpClassLoader:C++编写,加载核心类 java.*
  • ExtClassLoader:Java编写,加载扩展库 javax.*
  • AppClassLoader:Java编写,加载程序所在目录
  • 自定义ClassLoader:Java编写,定制化加载
三种类加载器:Bootstrap、Extension、System

JVM加载的时候默认采用了双亲委派的类加载机制。


public Class loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  

protected synchronized Class loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  

    // 首先判断该类型是否已经被加载 
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
        try {  
            if (parent != null) {  
                //如果存在父类加载器,就委派给父类加载器加载  
                c = parent.loadClass(name, false);  
            } else {    // 递归终止条件
                // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
                // parent == null就意味着由启动类加载器尝试加载该类,  
                // 即通过调用 native方法 findBootstrapClass0(String name)加载  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
            // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  

自定义类加载器
关键函数

protected Class findClass(String name) throws ClassNotFoundException{
        throw new ClassNotFoundException(name);
}

protected final Class defineClass(byte[] b,int off, int len) throws ClassFormatError{
        return   defineClass(null, b, off, len, null);
}

测试用的MyClassLoader

public class MyClassLoader extends ClassLoader {
    static {
        System.out.println("MyClassLoader");
    }
    public static final String driver = "/Users/oneball/Downloads/sample/src/classloader/";
    public static final String fileTyep = ".class";

    public Class findClass(String name) {
        byte[] data = loadClassData(name);
        return defineClass(data, 0, data.length);
    }

    public byte[] loadClassData(String name) {
        FileInputStream fis = null;
        byte[] data = null;
        try {
            File file = new File(driver + name + fileTyep);
            System.out.println(file.getAbsolutePath());
            fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int ch = 0;
            while ((ch = fis.read()) != -1) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("loadClassData-IOException");
        }
        return data;
    }
}
public class ClassLoaderChecker {

    public static void main(String[] args) {
        MyClassLoader cl1 = new MyClassLoader();
        try {
            Class c1 = cl1.loadClass("Wali");
            Object object = c1.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            System.out.println("main-ClassNotFoundException");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

1、文件系统类加载器

public class FileSystemClassLoader extends ClassLoader {  

    private String rootDir;  

    public FileSystemClassLoader(String rootDir) {  
        this.rootDir = rootDir;  
    }  

    // 获取类的字节码  
    @Override  
    protected Class findClass(String name) throws ClassNotFoundException {  
        byte[] classData = getClassData(name);  // 获取类的字节数组  
        if (classData == null) {  
            throw new ClassNotFoundException();  
        } else {  
            return defineClass(name, classData, 0, classData.length);  
        }  
    }  

    private byte[] getClassData(String className) {  
        // 读取类文件的字节  
        String path = classNameToPath(className);  
        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();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  

    private String classNameToPath(String className) {  
        // 得到类文件的完全路径  
        return rootDir + File.separatorChar  
                + className.replace('.', File.separatorChar) + ".class";  
    }  
}  
1.2 为什么要使用双亲委派机制去加载类

避免多份同样字节码的加载

31.3 类的加载方式
  • 隐式加载:new
  • 显式加载:loadClass,forName等

loadClass和forName的区别

  • Class.forName得到的class是已经初始化完成的
  • ClassLoader.loadClass得到的class是还没有链接的
二. JVM加载时机与加载过程

JVM类加载机制主要包括两个问题:类加载的时机与步骤类加载的方式

1、加载(Loading)
在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情:
(1) 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3) 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

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

  • 文件格式验证:验证字节流是否符合Class文件格式的规范(例如,是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型)
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了java.lang.Object之外);
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
  • 符号引用验证:确保解析动作能正确执行。
      验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3、准备(Preparation)
  准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
4、解析(Resolution)
 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
5、初始化(Initialization)
类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。是执行类构造器()方法的过程。 类构造器本质上是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器()。
虚拟机会保证一个类的类构造器 () 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

总结:类的装载过程
加载:通过Classloader加载class文件字节码,生成Class对象
链接 下面三步
校验:检查加载的class的正确性和安全性
准备: 为类变量分配存储空间并设置类变量初始值
解析:JVM将常量池的符号引用转换为直接引用
初始化:执行类变量赋值和静态代码块。

在Java中, 创建一个对象常常需要经历如下几个过程:
父类的类构造器() ->
子类的类构造器() ->
父类的成员变量和实例代码块 ->
父类的构造函数 ->
子类的成员变量和实例代码块 ->
子类的构造函数。

实例:

public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {   //静态代码块
        System.out.println("1");
    }

    {       // 实例代码块
        System.out.println("2");
    }

    StaticTest() {    // 实例构造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("4");
    }

    int a = 110;    // 实例变量
    static int b = 112;     // 静态变量
}

答案: 2
3
a=110,b=0
1
4

首先执行了

static StaticTest st = new StaticTest();

实例初始化完全发生在静态初始化之前,当然,这也是导致a为110b为0的原因。

三. 对象创建过程

当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量 及其 从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。
在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。

public class Student implements Cloneable, Serializable {

    private int id;

    public Student() {

    }

    public Student(Integer id) {
        this.id = id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }

    @Override
    public String toString() {
        return "Student [id=" + id + "]";
    }

    public static void main(String[] args) throws Exception {

        System.out.println("使用new关键字创建对象:");
        Student stu1 = new Student(123);
        System.out.println(stu1);
        System.out.println("\n---------------------------\n");


        System.out.println("使用Class类的newInstance方法创建对象:");
        Student stu2 = Student.class.newInstance();    //对应类必须具有无参构造方法,且只有这一种创建方式
        System.out.println(stu2);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Constructor类的newInstance方法创建对象:");
        Constructor constructor = Student.class
                .getConstructor(Integer.class);   // 调用有参构造方法
        Student stu3 = constructor.newInstance(123);   
        System.out.println(stu3);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Clone方法创建对象:");
        Student stu4 = (Student) stu3.clone();
        System.out.println(stu4);
        System.out.println("\n---------------------------\n");

        System.out.println("使用(反)序列化机制创建对象:");
        // 写对象
        ObjectOutputStream output = new ObjectOutputStream(
                new FileOutputStream("student.bin"));
        output.writeObject(stu4);
        output.close();

        // 读取对象
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                "student.bin"));
        Student stu5 = (Student) input.readObject();
        System.out.println(stu5);

    }
}

实例变量初始化、实例代码块初始化 以及 构造函数初始化。

https://blog.csdn.net/justloveyou_/article/details/72466105
https://blog.csdn.net/justloveyou_/article/details/72217806
https://blog.csdn.net/justloveyou_/article/details/72466416

你可能感兴趣的:(Java基础 (20) 类加载)