JVM系列(二)[Class加载过程,双亲委派机制,自定义一个ClassLoader,LazyLoading,父子类构造器加载顺序,混合型-解释&编译]

文章目录

  • 类加载和初始化
    • class cycle
    • 类加载器ClassLoader
      • 求证一下BootStrap,Ext,App都加载哪些类
    • 双亲委派
      • 是什么
      • 为什么
      • 可以打破双亲委派机制吗?
    • 从源码角度去理解ClassLoader
      • ClassLoader简单读源码
      • 查实自定义一个简单的Class Loader
    • LazyLoading & 父子类的构造器加载顺序
      • 加载顺序的问题:
    • java是解释型还是编译型语言?

类加载和初始化

class cycle

首先一个class文件在硬盘里面
然后JVM去对它进行以下行为:

  1. Loading,把class文件load到内存,双亲委派(安全)
  2. Linking,分三小步:
    1. verification,校验文件是否复合JVM规定的class格式.
    2. preparation,静态变量赋默认值,比如int-0,double-0.0,boolean-false
    3. resolution,解析,
      将类、方法、属性等符号引用解析为直接引用
      如常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
  3. Initializing,初始化,这时候静态变量赋初始值(代码指定的值),调用静态代码块

一般我们只要记住,静态变量的初始化分为两步,Linking时是默认值,Initializing后才是初始化的值
成员变量的初始化其实也是分两步,第一步申请内存空间时是默认值,第二步调用构造方法时才是初始化的值

类加载器ClassLoader

一个class文件的Loading,load出两个东西:
1.class文件的二进制编码加载到内存
2.生成一个与之对应的Class对象,改对象指向内存中的class编码文件

如果打印一下String的ClassLoader.会发现结果为null:
System.out.println(String.class.getClassLoader());

这是因为:
最顶层的加载器Bootstrap是用C++来实现的,在JAVA中没有与之对应的类.
所以Bootstrap加载出来的类,比如String,获取到的ClassLoader是null.

类加载器的分层关系:

注意
1.这个上层加载器,即父加载器,是逻辑上的关系,其实就是一个成员变量
2.不是类的继承关系,那是另一种维度的关系
3.加载器也是一个对象,也要由另一个加载器加载,但并不一定是由他的parent加载,是谁不一定.最终都是由Bootstrap加载的
设一个加载器a的上层加载器是b,那么 a不一定是被b加载的

求证一下BootStrap,Ext,App都加载哪些类

其实AppClassLoader和ExtClassLoader都是sun.misc.Launcher的内部类
(JDK11把Launcher换成了ClassLoaders)
Launcher里面可以看到这三个内部类,和他们各自加载的path
BootClassPathHolder:System.getProperty(“sun.boot.class.path”)
AppClassLoader:java.class.path
ExtClassLoader:java.ext.dirs

我们写个方法打印一下这些东西就看出来了:

	public static void main(String[] args) {
        System.out.println("boot-------------------");
        System.out.println(System.getProperty("sun.boot.class.path").replaceAll(":", System.lineSeparator()));
        System.out.println("ext-------------------");
        System.out.println(System.getProperty("java.ext.dirs").replaceAll(":", System.lineSeparator()));
        System.out.println("classpath-------------------");
        System.out.println(System.getProperty("java.class.path").replaceAll(":", System.lineSeparator()));
    }

双亲委派

是什么

双亲委派并不是指父母双方,而是指"查找类时从子到父,加载类时从父到子"的这么一个机制.

具体含义:
众所周知,ClassLoader加载完一个类后,会放入一个ClassCache,下次再用时就不需重复加载了.每个ClassLoader有自己的ClassCache

  • 当我们需要找一个类时,会先交给最下层的ClassLoader,在ClassCache找,如果找到了就返回结果,如果找不到就交给上层加载器,上层加载器进行同样的操作,直到Bootstrap.
  • 真正去加载这个类的时候,会自上到下开始加载.
    每个ClassLoader先看自己管辖的类里面有没有需要加载的class,如果有就加载返回,如果没有就交给下一层去加载.
    如果都没有就抛异常CLassNotFoundException.

为什么

为啥不直接放到一个ClassCache里面,这样就不用层层查找了啊?
这里主要是出于安全考虑.
假设黑客小明自定义了一个java.lang.String对象,里面做了些非法操作;如果不分层查找的话,用户就会用到他自定义的String,阴谋得逞;
双亲委任机制下,使用String时,先看看父加载器是否已加载,直到找到Bootstrap后直接返回String类.

可以打破双亲委派机制吗?

可以,自定义一个classLoader,重写loadClass方法就可以打破.
热加载/热部署的时候,可能会重写loadClass(),打破双亲委派机制

从源码角度去理解ClassLoader

  1. 继承ClassLoader(这里用到了模板方法设计模式)
  2. 重写模板方法findClass,调用defineClass
  3. 自定义类加载器 加载 加密的class,防止反编译和篡改

ClassLoader简单读源码

别的方法ClassLoader类已经写好啦(模板设计模式),
关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)

	protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 这个查找调用了native方法,具体可能要看HotSpot或者其他JVM源码了,可以理解为一个"缓存"
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                // 然后调用parent的loadClass,parent也是先检查下是否已加载
                // 然后调用parent.parent.loadClass或者findBootstrapClassOrNull
                // 这里体现了双亲委派的第一步,查找类时从子到父
                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();
                    // 这里需要各ClassLoader实现,需要去实现双亲委派的第二步,加载类时从父到子
                    c = findClass(name);

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

查实自定义一个简单的Class Loader

  • 如果Load的class本项目空间已经有啦,那么就不会走自定义的findClass方法了,而是直接由Launcher$AppClassLoader加载出来了
  • 所以我们自定义一个CLassLoader,一般是加载一个其他地方的class,比如从RPC服务中获取
  • 远程传输class文件一般会对对class加密,拿到class文件的字节数组后再解密;
    最简单的是对方发送时对一个token做异或(xor,^)运算,我们拿到后再对那个token做异或即可解密.

CompilerAPI 动态编译,直接在内存中完成编译源码和加载Class,不需要经过硬盘了

我这里com.example.springboot2.test.Hello是另一个项目空间的类,test.Hi是自己的项目空间的类

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("MyClassLoader.findClass");
        String parent = "/Users/liweizhi/IdeaProjects/wzdemo/springboot2/target/classes/";
//        String parent = "/Users/liweizhi/IdeaProjects/wzdemo/demo1/target/classes";
        File f = new File(parent, name.replace(".", "/").concat(".class"));
        FileInputStream fis = null;
        ByteArrayOutputStream bao = null;
        try {
            fis = new FileInputStream(f);
            bao = new ByteArrayOutputStream();

            /*int b = 0;
            while ((b = fis.read()) != -1) {
                bao.write(b);
            }*/

            byte[] buffer = new byte[1024];
            for (int len = 0; (len = fis.read(buffer)) != -1; ) {
                bao.write(buffer, 0, len);
            }

            byte[] bytes = bao.toByteArray();

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bao != null) {
                try {
                    bao.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return super.findClass(name);
    }

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader classLoader = new MyClassLoader();
//        String className = "com.example.springboot2.test.Hello";
        String className = "test.Hi";
        Class<?> aClass = classLoader.loadClass(className);
        System.out.println(aClass.getClassLoader());
    }
}

LazyLoading & 父子类的构造器加载顺序

  • Loading,把class文件load到内存;Initializing初始化
  • JVM规范并没有规定什么时候加载
  • 但是规定了什么时候必须初始化:
    1. new getstatic putstatic invokestatic指令,访问final变量除外
    2. java.lang.reflect对类进行反射调用时
    3. 初始化子类的时候,父类首先初始化
    4. 虚拟机启动时,被执行的主类必须初始化
    5. 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

这里涉及到一个加载顺序的问题

加载顺序的问题:

  1. final static修饰的变量,在类加载前就初始化好了,访问它不需要初始化类.
  2. 访问静态变量时需要加载类,先加载父类执行父类的静态代码块,再加载自己执行自己的静态代码块.
  3. 如果有非静态代码块或者构造器的内的代码,整体的顺序是:
    1. Parent static block
    2. Child static block(这时子类加载完毕,先加载父类)
    3. Patent block
    4. Patent constructor
    5. Child block
    6. Child constructor(这时子类对象创建完毕,先调用父类的代码块和构造器)
public class InitializingTest {
    public static void main(String[] args) throws ClassNotFoundException {
//        System.out.println(Parent.a); // 先加载Parent,然后打印a
//        System.out.println(Parent.s);
//        System.out.println(Child.q); // 先加载Parent, 后加载Child, 然后打印q
//        System.out.println(Child.w);
//            Class.forName("com.example.demo.jvm.InitializingTest$Parent");
        // 这个和打印Child.q时一样, 其实就是当第一次打印Child.q,去加载了
//        Class.forName("com.example.demo.jvm.InitializingTest$Child");

        Child child = new Child();
    }

    static class Parent {
        static int a = 1;
        static final int s = 2;

        static {
            System.out.println("Parent static block");
        }

        {
            System.out.println("Patent block");
        }

        public Parent() {
            System.out.println("Patent constructor");
        }

    }

    static class Child extends Parent {
        static int q = 3;
        static final int w = 4;

        static {
            System.out.println("Child static block");
        }


        {
            System.out.println("Child block");
        }

        public Child() {
            System.out.println("Child constructor");
        }
    }
}

java是解释型还是编译型语言?

答:默认是混合模式,解释器+JIT,当某个方法调用很频繁时就走JIT
也可以指定为单纯的解释性/编译型

  • 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操作系统认识的本地代码去执行
  • 编译:这里编译是指,直接编译成操作系统认识的本地代码,不用JVM在运行时解释了
  • 解释器: bytecode interpreter
  • JIT: Just In-Time compiler
  • 混合模式:混合使用解释器 + 热点代码编译器
    1. 其实阶段是解释执行
    2. 当一个方法或者循环指令的调用频率很高时,这部分代码由JIT进行编译,下次执行就不用解释了,提高速度
  • 可以通过JVM参指定模式:
    1. -Xmixed 混合模式,默认
    2. -Xint 解释模式,启动快,执行慢
    3. -Xcomp 编译模式,启动慢,执行快

搞个测试类,求证一下:

我的电脑上,默认大概五六秒完成;
加了-Xmixed后还是五六秒;
加了-Xint后,过了很久还没完事,不等了,反正时间很长了
加了-Xcomp后,四五秒完事,比混合模式稍微快一些;但是前面编译时间略久,会产生运行比混合模式还慢的感觉.

public class T009_WayToRun {
    static int count = 10_0000;

    public static void main(String[] args) {
        for (int i = 0; i < count; i++) {
            m();
        }

        long start = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            m();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void m() {
        for (long i = 0; i < count; i++) {
            long j = i % 3;
        }
    }
}

你可能感兴趣的:(JVM)