网上的知识都比较散不够集中,本文只做知识点概述,如需要深究再根据知识点去深入学习。
类的加载、连接与初始化
- 加载:查找并加载类的二进制数据,转为方法区的数据结构,生成对应的JAVA.LANG.CLASS对象
- 连接
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化
- 解析:将类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
类的加载
类加载的最终产物是位于对中的Class对象,此对象封装了类在方法区内的数据结构,并想JAVA程序员提供访问方法区内的数据结构的接口(反射)
类加载的过程
- 通过类的全限定名获取定义此类的二进制流(文件、网络、计算例proxy、由其他文件生产jsp)
- 将这个字节流的静态存储结构转化为方法区运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据访问入口
类加载器
- 只有被同一个类加载器加载的类才有可能相等。相同的字节码被不同的类加载器加载不相同。
- 自定义类加载器优点:高度灵活性、自定义加载器实现热部署、代码加密
- 类加载器分类
- 启动类加载器,C++实现,虚拟机的一部分。用于加载javahome lib下的类
- 扩展类加载器。加载 javahome /lib/ext目录的类
- 应用程序类加载器:加载用户类路径上的所指定的类库
- 自定义类加载器
常见问题
- 类加载器错误linkagerr(例如JAVA1.5编译放到java1.6里面)
- 若一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器。所有能成功返回Class对象应用的类加载器(包含定义类加载器)都被称为初始类加载器
- 同一个命名空间内的类是互相可见的。子类加载器能看到父类加载器中的类,而父类加载器不能子类加载器的类。若2个加载器直接或者间接的父子关系,那么他们各自是互不可见的(用反射解决)
双亲委派模型
- 如果一个类加载器收到了类加载请求,它首先不会尝试直接去加载这个类,而是吧加载请求委派给父类进行加载
- 每一层类加载器都把类加载请求委派给父类加载器,直至加载请求传递给顶层的启动类加载器
- 如果启动类加载器无法完成加载请求,子类加载器尝试去加载。若发起类加载请求的类加载请求的类加载器无法完成加载请求时,则抛出ClassNotFoundException。从而不再调用其子类加载器进行加载。
- 优点:java类加载器具备了一种带优先级的关系,越是基础的类,越是被上层类加载器进行加载。保证了java程序的运行。
类的连接
类被加载后就进入连接阶段,连接阶段就是将已经读入到内存的类的二进制数据合并到虚拟机的运行环境中
类的验证
- 类文件的结构检查
- 语义检查:确保类本身符合的java语言的语法规定
- 字节码验证:确保字节码流(java方法:动态方法和静态方法)在java虚拟机中安全的执行
- 二进制兼容的验证:确保相互引用的类之间协调一致
准备阶段:
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。
- 这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中;
- 其次这里所说的初始值「通常情况」下是数据类型的零值。假设一个类变量的定义为public static int value = 123; 那么变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这个时候尚未执行任何 Java 方法,而把 value 赋值为 123 的** putstatic 指令是程序被编译之后,存放于类构造器** () 方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。
- 如果类字段的字段属性表中存在 ConstantsValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指的值。假设上面的类变量 value 的定义变为 public static final int value = 123;,编译时 JavaC 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。那么到底什么是符号引用和直接引用呢?
- 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以上任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
类的加载和连接并不是完全串行的。是并行执行的。
类的初始化
初始化条件
- 遇到new、getstatic、putstatic或者invokestatic(执行静态方法)
- 使用java.lang.reflect包进行反射调用
- 当初始化一个类,如果一个父类还未进行初始化,则需先触发父类的初始化
- 虚拟机启动时,需要执行一个主类(包含main方法的那个类),虚拟机会先初始化这个主类
- 不被初始化的例子
- 子类引用父类的静态字段,父类不会被初始化
- 通过数组定义来引用类
- 调用类的变量
常见问题
- 前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。初始阶段是执行类构造器 () 方法的过程。
- 只有在java类在主动使用时候,才会被初始化(6种主动调用方式)
- 虚拟机初始化一个类,要求所有父类类被初始化,但接口除外。一个类编译的时候已经能获得时,不会进行初始化,否则不会。
参考
- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 2 版)》
- 叶子猿-《深入理解Java虚拟机》