JVM字节码文件
ClassFile {
u4 magic; // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE
u2 minor_version; // 分别为Class文件的副版本和主版本
u2 major_version;
u2 constant_pool_count; // 常量池计数
cp_info constant_pool[constant_pool_count-1]; // 常量池内容
u2 access_flags; // 类访问标识
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 实现的接口数
u2 interfaces[interfaces_count]; // 实现接口信息
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 包含的字段信息
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 包含的方法信息
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 各种属性
}
attribute_info(属性表集合)
Code属性表:attribute_info中最重要的一个属性
其他属性略
java 程序方法体内的代码经过Javac 的编译之后,最终变为字节码指令存储在Code属性内。
如果把一个Java程序中的信息分为代码(方法体里的代码),元数据(MetaData,包括类,字段,方法定义及其他信息)两部分,那么在class文件中,code属性用于描述代码,所有的其他数据项目用于描述元数据。
code 属性表中的几个重要的表项
max_stack:操作数栈的最大深度
max_locals:局部变量所需要的存储空间(单位slot)
code_length:字节码指令的长度
code:存储编译完后的字节码指令
其他略
所以说,就像前面提到的一样,栈帧的大小在编译的时候就确定下来了。
类加载时机
遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
使用 java.lang.reflect 包的方法对类进行反射调用的时候。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
类加载过程
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析,初始化,使用,卸载 七个阶段,其中验证,准备,解析三个部分统称为连接。
-
加载(Loading):
1),通过一个类的全限定名来获取定义此类的二进制字节流(即class文件)
2),将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3),在内存中(堆上)生成一个代表这个类的Java.lang.class 对象,作为方法区这些数据的访问入口 -
验证(Verification):这一阶段的目的是确保class 文件的字节流中包含的信息符合Jvm规范
验证阶段大致上会完成以下的四个动作的检验:
文件格式验证:
例如,版本号是否在范围内元数据验证:进行语义分析,
例如,这个类的是否有父类(除Object类外,所有的类都有父类)字节码验证:确保程序语义合法,
例如,保证方法体内的类型转换是有效的(不能将父类对象赋给子类引用)符号引用验证:该类是否缺少或被禁止访问它依赖的某些外部类。
-
准备(Preparation):准备阶段正式为类中定义的变量(即静态变量)分配内存,并设置类变量的初始零值。(如果该类变量被final修饰,那么显式赋值)
-
解析(Resolution):JVM将常量池内的符号引用替换为直接引用的过程
-
初始化(Initialization):类的初始化是类加载过程的最后一个步骤,(JVM 开始真正执行类中编写的程序代码),初始化阶段就是执行类的构造器 < clinit >()方法的过程
< clinit >()方法是javac 编译器的自动生成物,它是由所有的类变量赋值动作,静态语句块中的语句块合并产生的
了解了这些就明白了,为什么能直接访问静态的类变量,不能直接访问非静态的实例变量?
因为此时非静态的实例变量还没有分配内存呢,根本不存在,肯定会报错了。
!!!留疑
静态方法为什么能直接通过方法名()访问,非静态方法为什么必须通过对象名.方法名()访问,是否是因为JVM中调用他们的指令不同造成的?(了解一下 invoke 指令)
类加载器
JVM 设计的时候,有意把类加载阶段中的 1.通过一个类的全限定名来获取一个类的二进制字节流。这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码就被称为”类加载器“。
- class 文件的显式加载:指的是在代码中通过调用ClassLoader 加载 class 对象,如直接使用 Class.forName(name) 或者 this.getClass().getClassLoader().loadClass() 加载class对象
- class 文件的隐式加载:通常使用的,比如 new myClass()。
-
三层加载器
1)启动类加载器
2)扩展类加载器
3)应用程序类加载器
4)用户自定义类加载器
-
JDK 8 双亲委派模型
1)如果一个类加载收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
3)如果父类的加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模型优点
- 避免类的重复加载,确保一个类的全局唯一性(当父ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次)
- 保护程序安全,防止核心API被随意篡改
破坏
- Tomact。
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器
2.JDK9
- JDK 9 双亲委派模型
- 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。
扩展类加载器被平台类加载器(Platform ClassLoader)取代