往期JVM系列:java
本节主要内容:数据库
类加载阶段描述segmentfault
类的生命周期包含下面7个阶段,其中前五步属于类加载阶段:数组
加载阶段,虚拟机作了如下3件事情:安全
java.lang.Class
对象,做为方法区这个类的各类数据的访问入口简单一句话归纳: 把代码数据加载到内存中,加载完成后,在方法区实例化一个对应的Class对象。
相对于类加载过程的其余阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动做)是开发人员可控性最强的,由于加载阶段既可使用系统提供的引导类加载器来完成,也能够由用户自定义的类加载器去完成,开发人员能够经过定义本身的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()
方法)。网络
对于数组类而言,状况就有所不一样,数组类自己不经过类加载器建立,它是由Java虚拟机直接建立的。可是数组类的元素类型(Element Type,指的是数组去掉全部维度的类型)最终是要靠类加载器去建立,若是数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。数据结构
数组类的可见性与它的组件类型的可见性一致,若是组件类型不是引用类型,那数组类的可见性将默认为public
。多线程
当 JVM 加载完 Class 字节码文件并在方法区建立对应的 Class 对象以后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大体能够四个阶段:框架
是否以魔法数0xCAFEBABE开头、常量池的常量中是否有不被支持的常量类型等等。jvm
该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区以内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储。
只有经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,因此后面的3个验证阶段所有是基于方法区的存储结构进行的,不会再直接操做字节流。
后面三个阶段能够概括为代码逻辑校验,JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误,好比final 是否合规、类型是否正确、静态变量是否合理等。
简单一句话归纳: 验证字节流信息符合当前虚拟机的要求,防止被篡改过的字节码危害JVM安全。
当完成字节码文件的校验以后,JVM 便会开始为类变量分配内存并设置类变量初始值。
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一块儿被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在全部实例化操做以前,而且类加载只进行一次,实例化能够进行屡次。
注意:这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | 'u0000' | reference | null |
byte | (byte)0 |
简单一句话归纳: 为静态变量分配内存,而且设置默认值。
这里举一个“特殊”知识点。
上面提到,在“一般状况”下初始值是零值,那相对的会有一些“特殊状况”:若是类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value的定义变为:
public static final int value = 123;
在编译时Javac将会为被static和final修改的常量生成 ConstantValue属性
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
为何 static final 会直接被赋值?final 关键字在 Java 中表明不可改变的意思,意思就是说 value 的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,所以被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,因此就没有必要在准备阶段对它赋予用户想要的值。
当经过准备阶段以后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
其中解析过程在某些状况下能够在初始化阶段以后再开始,这是为了支持 Java 的动态绑定。
简单一句话归纳: 解析类和方法,将常量池的符号引用替换为直接引用,确保类与类之间相互引用正确性,完成内存结构布局。
类初始化阶段是类加载过程的最后一步,前面的类加载过程当中,除了在加载阶段用户应用程序能够经过自定义类加载器参与以外,其他动做彻底由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
简单一句话归纳:
初始化阶段,执行类构造器<clinit>()
方法(类变量赋值、静态语句块),若是赋值运算是经过其余类的静态方法来完成的,那么会立刻解析另一个类,在虚拟机栈中执行完毕后经过返回值进行赋值。
<clinit>()
方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它以前的类变量,定义在它以后的类变量只能赋值,不能访问。例如如下代码:
public class Test { static { i = 0; // 给变量赋值能够正常编译经过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; }
因为父类的 <clinit>()
方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如如下代码:
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); // 2 }
接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成 <clinit>()
方法。但接口与类不一样的是,执行接口的 <clinit>()
方法不须要先执行父接口的 <clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的 <clinit>()
方法。
虚拟机会保证一个类的<clinit>()
方法在多线程环境下被正确的加锁和同步。
若是多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()
方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>()
方法完毕。若是在一个类的 <clinit>()
方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。
示例代码以下:
public class DeadLoopClassDemo { static class DeadLoopClass { static { /*若是不加上这个if语句,编译器将提示"Initializer does not complete normally"并拒绝编译*/ if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); 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() + "run over"); }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); } }
两个类相等,须要类自己相等,而且使用同一个类加载器进行加载。这是由于每个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字作对象所属关系断定结果为 true。
从 Java 虚拟机的角度来说,只存在如下两种不一样的类加载器:
从 Java 开发人员的角度看,类加载器能够划分得更细致一些,相似于原始部落结构,存在权力等级制度。
最高层:启动类加载器(Bootstrap ClassLoader)。
C++
实现,是虚拟机自身的一部分;<JAVA_HOME>/lib
路径下的核心类库,没法被Java程序直接引用。Object
, System
, String
等第二层:扩展类加载器(Extension ClassLoader),JDK9 及之后的版本 称为平台类加载器(Platform ClassLoader)。
<JAVA_HOME>/lib/ext
路径下的扩展类库,开发者能够直接使用扩展类加载器第三层:应用程序类加载器(Application ClassLoader)
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。若是低层次的类加载器想加载一个未知类,要很是礼貌地向上逐级询问 :“请问,这个类已经加载了吗?” 被询问的高层次类加载器会自问两个问题,第一,我是否已加载过此类?第二,若是没有,是否能够加载此类?只有当全部高层次类加载器在两个问题上的答案均为“否”时,才可让当前类加载器加载这个未知类。如上图所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader ,而后向下逐级尝试是否可以加载此类,若是都加载不了,则通知发起加载请求的当前类加载器 ,准予加载。
简单一句话: 一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器没法完成时才尝试本身加载。
该模型要求除了顶层的启动类加载器外,其它的类加载器都要有本身的父类加载器。这里的父子关系通常经过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
采用双亲委派模式的是好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次防止恶意覆盖Java核心API。
例如 java.lang.Object 存放在 rt.jar 中,若是编写另一个 java.lang.Object 并放到 ClassPath 中,程序能够编译经过。因为双亲委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是由于 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中全部的 Object 都是这个 Object。
如下是抽象类 java.lang.ClassLoader 的代码片断,其中的 loadClass() 方法运行过程以下:先检查类是否已经加载过,若是没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试本身去加载。
public abstract class ClassLoader { // The parent class loader for delegation private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }
java.lang.ClassLoader
的 loadClass()
实现了双亲委派模型的逻辑,自定义类加载器通常不去重写它,可是须要重写 findClass()
方法。
如示例:
public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if (result == null) { throw new FileNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (Exception e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } private byte[] getClassFromCustomPath(String name) { // TODO 从自定义路径中加载指定类 return null; } }
public static void main(String[] args) { URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } }
执行结果:
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/classes
使用-XX:+TraceClassLoading
参数,能够在启动时观察加载了哪一个jar包中的哪一个类。此参数在解决类冲突时特别实用。由于不一样JVM环境对于加载类的顺序并不是是一致的。
部分示例:
[Opened C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.io.Serializable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.Comparable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.CharSequence from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.String from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.reflect.Type from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.Class from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.Cloneable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.ClassLoader from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.System from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] [Loaded java.lang.Throwable from C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar] ......
因为加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可使用条件断点功能。拿HashMap
的加载过程为例,在ClassLoader#loadClass()
处打个条件断点,效果以下,
若是本文有帮助到你,但愿能点个赞,这是对个人最大动力。