一、Java文件编译(未完待续)
跨平台---一次编译,到处运行。其中开发人员完成 .Java文件以及相关引用包(.java文件)通过编译变成 .class文件。其中 .class文件是由8位字节进行存储,有无符号数和表构成。
无符号数:属于基本的数据类型,以u1、u2、u4、u8来代表1个字节,2个字节、4个字节和8个字节构成的无符号数,无符号数可以用来描述数字、索引、数量值或者按照UTF-8编码的字符串
表:无符号数或者其他表组成的复杂数据类型,所有表都以 “_info”结尾。表用于描述有层次关系的复合结构数据,整个class文件本质上就是一张表。
二、JVM .class文件加载
sun.misc.Launcher(虚拟机的入口应用)
JVM有三种加载加载器,从顶之下Bootstrap ClassLoader-->Extention ClassLoader-->Appclass Loader(开发者编写的Java代码)
Bootstrap ClassLoader:
最顶层的加载类,主要加载核心类库,其中加载地址为%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。可以由JVM启动参数(-Xbootclasspath)来指定加载具体目录下面的包,默认(System.getProperty("sun.boot.class.path")绝对地址)。
Extention ClassLoader:
扩展类加载器,可以通过(-D java.ext.dirs)参数来指定非默认加载地址。加载<JAVA_HOME>\lib\ext目录中的包,默认((System.getProperty("java.ext.dirs")j绝对地址)。
App ClassLoader:
应用类加载器,加载开发者编写的Java项目文件,默认地址(System.getProperty("java.class.path")),这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。
三、类加载过程:(装载、验证、准备、解析、初始化、使用、卸载)
1.装载:
(1).通过一个类的全限定名来获取定义此类的二进制字节流。
(2).将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3).在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证:
确定class文件字节流符合JVM要求,安检。主要检查其中文件格式(文件开头格式、版本号、变量类型、语义、符号等等)
1.文件格式验证:
(1)是否以魔数0xCAFEBABE开头。
(2)主、次版本号是否在当前虚拟机处理范围之内。
(3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
(4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
......
2.元数据验证:
(1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
(2)这个类是否继承了不允许被继承的类(被final修饰的类)。
(3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
(4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
......
3.字节码验证:
主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
......
(Halting Problem:通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确的检查出程序是否能在有限时间之内结束运行。)
4.符号引用验证:
符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
(1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。
(2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
3.准备:
为类变量分配内存以及变量初始化,(static 修饰过的)。实例变量不在该阶段分配内存,在实际使用(New的时候进行在JVM堆中进行分配)。
public static final int value;
特例:在虚拟准备阶段进行ConstantValue进行赋值。
4.解析:
虚拟机将常量池内的符号应用变成直接应用的过程;
符号引用:是一组符号用来描述所应用的目标,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。应用的目标不一定加载到内存当中,例如在虚拟机编译时Java文件中应用的其他类的地址时候只能用符号来代替(在org.simpe.People类中引用了org.simple.Test 类,在编译的时候实际上虚拟机很可能不知道Test类的地址,所以在编译的时候会将该类用符号进行代替,在Jvm在解析时再进行转换)各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接应用:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
5.初始化:
类加载的最后一步,开始执行class中定义的Java代码,其中在涉及到静态变量以及静态代码块时有些地方需要注意:
(1)编译器收集的顺序是由代码编写的顺序决定的,静态代码块中只能访问到定义在在惊呆代码块之前的的变量,而在他之后的变量只能赋值不能访问。
(2)初始化方法执行的顺序:JVM会保证子类初始化方法执行之前父类的初始化执行方法已经执行完毕,所以父类的静态语句执行顺序会优先于子类静态语句。
(3)clinit()对于类和方法来说不是必须的,如果类和方法中没有静态语句则不会为这个类生成cilnit 方法。
(<clinit>
方法是由编译器自动收集的,包括所有类变量(静态非final)赋值和静态语句块(static{}))
(4)接口中不能使用静态语句块,接口的实现类也不会执行接口的clinit方法。父类接口中使用到定义变量在初始化阶段也子类接口也不会执行父类接口,只有在使用时才会执行。
(5)虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁同步,如果多线程同时去初始化同一个变量则只有一个线程成功同步,其他线程进入阻塞。
加载详解:https://blog.csdn.net/chenge_j/article/details/72677766
符号直接引用;https://www.cnblogs.com/shinubi/articles/6116993.html
类加载器:https://blog.csdn.net/u010942465/article/details/81987365
JVM运行原理:https://www.cnblogs.com/lishun1005/p/6019678.html