类的加载,连接与初始化
加载:查找并加载类的二进制
连接
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转为直接引用
初始化:为类的静态变量赋于正确的初始值
Java程序对类的使用方式可以分为两种
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们
主动使用
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值‘
- 调用类的静态方法
- 反射(如Class.forName(“com.test.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(如执行 java Test)
- JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
除了上述7种情形,其他使用Java类的方式都被看作被动使用,不会导致类的初始化。
类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时的数据区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来防撞类在方法区的数据结构。
类加载器
- Java虚拟机自带的类加载器
- 根类加载器(Bootstrap)从系统属性sun.boot.class.path指定的目录加载类(jar, class),它并没有继承ClassLoader
- 扩展类加载器(Extension)它的父加载器为根类加载器,它从系统属性java.ext.dirs指定的目录加载类库,或者从JDK的安装目录的jre/lib/ext目录加载类库(即jar)
- 系统(应用)类加载器(System/App)它的父加载器为扩展类加载器,它从环境变量classpath或系统属性java.class.path指定的目录加载类(jar, class),它是用户自定义加载器的默认父加载器,系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。
- 用户自定义的类加载器
- java.lang.ClassLoader的子类
- 用户可以定制类的加载方式
类加载器会把类加载到Java虚拟机中,从JDK 1.2版本开始,类的加载过程采用双亲委托机制,这种机制能更好的保证Java平台的安全,在此委托机制中,除了Java虚拟机自带的根类加载器外,其余的加载器都有且只有一个父加载器,当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若如加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
类的初始化
- 步骤
- 假如这个类还没有被加载和连接,那就先进行加载和连接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化语句,那就依次执行这些初始化语句
类的初始化时机
- 当Java虚拟机初始化一个实例时,要求它所有的父类都已经初始化,但是这条规则并不适用于接口
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
获取ClassLoader的途径
获取当前类的ClassLoader
clazz.getClassLoader()
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
获取调用者的ClassLoader
sun.reflect.Relection.getCallerClass().getClassLoader()
类加载器的双亲委托机制
1 | ClassLoader loader1 = new MyClassLoader(); |
双亲委托机制的优点是能够提高软件系统的安全性。因此在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码替代父加载器加载的可靠代码,例如:java.lang.Object 类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载包含有恶意代码的java.lang.Object类
命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
- 在同一个命名空间中,不会出现类的完成名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的全限定名(包括类的包名)相同的两个类
java.lang.ClassCastException: com.test.Test cannot be cast to com.test.Test
类的卸载
由用户自定义的类加载器所加载的类是可以被卸载的
SPI (Service Interface Provider)
例子:JDBC
字节码
1 | javap -c com.test.Test1 |
使用javap -verbose命令分析一个字节码时,将会分析字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。
魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE
魔数之后的4个字节时版本信息,前两个字节时minor version(次版本号),后两个字节时major version(主版本号)这里的版本号为00 00 00 34 换算成十进制,表示次版本号为0,主版本号为52. 所以该文件的版本号为1.8.0,可以通过java -version来验证。
- 常量池(constant pool):紧接着主版本号之后就是常量池的入口,一个Java类中定义的很多信息都是常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存在在常量池中,常量池中主要存储两类常量:字面量与符号引用,字面量如文本字符串,Java中声明为final的常量等,而符号引用和接口的全限定名,字段的名称和描述,方法的名称和描述符等。
- 常量池的总体结构:Java类所对应的常量池主要由常量数量和常量池数组这两个部分组成。常量池数量紧跟在主版本号之后,占2个字节;常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的。长度当然也就不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占1个字节,JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不实用)。目的是满足某些常量池索引值的数据在特定情况下需要表达「不引用任何一个常量池」;根本原因在于,索引值为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量对应null值,常量池的索引从1而非0开始。
- 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序),根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象全限定名称来表示,为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母表示,如下所示:
B - byte, C - char, D - double, F - float, I = int, J = long, S - short, Z - boolean, V - void, L - 对象类型,如Ljava/lang/String;
- 对于数组类型来说,每一个维度使用一个前置[来表示,如
int[]
被记录为[I,
String[][]
被记录为[[java/lang/String;
- 用描述符来描述方法的时候,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法:
String getPersonByIdAndName(int id, String name)
的描述符为:([I,Ljava/lang/String;)Ljava/lang/String;
完整的Java字节码结构
1 | ClassFile { |
u1, u2, u4, u8 表示1个字节,2个字节,4个字节,8个字节
1 | javap -verbose org.xxz.Test1 |
常量池表,访问控制权限表
jclasslib
基于栈的指令集与基于寄存器的指令集之间的关系:
- JVM执行指令时采取的方式是基于栈的指令集
- 基于栈的指令集主要的操作由入栈和出栈
- 基于栈的指令集的优势在于它可以在不同的平台之间移植,而基于寄存器的指令集是与硬件紧密关联的,无法做到可移植
- 基于栈的指令的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令数量要多,基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中执行的,速度要快很多,虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。
JVM 模块
虚拟机栈:Stack Frame 栈帧
程序计数器:Program Counter
本地方法栈:主要用于处理本地方法
堆(Heap):JVM管理的最大一块内存空间
方法区(Method Area):存储元数据。永久代(Permanent Generation),从JDK1.8开始,已经彻底废弃了永久代,使用元空间(Meta Space)
运行时常量池:方法区的一部分内容
直接内存:Direct Memory
本文标题:深入理解JVM
本文链接:https://xxzkid.github.io/2019/深入理解JVM/