4、JVM中的类加载机制
1、Class文件结构
Java中程序会编译成class文件运行在JVM平台中,其实JVM具有平台微惯性,不仅仅可以运行Java程序,它其实是与Class文件这种特定的二进制文件进行关联的,Class文件中包含了虚拟机指令、符号表以及程序运行的相关信息,并且为了安全,JVM对Class文件规定了许多强制性的语法和结构约束
Class文件是一8位二进制为进本单位的二进制数据流,文件按格式与C语言结构类似,只有无符号数和表,各数据项严格按照顺序进行排列,没有任何分隔符,当需要遇到占用8位以上的数据时,会按照高位在前的方式每8位字节一组分割进行存储
如下是一个简单的Java代码:
public class TClass { public static void main(String[] args) throws Exception { int i = 2; String s = "111"; if(i == 2){ s = s+i; } System.out.println(i+"&&"+s); } }
使用JClsslib工具查看编译后的class文件,
无符号数是进本的数据类型,用u1、u2、u4、u8来分别代表一个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值
而表是以无符号数或者其他表构成的复合型数据类型,习惯以_info来结尾
使用Sublime工具查看编译后的Class文件(十六进制),Class文件字节是从0开始计数,头四个字节被称为魔数(ca fa ba be),它主要是用来是判断这个Class文件是否能被虚拟机接受,第5(00)、第6(00)个字节是次版本号,第7(00)、第8(34,说明JDK所有版本都可以运行这个程序,若主版本号大于48,则需要1.8以上的JDK来运行)个字节是主版本号
主版本号后面是常量池(003c),常量池是Class文件中的资源仓库,也是与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据类型,常量池中主要存放两个常量:字面量和符号引用
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等
符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
常量池后是访问标志,访问标志是用来说明这个class是类还是接口,是什么类型
访问标志后是类索引、夫类索引和接口索引的集合,用来确定类的继承关系
其后是字段表集合,用来描述接口或者类中声明的变量,字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问
性,会自动添加指向外部类实例的字段
字段表集合后是方法表集合,用来描述方法的定义,
其后是属性表集合,存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息
cafe babe 0000 0034 003c 0a00 0c00 2208 0023 0700 240a 0003 0022 0a00 0300 250a 0003 0026 0a00 0300 2709 0028 0029 0800 2a0a 002b 002c 0700 2d07 002e 0100 063c 696e 6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 6501 0004 7468 6973 0100 084c 5443 6c61 7373 3b01 0004 6d61 696e 0100 1628 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 5601 0004 6172 6773 0100 135b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 0100 0169 0100 0149 0100 0173 0100 124c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b01 000d 5374 6163 6b4d 6170 5461 626c 6507 002f 0100 0a45 7863 6570 7469 6f6e 7307 0030 0100 0a53 6f75 7263 6546 696c 6501 000b 5443 6c61 7373 2e6a 6176 610c 000d 000e 0100 0331 3131 0100 176a 6176 612f 6c61 6e67 2f53 7472 696e 6742 7569 6c64 6572 0c00 3100 320c 0031 0033 0c00 3400 3507 0036 0c00 3700 3801 0002 2626 0700 390c 003a 003b 0100 0654 436c 6173 7301 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 0100 106a 6176 612f 6c61 6e67 2f53 7472 696e 6701 0013 6a61 7661 2f6c 616e 672f 4578 6365 7074 696f 6e01 0006 6170 7065 6e64 0100 2d28 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 294c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 4275 696c 6465 723b 0100 1c28 4929 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 6742 7569 6c64 6572 3b01 0008 746f 5374 7269 6e67 0100 1428 294c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b01 0010 6a61 7661 2f6c 616e 672f 5379 7374 656d 0100 036f 7574 0100 154c 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d3b 0100 136a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 0100 0770 7269 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 0021 000b 000c 0000 0000 0002 0001 000d 000e 0001 000f 0000 002f 0001 0001 0000 0005 2ab7 0001 b100 0000 0200 1000 0000 0600 0100 0000 0100 1100 0000 0c00 0100 0000 0500 1200 1300 0000 0900 1400 1500 0200 0f00 0000 9c00 0300 0300 0000 3b05 3c12 024d 1b05 a000 16bb 0003 59b7 0004 2cb6 0005 1bb6 0006 b600 074d b200 08bb 0003 59b7 0004 1bb6 0006 1209 b600 052c b600 05b6 0007 b600 0ab1 0000 0003 0010 0000 001a 0006 0000 0003 0002 0004 0005 0005 000a 0006 001d 0008 003a 0009 0011 0000 0020 0003 0000 003b 0016 0017 0000 0002 0039 0018 0019 0001 0005 0036 001a 001b 0002 001c 0000 0009 0001 fd00 1d01 0700 1d00 1e00 0000 0400 0100 1f00 0100 2000 0000 0200 21
为什么要使用魔数?
基于安全方面的考虑,若使用文件扩展名可以随意的改动,而采用魔数,文件格式的制定者可以随意指定魔数,只要这个魔数没有被广泛的使用过不会产生混淆就可以
2、字节码指令
JVM中指令由一个字节长度的、代表着某种特定操作含义的数字以及跟随其后的0至多个代表此操作所需参数而构成
字节码指令集的操作码总数不可能超过 256 条,因为JVM中限制操作码的长度为一个字节(即 0~255)
具体字节码指令可以参考:https://cloud.tencent.com/developer/article/1333540
代码:
1 public class TClass { 2 public static void main(String[] args) throws Exception { 3 int i = 2; 4 String s = "111"; 5 if(i == 2){ 6 s = s+i; 7 } 8 System.out.println(i+"&&"+s); 9 } 10 }
字节码指令:
0 iconst_2 1 istore_1 2 ldc #2 <111> 4 astore_2 5 iload_1 6 iconst_2 7 if_icmpne 29 (+22) 10 new #3 <java/lang/StringBuilder> 13 dup 14 invokespecial #4 <java/lang/StringBuilder.<init>> 17 aload_2 18 invokevirtual #5 <java/lang/StringBuilder.append> 21 iload_1 22 invokevirtual #6 <java/lang/StringBuilder.append> 25 invokevirtual #7 <java/lang/StringBuilder.toString> 28 astore_2 29 getstatic #8 <java/lang/System.out> 32 new #3 <java/lang/StringBuilder> 35 dup 36 invokespecial #4 <java/lang/StringBuilder.<init>> 39 iload_1 40 invokevirtual #6 <java/lang/StringBuilder.append> 43 ldc #9 <&&> 45 invokevirtual #5 <java/lang/StringBuilder.append> 48 aload_2 49 invokevirtual #5 <java/lang/StringBuilder.append> 52 invokevirtual #7 <java/lang/StringBuilder.toString> 55 invokevirtual #10 <java/io/PrintStream.println> 58 return
3、类加载机制
类从被虚拟机加载到卸载,整个生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)
其中验证、准备、初始化、卸载的顺序是一定的,解析确不是,一定情况下可以先初始化再解析
加载
什么时候进行加载没有特别规定,由虚拟机自己自行把握,
加载阶段虚拟机需要完成以下 3 件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
验证
连接的第一步,确保class文件符合当前虚拟机要求,并且不会危害虚拟机的自身安全
验证阶段主要分为:格式验证、元数据验证、字节码验证、符号引用验证
准备
为类中定义的变量(仅包括被 static 修饰的变量,不包含实例变量,实例变量会在对象实例化时随着对象一起分配在堆中)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
解析
JVM 将常量池内的符号引用替换为直接引用的过程,解析可分为:类或接口解析、字段解析、类方法解析、接口方法解析
符号引用是一种定义,可以是任何字面上的含义,而直接引用存在于内存中,就是直接指向目标的指针、相对偏移量
初始化
初始化阶段是对程序中static语句进行操作的,JVM中对初始化的触发条件进行了严格的规定:
1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的Java 代码场景是:
使用 new 关键字实例化对象
读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)
调用一个类的静态方法
2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类
5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法
句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
6)当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前
被初始化
4、类加载器
JDK中提供了三层类加载器:
Bootstrap ClassLoader
启动类加载器,由C++语言编写的加载器,随着JVM启动,位于最顶层,用来加载核心类库,任何加载器的加载行为都要对它进行询问
Extention ClassLoader
扩展类加载器,是一个Java类,继承自URLClassLoader,用于加载 lib/ext 目录下的 jar 包和 .class 文件
Application ClassLoader
应用程序类加载器,我们写Java类的默认加载器,用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码会首先尝试使用这个加载器来使用
Custom ClassLoader
自定义加载器,用来支持一些个性化的扩展功能
双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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) {} if (c == null) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
JVM是按需动态加载,从原码中可以看出,双亲委派机制就是向上询问是否已经加载、向下尝试是否可以加载
向上询问是否已经加载,可以防止重复加载同一个class文件
向下尝试是否可以加载,可以保证核心class文件不会被篡改,即使篡改也不会去加载,即使加载了也不会是同一个class对象,不同的加载器加载同一个class也不是同一个对象,避免class执行发生危险