1.加载过程:
1.1 通过类的全限定名获取二进制流
1.2 通过字节流代表的静态存储结构转换成方法区运行时数据结构
1.3 生成这个类的class 文件,作为方法区的访问入口
(方法区:方法区是各个线程共享的内存区域,用于存储被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码,运行时常量池。详细请看《深入虚拟机》)
总的来说就是,通过类名把文件加载到二进制字节流,然后转成方法区的存储接口,在生成class作为方法访问入库。
1.4 这个过程涉及到类加载的双亲委派机制
1.4.1 类加载器
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存
1.4.2 类加载器的种类
1.4.2.1、BootstrapClassLoader(启动类加载器) :
最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或 被 -Xbootclasspath参数指定的路径中的所有类。
1.4.2.2、ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
1.4.2.3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
1.4.2.4 自定义类加载器
1.4.3 双亲委派机制
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
1.4.4 双亲委派模型工作工程:
1.当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
2.当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
3.如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
6.如果均加载失败,就会抛出ClassNotFoundException异常。
1.4.5 双亲委派的好处
避免重复加载 和 避免核心类篡改
2、验证阶段
2.1 文件格式验证
校验字节流是否符合class的文件规范 例如:
- 是否以魔数0xCAFEBABE开头。
- 主、次版本号是否在当前虚拟机处理范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
2.2 元数据验证
对字节码文件信息进行语义分析 例如
- 这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
- 这个类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
2.3 字节码验证
主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段
将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的
事件,例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的
2.4 符号引用验证
符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)
的信息进行匹配性校 验,通常需要校验以下内容:
- 符号引用中通过字符串描述的全限定名是否能够找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
3、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111
4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或
接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用:
是一组符号来描述目标,可以是任何字面量。
直接引用:
是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
举个例子:
在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都
准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道
这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以
直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
总结:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类
或者字段、方法在内存中的指针或者偏移量
5、初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶
段是执行类构造器 <clinit> ()方法的过程。
注:当然在初始化过程时候会出现多线程并发安全问题中,因为 <clinit>() 方法是带锁线
程安全,可能会引发死锁,所以虚拟机规范了初始化过程
1.当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个
类,
2.读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始
化。
3.初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
4.当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会
先初始化这个类。
5.当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为
REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始
化,则需要先触发器初始化。
6、使用
7、卸载