转载

JVM—>类加载篇

why

为什么要进行类加载?

  • 编译后的Class文件并不能直接被JVM使用
  • Class文件是对类描述的一段二进制字节流
  • JVM是一个进程,只能对内存中的数据进行操作

要将Class文件加载到JVM中,然后根据描述在不同的内存空间给它分配内存

类加载步骤

  • 加载
  • 连接

    • 验证
    • 准备
    • 解析
  • 初始化
  • 使用
  • 卸载

一、加载

作用

将二进制字节流存储在方法区中,然后在堆内存中实例化一个Class类对象,这个对象作为访问方法区中的类型数据的外部接口

特性

  • 这是可控性最强的阶段,这个阶段可以自定义很多自己的东西

    • 安全:加载加密Class文件,避免程序逻辑曝光
    • 动态代理技术:编写反射接口,在运行时计算生成对象
    • 从其他文件生成:由JSP文件生成对应的Class文件
  • Class文件不一定是存在磁盘,只是指一段二进制字节流
  • RPC框架的原理是将Class文件传过去,在另外一端进行加载并使用
  • 同一个Class文件经过不同的类加载器加载,得到的对象是不同的
  • 加载阶段和连接阶段是交叉进行的

类加载条件

  • 通过new实例化对象
  • 反射调用
  • 实例化子类的时候,会先实例化父类
  • JVM启动的时候会先加载main函数所在的主类

证明间接引用不会触发类加载

添加虚拟机启动参数打印加载阶段的信息

-XX:+TraceClassLoading

1. 通过子类引用继承的静态成员变量,不会触发子类初始化

public class NotInitialization{
    public static void main(String []args){
        System.out.print(SubClass.value);
    }
}

class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public final static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

结果打印:

SuperClass init!

2. 通过定义数组,并不会初始化对象

public class NotInitialization{
    public static void main(String []args){
        SuperClass []superClasses = new SuperClass[10];
    }
}

结果:没有显示init

3. 应用类的静态常量,不会触发该类的加载

public class NotInitialization {
    public static void main(String[] args) {
        System.out.print(SuperClass.value);
    }
}

class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public final  static    int value = 123;
}

结果:没有显示init

原因:编译阶段会做常量传播优化,将vale的值存在NotInitialization类对应的常量池中,执行main方法相当于仅调用自身常量池的引用,且不持有SuperClass的引用

二、连接

1. 验证

确保加载进来的Class文件字节流符合规范,不会危害JVM安全

可通过 -Xverify:none 参数来关闭大部分类验证,缩短短类加载时间

特性

与加载阶段交叉运行,最耗费时间的

① 文件格式校验

  • 保证字节流符合Class文件规范,并且可以被当前JVM处理
  • 只有文件格式校验通过,这段字节流才允许存储在方法区中
  • 后面三个验证阶段都是直接验证的方法区,不会再操作字节流

② 元数据验证

对字节码的描述进行语义校验

  • 是否有父类
  • 是否继承了final类
  • 如果这个类不是抽象类,是否实现了父类/接口要求实现的方法
  • 重载/复写是否符合规范,如覆盖父类的final字段、参数列表相同但返回类型不同

③ 字节码验证

通过数据流和控制流分析确保语义合法且符合逻辑,确保其不会危害虚拟机

  • 确保所有跳转指令不会跳转到方法体意外的字节码指令
  • 确保方法体中类型转化是有效的,避免把对象赋值给跟它不相干的数据类型

特性

① 最耗费时间

② 无法确保字节码验证后的代码没问题,程序无法准确判断

③ 在javac编译器里加了StackMapTable优化字节码验证时间

④ 符号引用验证

保证解析行为能够正常执行

特性

① 检查是否能根据符号引用中的全限定名找到对应的类

② 符号引用中的类、字段、方法的是否可被当前类访问(权限规则)

2. 准备

给静态变量分配内存并进行初始化

特性

① java7以及以前是在方法区中分配,之后是配置在java堆中

② 静态常量的话会在这里进行初始化并且赋值,静态变量的话仅初始化(赋0值)

3. 解析(重点)

将常量池中的符号引用转为直接引用

  • 符号引用:java类在编译时并不知道引用对象的内存地址,就用符号表示

    • 即便是引用自身的成员变量也是符号引用
    • 引用的目标不一定是已经加载到内存的内容
  • 直接引用:直接指向目标的指针

    • 引用的目标一定在虚拟机中存在
    • 性能比符号引用快

三. 初始化

真正执行类中编写的java代码

特性

① 初始化前由类加载器主导,从初始化开始由程序主导

② 初始化阶段就是执行类构造器的构成

双亲委派模型

每个类加载器在收到类加载请求时,会优先委派给父类加载器,只有当父加载器在自己的搜索范围没有找到所需的类,子加载器才会尝试自己去加载

作用

确保基础类是相同的类加载器加载,保证java程序的稳定运行

避免不同的类加载器去加载常用的类如Object,会导致应用程序混乱

三层类加载器

  • 启动类加载器
    负责加载lib下jvm能识别的jar
  • 扩展类加载器
    负责加载libext目录中的jar包,一般存放通用的jar
  • 应用程序类加载器
    负责加载用户类路径所有的类库

特性

  • java一直保持三层类加载器,双亲委派的类加载架构
  • 类加载器由两种组成
    ① 启动类加载器:Bootstrap 有C++实现,是虚拟机的一部分和其他类加载器
    ② 其他类加载器由Java语言实现,并且都继承java.lang.ClassLoader组成

破坏双亲委派模型

为什么要破坏

弄懂OSGi的实现

应用场景

  • OSGi热部署
  • 代码热替换

问题

1. 类在什么情况下会卸载?

答:同时满足这三个条件 ① 这个类的所有实例都被回收 ② 加载该类的ClassLoader已被回收 ③ 该类对应的java.lang.Class对象没有被任何地方引用,无法通过反射创建对象

类卸载就是在方法区中清空该类的信息,java8及之后永生带消除,里面存放的出数据也就是类的信息被移动到java堆

2. 连接的阶段的各个细节到底都做了些什么?

答:验证、准备、解析

3. 什么是双亲委托机制?好处?什么场景下需要避开这个机制?

答:因为不同加载器加载同一个类出来的对象是不相等的,假如用多个加载器加载Object,会导致代码比较混乱。所以需要确保基础用的类都是相同的类加载器加载。双亲委派模型就是指每个类加载器在收到加载类的请求时,会优先委托父加载器加载,除非父加载器无法加载,才会自己尝试加载。热部署的情况下需要避开

4. 怎么证明类就是按照你说的那样加载?如何证明?

答:具体加载的步骤无法看到,只能通过加-XX:+TraceClassLoading查看是否被加载

小结

  1. 熟悉类加载机制可以引导开发者更规范的编写代码
  2. 熟悉类加载机制可以根据业务编写更灵活的代码(OSGi)
  3. 熟悉类加载机制可以快速定位问题
原文  https://segmentfault.com/a/1190000022526387
正文到此结束
Loading...