Fork me on GitHub

类加载和初始化

类加载过程

image

  • 加载时机
    • 使用new实例化对象时,读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时,调用静态方法时。
    • 对内进行反射调用时。
    • 当初始化一个类时,如果父类没有进行初始化,需要先初始化父类。
    • 启动程序所使用的main方法所在类。
    • 当使用1.7的动态语音支持时。
  • 加载阶段
    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 连接阶段
    • 文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
    • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
    • 符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
  • 准备
    • 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
    • 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
  • 解析
    • 将常量池中的符号引用替换为直接引用(内存地址)的过程,同时将class字节码文件翻译成机器码。

符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。

  • 初始化
    • 为类的静态变量赋初值。
  • 卸载阶段
    • 执行了System.exit()方法。
    • 程序正常执行结束。
    • 程序在执行过程中遇到了异常或错误而异常终止。
    • 由于操作系统出现错误而导致Java虚拟机进程终止。

双亲委派机制

名词解释

  • Bootstrap classLoader
    主要负责加载核心的类库(java.lang.*等)。
  • ExtClassLoader
    主要负责加载jre/lib/ext目录下的一些扩展的jar。
  • AppClassLoader
    主要负责加载应用程序的主函数类。

代码

public class OneselfClassLoader extends ClassLoader{

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

        //查找文件
        File file=new File("D:/"+name.replace(".","/").concat(".class"));
        try {
            FileInputStream stream=new FileInputStream(file);
            byte[] b=new byte[1024];
            int c=0;
            StringBuilder builder=new StringBuilder();
            while ((c=stream.read(b))!=-1){
                builder.append(new String(b,0,c,"utf-8"));
            }
            stream.close();
            byte[] bytes = builder.toString().getBytes();
            //把二进制流转换为Class对象
            return defineClass(name,bytes,0,bytes.length);
        }catch (Exception e){
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    public void demo() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //创建类加载器
        ClassLoader loader=new OneselfClassLoader();
        //进行加载
        Class<?> aClassOne = loader.loadClass("com.yxkj.jvm.basic.HelloWorld");
        ClassLoader loaderTwo=new OneselfClassLoader();
        Class<?> aClassTwo = loaderTwo.loadClass("com.yxkj.jvm.basic.HelloWorld");
        HelloWorld helloWorld=(HelloWorld)aClassOne.newInstance();
        helloWorld.demo();
        System.out.println("-------->"+(aClassTwo==aClassOne));
    }
}

Hello.class加载过程

不考虑我们自定义类加载器,Hello.class这样的文件首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了。这时候BootstrapClassLoader开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层或加载到内存中,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

Hello.class加载流程图

image

自定义类加载器

重写findClass方法

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

        //查找文件
        File file=new File("D:/"+name.replace(".","/").concat(".class"));
        try {
            FileInputStream stream=new FileInputStream(file);
            byte[] b=new byte[1024];
            int c=0;
            StringBuilder builder=new StringBuilder();
            while ((c=stream.read(b))!=-1){
                builder.append(new String(b,0,c,"utf-8"));
            }
            stream.close();
            byte[] bytes = builder.toString().getBytes();
            return defineClass(name,bytes,0,bytes.length);
        }catch (Exception e){
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    public void demo() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //创建类加载器
        ClassLoader loader=new OneselfClassLoader();
        //进行加载
        Class<?> aClassOne = loader.loadClass("com.yxkj.jvm.basis.HelloWorld");
        ClassLoader loaderTwo=new OneselfClassLoader();
        Class<?> aClassTwo = loaderTwo.loadClass("com.yxkj.jvm.basis.HelloWorld");
        HelloWorld helloWorld=(HelloWorld)aClassOne.newInstance();
        helloWorld.demo();
        System.out.println("-------->"+(aClassTwo==aClassOne));
    }
  • 输出
HelloWorld
-------->true

使用场景:加密:众所周知,java代码很容易被反编译,如果你需要把自己的代码进行加密,可以先将编译后的代码用某种加密算法加密,然后实现自己的类加载器,负责将这段加密后的代码还原。
从非标准的来源加载代码:例如你的部分字节码是放在数据库中甚至是网络上的,就可以自己写个类加载器,从指定的来源加载类。
动态创建:为了性能等等可能的理由,根据实际情况动态创建代码并执行。

打破双亲委派机制

重写loadClass方法

  • 代码
public static class MyLoader extends ClassLoader{
        /**
         * @Description:
         * @Author: zhuyang
         * @Date: 2022/1/21 10:55
         * @param name:
         * @return: java.lang.Class<?>
         *     通过自定义的MyLoad类加载通过一个class文件,判断加载后的结果,发现不相等!说明class文件被重新加载进内存了!
         *     按照类加载机制,如果某一个clas文件已经被加载进内存后是无法再重新被加载的,
         *     此时说明,已经打破了双亲委派的加载机制。
         **/
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            //查找文件
            File file=new File("D:/"+name.replace(".","/").concat(".class"));
            if(!file.exists()) return super.loadClass(name);
            try {
                InputStream stream=new FileInputStream(file);
                byte[] b=new byte[stream.available()];
                stream.read(b);
                stream.close();
                return defineClass(name,b,0,b.length);
            } catch (Exception e) {
                e.printStackTrace();
            }

            return super.loadClass(name);
        }
    }


    public void demo() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //创建类加载器
        MyLoader loader=new MyLoader();
        //进行加载
        Class<?> aClassOne = loader.loadClass("com.yxkj.jvm.basis.HelloWorld");
        ClassLoader loaderTwo=new MyLoader();
        Class<?> aClassTwo = loaderTwo.loadClass("com.yxkj.jvm.basis.HelloWorld");
        System.out.println("-------->"+(aClassTwo==aClassOne));
    }
  • 输出
-------->false

Gitee地址

https://gitee.com/zhuayng/foundation-study/tree/develop/JavaBasis/JVM/src/main/java/com/yxkj/jvm/basis

参考

https://blog.csdn.net/u013412772/article/details/80837735

posted @ 2020-05-03 21:14  晨度  阅读(2219)  评论(0编辑  收藏  举报