文章目录
JVM
Java类文件结构
// 左边表示占用几个字节,右边是说明
ClassFile {
u4 magic; //魔术
u2 minor_version; //小版本号
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];//附加属性信息
}
详细说明见官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
类文件(二进制字节码文件)中包含:类基本信息、常量池、类方法定义(其中包含虚拟机指令)
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
}
}
将这个源码编译(javac指令)后生成二进制字节码文件,JVM将要执行的字节码通过类加载器ClassLoader加载进内存,再通过字节码校验器的校验后,Java解释器翻译成对应的机器码,最后再系统中解释运行。
编译成字节码文件的源文件不一定是Java写的,只要符合JVM规范编译成的字节码文件,都能放到JVM上正确执行。
转成16进制查看:
反编译javap -v Main.class
上面的二进制字节码文件:
类的加载
概念
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
也可以理解为:类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象用来封装类在方法区内的数据结构。(具体可参考我的另一篇博客:https://blog.csdn.net/IT_10/article/details/103877199最开始部分对反射的讲解)
类的加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备、解析统称为连接(Linking)。
加载、验证、准备、初始化、卸载这5个过程顺序是确定的,而解析有可能会在初始化阶段之后进行,这是为了支持Java语言的运行时绑定
加载:
- 通过一个类的全限定类名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(C++的
instanceKlass
结构) - 在内存中生成一个代表这个类的java.lang.Class对象(镜像),作为方法区中这个类的各种数据的访问入口
- 如果这个类还有父类没有加载,先加载父类
- 加载和连接可能是交替运行的
加载.class文件的方式:
- 从本地系统中直接加载
- 通过网络下载.class文件,如Web Applet
- 从zip压缩包中读取,是jar、war格式的基础
- 运行时计算生成,如动态代理技术
- 由其他文件生成,如JSP应用
- 从专用数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
instanceKlass的部分field:
类的加载总结说就是把类的字节码载入方法区中,使用C++的instanceKlass
结构描述这个Java类,Java程序不能直接访问这个instanceKlass
结构。在堆中会生成这个类的一个镜像,也就是它的Class对象,这个镜像会保存对应的instanceKlass
的地址,instanceKlass
的_java_mirror属性也会保存这个镜像的地址。比如有一个Person
类,加载之后,会在方法区生成这个类的instanceKlass
结构描述这个类的信息,在Java堆内存中会生成Person
类的Class对象Person.class
。Person
类的每个实例对象头会存储Person.class
的地址,通过Person.class
间接访问instanceKlass
,就可以知道类的内部信息。如果反射调用类的getFields()
、getMethods()
等方法,就是通过这种方式在instanceKlass
结构中获取到数据的。
连接:
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值(如int型默认值是0,long类默认值是0L,float型默认值是0.0f)。
- static变量分配空间在准备阶段,真正赋值在初始化阶段;
- 如果static变量是final修饰的,但属于引用类型,真正赋值在初始化阶段
- 如果static变量是final修饰的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 解析:把常量池中的符号引用转换为直接引用(用指针的方式直接指向目标类的成员变量或成员方法)。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。比如对于一个类p1.p2.Test
,符号引用就是p1.p2.Test
。
为了进一步理解准备阶段的分配空间和赋值,下面举个例子:
class Test {
static int a;
static int b = 1;
static final int c = 2;
static final String d = "hi";
static final Object e = new Object();
}
javap -v Test.class
指令反编译字节码文件,部分如下:
{
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 2 // 基本类型的值在编译阶段就确定好了
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hi // 字符串常量的值在编译阶段就确定好了
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
com.exapmle.service.test8.Test();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/exapmle/service/test8/Test;
// 这个静态代码块是在初始化阶段才执行的
// 从下面的虚拟机指令可以看出,静态变量的赋值、引用类型常量的赋值是在
// 静态代码块中执行的,也就是在初始化阶段进行这部分变量/常量的赋值
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: iconst_1
1: putstatic #2 // Field b:I
4: new #3 // class java/lang/Object
7: dup
8: invokespecial #1 // Method java/lang/Object."<init>":()V
11: putstatic #4 // Field e:Ljava/lang/Object;
14: return
LineNumberTable:
line 11: 0
line 14: 4
}
初始化:初始化过程为类的静态变量赋予正确的初始化值以及执行静态代码块。
- 执行类构造器方法()的过程,此方法无需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在原文中出现的顺序执行。
如果类中没有静态变量或者静态代码块,则不会生成类构造器方法()。 - 如果该类有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
- 虚拟机必须保证一个类的()方法在多线程下被同步加载。因为要保证一个类只会被加载一次。
例
public class Test {
private static int num = 1;
static {
num = 2;
number = 2;
}
private static int number = 1;
public static void main(String[] args) {
//num:0->1->2
//number: 0->2->1
System.out.println(Test.num);
System.out.println(Test.number);
}
}
类的使用
Java程序对类的使用分为:
- 主动使用
- 被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。
主动使用类的情况(类的初始化时机)
- 创建类的实例
- 访问某个类或接口的静态变量(被final修饰、已经在编译期把结果放入常量池的静态字段除外),或对该静态变量赋值,或调用类的静态方法
- 调用Class.forName()加载类
- 初始化一个类时,如果发现其父类还未初始化,则先触发其父类的初始化
- Java虚拟机启动时先初始化被标明为启动类的类(包含main()方法的类)
被动使用类的情况
除了上边的几种情况,其他使用Java类的方式都是被动使用,都不会导致类的初始化。
接口的初始化和类的初始化不同之处在于上面6点中的第4点,初始化一个接口时,并不要求其父接口都初始化了,只要真正使用父接口的时候才会初始化。
类的加载与类的初始化举例
例1:子类访问父类的静态变量,只会触发父类的初始化
public class MyTest {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
}
class MyParent1 {
public static String str = "hello";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1 {
static {
System.out.println("MyChild1 static block");
}
}
结果:
分析:
MyChild1的静态代码块没有被执行,因为对于静态字段来说,只有直接定义了该字段的类才会被初始化。但是MyChild1类被加载了,可以在VM options中添加JVM参数-XX:+TraceClassLoading
追踪类的加载信息,运行程序查看输出:
例2:访问类中static final修饰的常量,不会被初始化
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2 {
public static final String str = "hello";
static {
System.out.println("MyParent2 static block");
}
}
结果:
分析:
因为加上final
关键字表示str是一个不可改变的变量,str被调用,因此在编译阶段hello
这个常量就会被存入到调用这个常量的方法(main方法)所在的类(MyTest2 )的常量池中,本质上调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。
例3:如果static final修饰的常量在运行期才能确定,那么访问这个常量的时候,类会被初始化
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = UUID.randomUUID().toString();
static{
System.out.println("MyParent3 static block");
}
}
结果:
分析:
因为只有运行期才能确地str的值,str不能放入MyTest3的常量池中
类加载器
Java虚拟机自带类加载器
- 根类加载器(启动类加载器或引导类加载器 BootStrap ClassLoader)
没有父类加载器,负责加载Java的核心类库,JAVA_HOME/jre/lib,如java.lang.*等,用C++实现,不是ClassLoader的子类。 - 扩展类加载器(Extension ClassLoader)
它的父类加载器是根类加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JAVA_HOME/jre/lib/ext子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动有扩展类加载器加载。是ClassLoader的子类。 - 系统类加载器(应用程序类加载器Application ClassLoader)
它的父类加载器是扩展类加载器,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。是ClassLoader的子类。
对于用户自定义的类,默认使用系统类加载器进行加载。
举例:
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("java.lang.String");
// 使用getClassLoader()获取类加载器,输出为null表示启动类加载器,
// 因为启动类加载器由C++编写,Java程序无法直接访问
System.out.println(aClass.getClassLoader());
}
}
破坏双亲委派模型
JDK有时候会打破双亲委派模式,比如Class.forName("com.mysql.jdbc.Driver")
,本应该是由启动类加载器加载,但是这个类并不在启动类加载器加载的目录下,因此最终会由应用程序类加载器加载。
自定义类加载器
为什么要用自定义类加载器:
- 想加载非classpath随意路径中的类文件
- 隔离加载类,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
- 解耦,常用在方法中
自定义类加载器的步骤:
实现:
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模型,即把请求交由父类处理,它是一种任务委派模型。
先看一个例子:
创建一个java.lang包(实际并不会这样做),然后编写一个String类,在main方法中实例化一个String对象,结果并没有输出self String Class
,也就是说,即使程序中出现了全限定类名相同的类,是不会报错的。但是到底会加载那个类,这就涉及到双亲委派机制。
package java.lang;
public class String {
static{
System.out.println("self String Class");
}
}
public class Test {
public static void main(String[] args) {
String str = new String();
System.out.println("加载String");
}
}
双亲委派机制(也称父亲委托机制)的工作原理是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成;
如果父类加载器还有其父类加载器,则进一步向上委托,请求最终将到达顶层的启动类加载器;
如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
现在可以解释上面的那个例子,当需要加载String类的时候,向上委托,最终到达启动类加载器,启动类加载器会加载以java开头的包下的类。
图解(下图展示的是一种包含关系,而非继承或者上下层关系):
双亲委派机制的优势:
- 避免类的重复加载
比如上面自定义java.lang.String类的例子 - 保护程序安全,防止核心API被随意篡改(沙箱安全机制)
比如自己创建一个java.lang包,在包下定义一个原本java.lang包下不存在的类,然后再该类中写一个main方法并执行,程序会报错SecurityException