一、从JVM架构说起
JVM分为三个子系统:
(1)类加载器子系统 (2)运行时数据区 (3)执行引擎
二、虚拟机启动、类加载过程分析
package com.darchrow;
/**
* @author mdl
* @date 2020/5/7 16:12
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
编译: 源文件-->class文件,这个时候只是将源文件翻译为JVM可识别的文件,落到磁盘上。
javac HelloWorld.java
运行:类加载器在此期间负责将class加载到内存,最终形成可以被JVM直接使用的java类型。
java HelloWord
系统将调用{JAVA_HOME}/bin/java.exe完成以下步骤:
1. 为JVM申请内存空间
2. 创建引导加载器BootstrapClassLoader实例,加载系统类到内存方法区域
3. 创建启动类实例Launcher,并取得类加载器ClassLoader
4. 使用获取到的ClassLoader加载我们定义的HelloWorld类
5. 加载完成JVM执行main方法
6. 结束,java程序结束,JVM销毁
-
引导类加载器 Bootstrap ClassLoader,它加载的是 {JRE_HOME}/lib下的类
-
创建启动类实例Launcher,并取得类加载器ClassLoader,这个时候获取到的有
ExtClassLoader、AppClassLoader
三、类的加载
类加载器并不需要程序“首次主动使用”时加载它,JVM规范允许加载器在预料某个类将要被使用时就预先加载它,如果在预先加载时出现错误,类加载器必须在程序首次主动时报告错误,如果这个类一直没有被程序主动使用,那么加载器不会报告此错误。
加载后数据的存储大概是这个样子的
类加载的过程
1) 加载
1. .class文件到内存
2. 类的数据结构到方法区
3. 堆区生成一个java.lang.Class对象,作为方法区数据结构的入口
2)连接
2.1) 校验:确保被加载的类的正确性
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
2.2) 准备:为类变量在方法区分配内存,并将其初始化为零值
类变量是被static修饰的变量
public static int value = 123;
准备阶段过后初始值为0
也有特例
public static final int value = 123;
final修饰的static变量为常量,准备阶段后就是123了
实例变量不会在此阶段分配内存,它将会在对象实例化时随着对象一起分配到堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只能进行一次,实例化可以进行多次)
2.3)解析:把类中的符号引用转换为直接引用
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
举例:
package com.darchrow.demo;
/**
* @author mdl
* @date 2020/5/8 11:44
*/
public class Animal {
}
package com.darchrow.demo;
/**
* @author mdl
* @date 2020/5/8 11:44
*/
public class Person {
private Animal pet;
}
Person类中引用了Animal,编译阶段只能用com.darchrow.demo.Animal代表Animal的真实地址,解析阶段,JVM将解析该符号引用,来确定com.darchrow.demo.Animal的真实地址(如果该类还没加载过,则先加载)。
3)初始化:虚拟机执行类构造器 <clinit>() 方法的过程
-
声明类变量为初始值
-
使用静态代码块为类变量指定初始值
<clinit>()方法有如下特点:
-
按顺序收集。特别注意的是,static{}代码块只能访问定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问。
package com.darchrow.demo; /** * @author mdl * @date 2020/5/8 14:24 */ public class TestStatic { static { int i = 0; // System.out.println(i); 这句编译器会提示“非法向前引用” } static int i = 1; public static void main(String[] args) { System.out.println(TestStatic.i); } }
-
虚拟机会保证子类
() 方法运行之前,父类的 () 方法已经执行结束。 package com.darchrow.demo; /** * @author mdl * @date 2020/5/8 14:36 */ public class Parent { static{ System.out.println("父类静态代码块"); } public Parent() { // 实例化发生在类加载之后 System.out.println("父类构造函数"); } }
package com.darchrow.demo; /** * @author mdl * @date 2020/5/8 14:36 */ public class Children extends Parent { static { System.out.println("子类静态代码块"); } public Children() { // 实例化发生在类加载之后 System.out.println("子类构造函数"); } public static void main(String[] args) { new Children(); } }
-
() 方法对于类非必须,如果类中不包含静态变量、静态代码块,编译器可以不生成此方法 -
接口中不可以使用静态语句块,但可以声明静态变量。区别于类,接口的
() 方法不需要先去执行父类的 () 方法,只有当父类中定义的变量使用时,父接口才初始化。另外,接口的实现类在初始化时同样不会调用接口的 () 方法。 -
() 方法多线程下已被正确的加锁和同步,如果多线程初始化一个类,将只有一个线程执行,其他线程将阻塞等待知道执行线程执行完毕。注意避免在一个类的 () 方法中有耗时操作。 初始化时机:其实就是主动引用
- new对象、访问类变量(final static除外)、类方法
- 使用反射:java.lang.reflect,如果类未被初始化,则先初始化
- 初始化类时发现父类未被初始化,先触发父类的初始化
- 虚拟机启动,用于指定主类并执行main方法,其实我们发现main方法就是类方法
- jdk1.7 动态语言支持
例外的情况即被动引用
-
子类引用父类的静态字段,不会触发子类的初始化
package com.darchrow.demo.passive; /** * @author mdl * @date 2020/5/8 15:16 */ public class SuperClass { public static int value =100; static { System.out.println("父类静态代码块"); } }
package com.darchrow.demo.passive; /** * @author mdl * @date 2020/5/8 15:16 */ public class SubClass extends SuperClass { static { System.out.println("子类静态代码块被调用"); } }
package com.darchrow.demo.passive; /** * @author mdl * @date 2020/5/8 15:19 */ public class Test { public static void main(String[] args) { System.out.println(SuperClass.value); } }
-
通过数组来定义引用类,不会触发此类的初始化,数组继承Object,包含数组的属性和方法。
还是上面的例子,改下Test类
package com.darchrow.demo.passive; /** * @author mdl * @date 2020/5/8 15:19 */ public class Test { public static void main(String[] args) { // System.out.println(SuperClass.value); SuperClass[] sca = new SuperClass[10]; } }
控制台无任何打印
-
常量在编译阶段会进入常量池(方法区),本质上并没有直接引用到定义常量的类,因此不会触发常量的初始化
package com.darchrow.demo; /** * @author mdl * @date 2020/5/8 15:25 */ public class ConstClass { static { System.out.println("我被调用了!"); } public static final String HELLO_WORLD="hello world"; }
package com.darchrow.demo.passive; import com.darchrow.demo.ConstClass; /** * 被动引用 * * @author mdl * @date 2020/5/8 15:19 */ public class Test { public static void main(String[] args) { // System.out.println(SuperClass.value); // SuperClass[] sca = new SuperClass[10]; System.out.println(ConstClass.HELLO_WORLD); } }