JVM第二课:详解Class加载过程

2022年01月14日 阅读数:4
这篇文章主要向大家介绍JVM第二课:详解Class加载过程,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

类加载和初始化

class cycle

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

  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后才是初始化的值
成员变量的初始化其实也是分两步,第一步申请内存空间时是默认值,第二步调用构造方法时才是初始化的值
局部变量则必须本身设置初始值web

在这里插入图片描述

类加载器 ClassLoader

一个class文件的Loading,load出两块内容:bootstrap

  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加载的缓存

双亲委派

双亲委派并非指父母双方,而是指"查找类时从子到父,加载类时从父到子"的这么一个机制.
在这里插入图片描述安全

具体含义

众所周知,ClassLoader加载完一个类后,会放入一个ClassCache,下次再用时就不需重复加载了.每一个ClassLoader有本身的ClassCacheide

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

父加载器不是类加载器的加载器,也不是类加载器的父类加载器svg

package character02;

public class T004_ParentAndChild {
   
   
    public static void main(String[] args) {
   
   
        System.out.println(T004_ParentAndChild.class.getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent());
        //System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent());
    }
}

为何须要双亲委派

为啥不直接放到一个ClassCache里面,这样就不用层层查找了啊?
这里主要是出于安全考虑.
假设黑客自定义了一个java.lang.String对象,里面作了些非法操做;若是不分层查找的话,用户就会用到他自定义的String,好比输入密码存成String,则会被黑客获取;
双亲委任机制下,使用String时,先看看父加载器是否已加载,直到找到Bootstrap后直接返回String类.

能够打破双亲委派机制吗?

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

类加载器的范围

ClassLoader是Launcher的内部类,具体能够去看一下Launcher的源码
在这里插入图片描述
查看一下具体路径

package character02;

public class T003_ClassLoaderScope {
   
   
    public static void main(String[] args) {
   
   
        String pathBoot = System.getProperty("sun.boot.class.path");
        System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathExt = System.getProperty("java.ext.dirs");
        System.out.println(pathExt.replaceAll(";", System.lineSeparator()));

        System.out.println("--------------------");
        String pathApp = System.getProperty("java.class.path");
        System.out.println(pathApp.replaceAll(";", System.lineSeparator()));
    }
}

从源码角度去理解ClassLoader

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

ClassLoader源码分析

别的方法ClassLoader类已经写好啦(模板设计模式),
ClassLoader的loadClass()源码

	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();
                    // loader的缓存中找不到,须要去实现双亲委派的第二步,加载类时从父到子
                    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对象,若是有,直接返回,没有则调用父加载器的loadClass()方法,看看父加载器是否能找到,若是一直调用到bootstrap加载器的缓存中都没找到,则从bootstrap加载器开始尝试加载该类,若是加载到则返回,不然一直回调到子类尝试加载

其中自定义ClassLoader的关键点就是下面的findClass方法,该方法直接抛异常,是个必须被子类重写的方法.(模板方法,钩子函数)
ClassLoader的findClass()源码

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

自定义一个简单的ClassLoader

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

拓展:

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

自定义ClassLoader实现:
在C盘下面有一个Hello.class文件,而且当前项目没有Hello.java,loadClass时,会先从父加载器开始找,即当父加载器不能加载该类时,就会调用自定义的findClass()方法查找是否能找到

package character02;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
   
   
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
   
   
        File f = new File("C:/", name.replace(".", "/").concat(".class"));
        System.out.println(f.getName());
        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[] bytes = bao.toByteArray();
            //经过这个方法把一个二进制数组转化为一个class对象
            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();
                }
            }
        }
        //找不到就调用父加载器的findClass方法
        return super.findClass(name);
    }

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

LazyLoading

了解便可,不需深究
在这里插入图片描述

加载顺序

  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(这时子类对象建立完毕,先调用父类的代码块和构造器)

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

在这里插入图片描述

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

  • 解释:众所周知,Java是跨平台的语言,JVM在运行时讲class字节码解释为操做系统认识的本地代码去执行
  • 编译:这里编译是指,直接编译成操做系统认识的本地代码,不用JVM在运行时解释了

默认模式是混合模式,就是混合使用解释器加热点代码编译。什么叫热点代码编译?
屡次被调用的方法,屡次被调用的循环进行编译,怎么检测呢?就是用一个计数器,每一个方法上都有一个方法,计数器循环有循环计数器。结果在发现某个方法一秒钟执行了超过某个10万次。我要对他进行编译,拿大腿想想我该怎么办?直接编译成本地代码,再用的话直接用本地的。不用解释器执行了。

这时候会有人问,为何不直接都编译成本地代码呢,执行效率更高,由于Java解释器如今效率已经很是高了,在一些简单的代码上它不属于便器。第二点,若是你有一段代码执行文件,特别特别多各类各样的类库,有时候好几十个class,这是正常的。你上来二话不说先给编译器让他编译,编译的过程会长的吓人,因此如今默认的模式是混合模式,可是彻底能够用参数来指定究竟是什么模式。

这个东西是能够验证的,有一个小程序,能够经过指定不一样的参数,看一下编译模式、解释模式、混合模式的执行速度上的差别。
差很少编译模式和混合模式都在3秒左右,解释模式耗时比较长。
默认是-Xmixed(混合模式) -Xint(纯解释)-Xcmp(纯编译)
检测热点代码:-XX:ComplieThreshold = 10000

package character02;

public class T009_WayToRun {
   
   
    public static void main(String[] args) {
   
   
        for(int i=0; i<10_0000; i++)
            m();

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

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