第一步:加载
- 获取二进制字节流
- 静态存储结构转化为方法区的运行时数据结构
- 在Java堆里生成一个类对象,作为方法区的访问入口
第二步:链接
验证
- 验证class文件的标识:魔数 Magic Number
- 验证class文件的版本号
- 验证常量池(常量类型、常量类型数据结构是否正确、UTF-8是否符合标准等)
- class文件的每个部分(字段表、方发表等)是否正确
- 元数据验证(父类验证、继承验证、final验证)
- 字节码验证(指令验证)
- 符号引用验证()
准备
为类变量分配内存并且设置类变量的初始化阶段。
只对static类变量进行内存分配。
static int n = 2;
初始化值是0,而不是2。因为此时还没有执行任何java方法()。
static final int n = 2;
对应到常量池ConstantValue,在准备阶段n必须被赋值成2。
类变量:一般称为静态变量。
实例变量:当对象被实例化的时候,实例变量就跟着确定。随着对象销毁而销毁。
解析
对符号引用进行解析。
直接引用:指向目标的指针或者偏移量。
解析就是将符号引用转换为直接引用。
主要涉及:类、接口、字段、方法(接口、类)等。
CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_InterfaceMethodref_info
CONSTANT_MethodType_info
CONSTANT_MethodHandler_info(方法=>vtable 接口=>itable)
CONSTANT_invokeDynamic_info
匹配规则:简单名字+描述符 同时满足
- 字段的解析
class A extends B implements C,D{
private String str;
}
在本类里面去找有没有匹配的字段===>如果类有实现接口,往上层接口找匹配的字段===>搜索父类
解析字段的顺序A(本类)=>C、D(上层接口)=>B(父类)===>Object(根类)
如果找到了,但是没有权限:java.lang.IllegalAccessError
如果失败:java.lang.NoSuchFieldError
- 类方法的解析
class A extends B implements C, D{
private void inc(); //方法的解析
}
在本类里面去找有没有匹配的方法===>去父类找匹配的方法===>接口列表里面去找匹配的方法(代表本类是一个抽象类,查找结束,抛出java.lang.AbstractMethodError异常)
如果找到了,但是没有权限:java.lang.IllegalAccessError
如果失败(没找到):java.lang.NoSuchMethodError
- 接口方法的解析
在本类里面去找有没有匹配的方法===>父类接口中递归查找
如果失败(没找到):java.lang.NoSuchMethodError
第三步:初始化
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量是指定初始值
- 使用静态代码块为类变量指定初始值
初始化就是执行()方法的过程。
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
-
创建类的实例,也就是new的方式
-
访问某个类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射(如Class.forName(“com.shengsiyuan.Test”))
-
初始化某个类的子类,则其父类也会被初始化
-
Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
-
执行了System.exit()方法
-
程序正常执行结束
-
程序在执行过程中遇到了异常或错误而异常终止
-
由于操作系统出现错误而导致Java虚拟机进程终止
类加载器
打印类加载器及其父加载器:
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
}
}
运行结果
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4554617c
null
类加载器用来把类加载到虚拟机中。Java虚拟机自带了以三种加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器(Extension ClassLoader):该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
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;
}
}
双亲委派模型意义:
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行