JVM - 类加载器

480 阅读8分钟

类加载

java从编码到执行

javac 编译 java ClassLoader 加载类 Class 字节码解释器 、JIT即时编译器 执行引擎

 JVM 包含 ClassLoader 字节码解释器 JIT 即时编译器 和执行引擎 所以 只要能编译成Class文件且遵守JVM标准的都能被JVM执行  kotlin  scala spark 等之类

什么是Class

Class文件是一堆字节码 二进制字节流 可以使用16进制查看Class文件 包含以下内容

 统一标识符、文件版本号 、class名称、 常量池、父类、实现接口数、实现接口列表、访问修饰符(比如 finalpublicprivate等..)、成员变量等等之类的 可以通过javap -v class文件 查看 或者安装插件JClass Lib
 
 比如前面统一标识符 CA FE BA BE  
 还有 00 00 00 34 文件版本号 16进制

官方文档中对Class的说明 有兴趣的可以自己去看官方文档

A class file consists of a single ClassFile structure:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
The items in the ClassFile structure are as follows:

Class加载过程

分为三个大阶段

  1. 加载(这里的加载指加载Class文件)
  2. Linking(链接)
  3. 初始化Class

image.png Loading Linking Initializing

Loading

Loading 阶段呢其实就是将对应的Class文件从存储区域读取到内存中的一个步骤 ClassLoader中的findClass查找对应的Class 将其加载到内存中

Linking

Linking阶段中分为三个小阶段

verification

verification主要是校验Class文件的合法性 校验对应的Class是否是一个正常的Class文件

preparation

preparation 是一个准备阶段,将对应Class文件中的静态变量的一些字段 赋予默认值 比如一个static int a = 8 这个时候a将被赋值为0

resolution

resolution 解析Class文件 将Class文件中对应的符号引用 转换为直接引用 这一块其实就是上一篇所说的方法区的数据装载以及动态方法调用的虚方法表[VTable]

initialization

initialization 初始化对应的Class,在Class文件如果拥有static修饰的变量 或者 静态代码块这个时候Class文件会生成一个叫<clinit>的方法,在初始化阶段主要是指向clinit方法 初始化Class文件,比如刚刚说的static int a = 8这时候a就等于8

image.png

ClassLoader 类加载器

一般分为两大类

一类为JVM虚拟机实现 BootstarpClassLoader 引导加载器

一类为Java中继承ClassLoader实现 ExtClassLoader AppClassLoader 自定义ClassLoader

image.png Bootstrap:
加载 resources.jar 、rt.jar 、charset.jar 等核心类 C/C++ 实现
一般获取getClassloader 返回null 就是被Bootstrap加载进来的。
Bootstrap 不是继承ClassLoader实现,Bootstrap是最底层的类加载器

Extension:
ExtClassLoader加载扩展jar包 或者由 -Djava.ext.dirs指定路径

Application:
AppClassloader 加载classpath 内容 -Djava.class.path

自定义加载器: 继承Classloader实现

什么是ClassLoader

简单来说ClassLoader 就是一个类加载器 用来将二进制数据或者其他数据加载到内存中 然后生成一个Class对象的指针指向对应的内存地址,完成Class文件读取的一个加载器

ClassLoader加载过程

image.png

双亲委派

image.png 当需要使用到一个Class文件的时候,加载器会先检查是否加载 按照上面的层次就是

自定义的加载器 检查自己是否有加载 如果没有就委派给自己的上一级加载器检查 (这里不是指继承关系 是层次关系 类加载器中有一个变量叫parent 指向上一层的加载器)也就是AppClassLoader 去加载

AppClassloader 如果也没有加载过 就再往上层去检查 ExtClassLoader 如果ExtClassLoader 也没有加载过 就在委派给Bootstrap去检查

最终如果都没有 就是由Bootstrap加载的 , 如果Bootstrap没有找到对应的Class就抛出ClassNotFoundException 往下委派

委派给ExtClassLoader 同样ExtClassLoader 也会去加载对应的class 如果没有加载到就抛出ClassNotFoundException 继续往下委托给AppClassLoader

如果AppClassLoader找不到 就交给自定义加载器 当然如果没有自定义加载器 到AppClassLoader 这一层就结束了 如果有自定义加载器 就去加载了 如果加载不到对应的Class类 就会抛出ClassNotFoundException

每一个Classloader 加载的class 都会自己缓存

可以自己查看对应sun.misc.Launcher 源码

为什么需要双亲委派?

  1. 安全问题

    主要是为了安全 如果不这么设计那么自己定义任何的Class 都由自己加载,比如使用一个第三方库 第三方库中写了一个java.lang.String类,那么就会被第三方自己加载到内存 那么当我们使用的String对象 有可能被第三方修改比如自己上报数据的钩子等等 那么我们使用String的数据都会被第三方拿走

  2. 资源浪费问题 同一个类可能会被加载多次,上一层可能已经被加载过了

能否破坏双亲委派模式?

双亲委派模型并非强制模型

  1. SPI 的调用方和接口定义方很可能都在 Java 的核心类库之中,而实现类交由开发者实现,然而实现类并不会被启动类加载器所加载,基于双亲委派的可见性原则,SPI 调用方无法拿到实现类。SPI Serviceloader 通过线程上下文获取能够加载实现类的classloader,一般情况下是 AppClassloader,绕过了这层限制,逻辑上打破了双亲委派原则。比如JDBC DriverManager

  2. 热加载 重写 loadClass 而不是 findClass 每次都从某个地址检查 如果有新的就重新加载 比如Tomcat中的热部署机制就类似如此

自定义ClassLoader

什么时候需要自己加载类?

热部署
动态远程加载lib
Spring中动态代理
自己加载资源
加密Class等(SDK中 采用加密 通过AppId去服务端获取秘钥 解密才能使用)
... 等等

Class如何加载类?

  1. 检查是否加载过Class 如果没有加载过 调用Parent(上一级)加载class 上一级抛出ClassNotFoundException 标识上一级都没有找到 通过findClass方法去查找类,所以如果需要自定义加载器 就需要重写findClass方法

    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;
     }  
    

ClassLoader采用的就是 模板模式 定义了基本实现逻辑 需要子类完善对应查找逻辑 AQS同样也是采用的模板模式 需要自己实现具体成功上锁的逻辑 以及释放锁的逻辑

findClass 可以 先加载一个文件成一个字节数组 然后使用defineClass 将二进制数据包装成Class类的指针 最后加载的实现是虚拟机实现的由C++实现的 过程查看 Class 如何加载至内存中 Java中Class 是需要才加载对应的class 懒加载方式

什么时候加载

JVM中没有规定什么时候去加载,JVM自己采用的是懒加载方式 但是规定了初始化的时候

    1. new getstatic putstatic invokestatic(指令)
    2.  反射调用时
    3.  初始化子类的时候,父类先初始化
    4.  虚拟机启动的时候 执行主类
    5. 动态语言支持 MethodHandle 解析的结果为 getstatic putstatic invokestatic 的方法时候 该类必须初始化

JVM如何执行Class文件

这里我画了个草图,介绍了下JVM整体包含的一些模块

image.png

JVM运行数据区 在之前的文章都说明过了,这里我们主要了解下执行引擎这一块

跨平台语言

首先我们了解下为什么说Java是跨平台语言?

所谓的跨平台语言 其实是靠JVM虚拟机,JVM虚拟机可以理解为一个Class文件执行虚拟环境,JVM自己定义了一套JVM指令集,通过解析Class文件,将对应的字节码文件翻译或者编译成虚拟机下一层的操作系统 所以才会说Java是跨平台语言,因为Class文件跟操作系统是没有任何关系的,只是虚拟机实现了一套将字节码文件翻译成对应的操作系统指令

JVM是一个虚拟机,Java是一个高级语言最后编译成Class字节码文件,类似其他的比如 Scala,kotlin,Groovy等最后都可以编译成Class字节码同样也是可以在JVM虚拟机上运行,只要遵守Class字节码规范的都可以运行在JVM虚拟机上

JVM执行过程

我们经常说Java属于半编译半解释语言,主要是因为Java有2个执行方向,一个是解释执行,一个是JIT编译成机器码指令执行

image.png

当然JIT即时编译过程还有很多 我这里把部分省略掉了 比如还有 中间代码生成 寄存器分配器 等