前面介绍了字节码的读法,下面就是把字节码存入到内存中,那么他又是怎么加载的这些字节码文件的呢?java
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验解析和初始化,最终造成能够被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制数据结构
咱们从上面的定义能够看出来他要经历 加载到内存-->数据校验-->解析-->初始化多线程
固然上面的过程并不详细,详细的过程以下:函数
类的生命周期总共细分为7个阶段
个人全部文章同步更新与Github--Java-Notes,想了解JVM,HashMap源码分析,spring相关能够点个star,剑指offer题解(Java版),设计模式。能够看个人github主页,天天都在更新哟。
邀请您跟我一同完成 repo
加载过程是第一步,他须要完成三个步骤
加载这个过程的实现多种多样,虚拟机规范也并无对其进行明确的约束,好比第一句 "经过全限定名获取二进制流"。没有规定用设么方法获取,从哪里获取。
那么就出现了多种多样的实现方式
对于一个数组类型和一个非数组类型,后者的可控性在这个阶段对于开发人员更强,
由于咱们可使用系统提供的引导类加载器,
也可使用本身定义的类加载器来完成(重写 loadClass完成),
[]
如int[]
是int,int[][]
是int)是由类加载器建立int[][]
是int[]
规则以下:
int[]
的组件类型是基本类型)
加载过程可能和验证阶段的一部分交叉进行,可是两个的开始时间还是保持固定的先后顺序
这个过程不必定非要进行,若是已经反复验证过,实施阶段能够经过 -Xverify:none 来关闭来进行优化。
这个阶段主要是保护虚拟机不会由于载入有害的字节流而崩溃
主要完成下列四个阶段的验证:
文件格式验证
0xCAFEBABE
开头只有经过了这个阶段,字节码才载入到内存中,后面的3个验证再也不对字节码进行操做,直接操做方法区的存储结构
元数据验证
字节码验证
主要是确保被校验的方法在运行时不会作出危害虚拟机的事件
符号引用验证
将符号引用转化为直接引用,这个转化动做将发生在解析阶段。主要校验下列内容
若是不能经过验证,会抛出java.lang.IncompatibleClassChangeError
异常类型的子类,如
准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存在方法区中分配
这里有两个注意点;
这里是类变量(被static修饰的变量)不是实例变量
设置初始值并不等于初始化,只是将其在内存中的值设置为"零值"
例如 public static int value = 123;
准备阶段完成以后是value 是0而不是123;
由于这个时候还没有执行任何Java方法,而把value赋值为123的
putstatic
指令是程序被编译后,存放于类构造器<clinit>()
方法中,因此这个会在初始化阶段执行。初始化阶段会讲到
若是是被final修饰的类变量,那么准备阶段完成以后他就是123
public static final int value = 123
;由于被final修饰的值是存放在 字节码文件的 ConstantValue属性表中,若是不了解这个能够看看个人这篇文章,能看懂的字节码
各个数据类型的初始值以下表
解析是虚拟机将常量池内的符号引用转换为直接引用的过程
初始化相对来讲比较重要,由于他是类加载的最后一步,也是开始真正执行类中定义的Java代码,前面的步骤除了能够本身定义类加载器以外都是由虚拟机主导或者控制的
主动引用:
new
、getstatic
、putstatic
、invokestatic
这四条命令的时候,若是类没有进行初始化,则进行初始化java.lang.reflect
包的方法对类进行反射调用的时候,若是没有,则初始化java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,而且这个方法句柄所对应的类没有进行初始化,则先触发初始化这5类场景被称为主动引用,除此以外,全部引用类的方法都不会触发初始化,成为被动引用
《深刻理解Java虚拟机》这本书,这段话放在了前面,不过我以为这个应该放在后面,由于没有以前的准备阶段,咱们并不知道初始化阶段是执行类构造器
<clinit>()
。因此他举得例子有些就看不懂,为啥要用static代码块
<clinit>()
包含的内容、执行顺序等等,这些也须要先了解
static{}
)<init>()
方法)不一样,他不须要显示的调用父类构造器,虚拟机会保证子类构造器方法执行前,父类构造器先执行。
<clinit>()
方法对于类或接口来讲并非必须的,若是一个类中没有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()
方法clinit
方法不须要先执行父类接口的<clinit>
方法。只有当父接口中定义的变量使用时,父接口才被初始化(对比主动引用的第三条)<clinit>()
方法<clinit>()
会被正确的加锁、同步,<clinit>()
方法,其余线程会被阻塞执行顺序
package classInit;
/** * 由于父类初始化必定要在子类的<clinit>()方法前,因此输出 2,参考执行顺序的第三条 */
public class ClinitTest {
static class Parent{
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
复制代码
多线程阻塞
package classInit;
public class DeadLockClass {
static class DeadLoopClass {
static {
if (true) {
System.out.println(Thread.currentThread() + "init DeadLockClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = () -> {
System.out.println(Thread.currentThread()+"start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread()+"end");
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
复制代码
由执行结果可知,线程1已经进入阻塞;
被动引用的例子1(我把类所有放在一个代码块中)
package classInit.example1;
public class SuperClass {
static {
System.out.println("superClassInit");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClassInit");
}
}
/** * 不会触发子类的初始化,由于他并无在那5个状况中 * 子类引用父类的静态字段,不会致使子类触发 */
public class ClassInit {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
复制代码
并无触发子类的初始化,由于子类并不符合上述5中条件
package classInit.example2;
import classInit.example1.SuperClass;
/** * 经过数组定义来引用类,不会触发此类的初始化 */
public class NotInit {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
复制代码
package classInit.example3;
public class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
/** * 没有输出 "ConstClass init", * 由于常量在编译阶段经过常量传播优化,已经将常量放进了 NotInit 类的常量池中 * 之后NotInit对常量的引用实际都被转化为 NotInit 对自身常量池的引用 * * 也就是说实际上 NotInit 的class文件中并无ConstClass的符号引用入口 * 这两个类在编译成Class文件以后就已经不存在联系了 */
public class NotInit {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
复制代码
至此,类加载的过程已经所有结束