Java类加载器

一.类的生命周期

Java类加载器_第1张图片

1. 加载(Loading):找 Class 文件

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

这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

2. 验证(Verification):验证格式、依赖

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3. 准备(Preparation):静态字段、方法表

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

这个阶段中有两个容易产生混淆的概念:

a .这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

b. 这里所说的初始值 “通常情况” 下是数据类型的零值,假设一个类变量的定义为:

public static int value =123;

那变量 value 在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把 value赋值为123的 putstatic 指令是程序被编译后,存放于类构造器 clinit() 方法之中,所以把 vaue
赋值为123的动作将在初始化阶段才会执行。

c.上面提到,在“通常情况”下初始值是零值,那相对的会有一些“特殊情况”:如果类字段的字段属性表中存在
ConstantValue属性,那在准备阶段变量 value 就会被初始化为 ConstantValue属性所指定的值,假设上面类变量value的定义变为:

   public static final int value = 123;

则编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为123。

ConstantValue 属性: 只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String(因为从常量池中只能引用到基本类型和String类型的字面量)。

扩展: final、static、static final修饰的字段赋值的区别?
1. static修饰的字段在加载过程中准备阶段被初始化,但是这个阶段只会赋值一个默认的值(0或者null而并非定义变量设置的值)初始化阶段在类构造器中才会赋值为变量定义的值。

2. final修饰的字段在运行时被初始化,可以直接赋值,也可以在实例构造器中赋值,赋值后不可修改。

3. static final修饰的字段在javac编译时生成comstantValue属性,在类加载的准备阶段直接把constantValue的值赋给该字段。可以理解为在编译期即把结果放入了常量池中。

4. 解析(Resolution):符号解析为引用

1.解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,在Class文件中符号引用以 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现:

a.符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

b.直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

2.解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5. 初始化(Initialization):构造器、静态变量赋值、静态代码块

1.类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
2.在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。我们先看一下clinit()方法执行过程中一些可能会影响程序运行行为的特点和细节,这部分相对更贴近于普通的程序开发人员。

()方法

a. ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下代码所示:

public class Test{
    static {
        i = 0;                 // 给变量赋值可以正常编译通过
        System.out.print(i);   // 这句编译器会提示"非法向前引用"
    }
    static int i = 1;
}

b. clinit()方法与类的构造函数(或者说实例构造器init()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的clinit()方法执行之前,父类的clinit()方法已经执行完毕。因此在虚拟机中第一个被执行的clinit()方法的类肯定是java.lang.Object。

c.由于父类的clinit()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如在代码清单中,字段B的值将会是2而不是1。

public class Clinit {
    
    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);
    }

}

d.clinit()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。

e.接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法。但接口与类不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。

f.虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果在一个类的clinit()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

6. 使用(Using)

7. 卸载(Unloading)

二.类的加载时机

1.会初始化的一些场景

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

2.可能会加载但不会初始化的一些场景

1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4. 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。
5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。(Class.forName”jvm.Hello”) 默认会加载
Hello 类。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)。

三.三类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器: 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

a. 启动类加载器(BootstrapClassLoader)

这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

b. 扩展类加载器(ExtClassLoader)

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

c. 应用类加载器(AppClassLoader)

这个类加载器由sun.misc.Launcher$App-Classloader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自已的类加载器,一般情况下这个就是程序中默认的类加载器。

四.加载器特点

类加载器的逻辑在 loadClass(String name, boolean resolve)中

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

1. 双亲委托:

Java类加载器_第2张图片

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

2. 负责依赖

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

3. 缓存加载

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

五.自定义类加载器

1. 重写findClass()方法

如果类没有被加载并且父类加载器加载失败则会调用自己的findClass()方法完成加载,重写 findClass() 保证了双亲委派规则,原始的 findClass()直接抛出一个异常, 说明就是等着我们去重写

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

2. 重写 findClass() 加载 base64 形式的类

2.1 自定位 java 类

package com.ashen.game.base64;

/**
 *  测试类, 编译后, 做加密处理
 *  javac -encoding UTF-8 HelloWord.java
 * @author HY
 */
public class Test {

    static{
        System.out.println("大家好,我是练习时长两年半的个人练习生,我喜欢唱、跳、rap、篮球、music!");
    }

}

2.2 进行编译:

javac -encoding UTF-8 HelloWord.java

2.3 然后用工具或者在linux或者mac上执行如下命令, 生成 base64 字符串

base64 HelloWord.class

2.4 自定义类加载器

package com.ashen.game.base64;

import java.util.Base64;

/**
 *  自定义类加载器. 加载 base64 类型的类
 * @author HY
 */
public class Base64ClassLoader extends ClassLoader {

    public static void main(String[] args) throws Exception {
        new Base64ClassLoader().findClass("com.ashen.game.base64.Test").newInstance();
    }

    @Override
    protected Class<?> findClass(String name) {
        String helloBase64 = "yv66vgAAADQAHAoABgAOCQAPABAIABEKABIAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAcACAcAFgwAFwAYAQBo5aSn5a625aW977yM5oiR5piv57uD5Lmg5pe26ZW/5Lik5bm05Y2K55qE5Liq5Lq657uD5Lmg55Sf77yM5oiR5Zac5qyi5ZSx44CB6Lez44CBcmFw44CB56+u55CD44CBbXVzaWPvvIEHABkMABoAGwEAGmNvbS9hc2hlbi9nYW1lL2Jhc2U2NC9UZXN0AQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACAABAAkAAAAdAAEAAQAAAAUqtwABsQAAAAEACgAAAAYAAQAAAAgACAALAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAKAAAACgACAAAACwAIAAwAAQAMAAAAAgAN";
        byte[] bytes = decode(helloBase64);
        // 从byte数组中还原class
        return defineClass(name,bytes,0,bytes.length);
    }

    public byte[] decode(String base64) {
        return Base64.getDecoder().decode(base64);
    }
}

输出:

大家好,我是练习时长两年半的个人练习生,我喜欢唱、跳、rap、篮球、music!

3. 加密class文件,重写 findClass() 加载加密文件

3.1 自定义 java 类

package com.ashen.game.secret;

/**
 *  测试类, 编译后, 做加密处理
 * @author HY
 */
public class Test {

    /**
     *  测试方法  javac -encoding UTF-8 HelloWord.java
     */
    public void test(){
        System.out.println("鸡你太美!baby 鸡你太美!baby 鸡你实在是太美!baby 鸡你太美!baby 迎面走来的你让我如此蠢蠢欲动!这种感觉我从未有!Cause I got a crush on you who you!");
    }

}

3.2 进行编译

javac -encoding UTF-8 Test.java

3.3 加密处理

package com.ashen.game.secret;

import java.io.*;

/**
 *  测试类, 编译后, 做加密处理
 * @author HY
 */
public class Secret {

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

        String path = "E:/class-loader/src/main/java/com/ashen/game/secret/";
        String className = "Test.class";
        String secretName = "Test.zlass";

        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            File file = new File(path +className);
            inputStream = new FileInputStream(file);
            // 读取数据
            int length = inputStream.available();
            byte[] byteArray = new byte[length];
            inputStream.read(byteArray);
            // 对数组进行加密操作
            byte[] classBytes = decode(byteArray);

            // 将转换后的数组输出到自定义文件中
            outputStream = new FileOutputStream(path + secretName);
            InputStream is = new ByteArrayInputStream(classBytes);
            byte[] buff = new byte[1024];
            int len = 0;
            while((len=is.read(buff))!=-1){
                outputStream.write(buff, 0, len);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(inputStream);
            close(inputStream);
        }

    }


    // 加密
    private static byte[] decode(byte[] byteArray) {
        byte[] targetArray = new byte[byteArray.length];
        for (int i = 0; i < byteArray.length; i++) {
            targetArray[i] = (byte) (255 - byteArray[i]);
        }
        return targetArray;
    }


    // 关闭
    private static void close(Closeable res) {
        if (null != res) {
            try {
                res.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

3.4 自定义类加载器解密加载

package com.ashen.game.secret;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

/**
 *  自定义类加载器, 加载一个 Test.xlass 文件,执行 test 方法,此文件内容是一个 Test.class 文件所有字节(x=255-x)处理后的文件
 * @author HY
 */
public class XlassLoader extends ClassLoader {

    public static void main(String[] args) throws Exception {
        // 相关参数
        final String className = "com.ashen.game.secret.Test";
        final String methodName = "test";
        // 创建类加载器
        ClassLoader classLoader = new XlassLoader();
        Class<?> clazz = classLoader.loadClass(className);

        // 创建对象
        Object instance = clazz.getDeclaredConstructor().newInstance();
        // 调用实例方法
        Method method = clazz.getMethod(methodName);
        method.invoke(instance);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        // 文件后缀
        final String suffix = "Test.zlass";
        // 获取输入流
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(suffix);
        try {
            // 读取数据
            int length = inputStream.available();
            byte[] byteArray = new byte[length];
            inputStream.read(byteArray);
            // 转换
            byte[] classBytes = decode(byteArray);
            // 从byte数组中还原class
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        } finally {
            close(inputStream);
        }
    }

    // 解码
    private static byte[] decode(byte[] byteArray) {
        byte[] targetArray = new byte[byteArray.length];
        for (int i = 0; i < byteArray.length; i++) {
            targetArray[i] = (byte) (255 - byteArray[i]);
        }
        return targetArray;
    }

    // 关闭
    private static void close(Closeable res) {
        if (null != res) {
            try {
                res.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

鸡你太美!baby 鸡你太美!baby 鸡你实在是太美!baby 鸡你太美!baby 迎面走来的你让我如此蠢蠢欲动!这种感觉我从未有!Cause I got a crush on you who you!

你可能感兴趣的:(Java,java,类加载器,jvm)