JVM类加载过程
-
加载
-
链接
- 验证
- 准备
- 解析
-
初始化
-
使用
-
卸载
以下五种情况,必须立即对类进行初始化
- 虚拟机在用户指定包含main方法的主类后启动时,必须先对主类进行初始化。
- 当使用new关键字对类进行实例化时,读取或者写入类的静态字段时,调用类的静态方法时,必须先触发对该类的实例化。
- 使用反射对类进行反射调用时,如果该类没有初始化,必须先触发其初始化。
- 初始化一个类,而该类父类还未初始化,需要先对其父类进行初始化。
- 在JDK7之后的版本中使用动态语言支持,java.lang.invoke.MethodHandle实例解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而该句柄对应的类还未初始化时,必须先触发其实例化
接口的初始化与类基本相同,唯一不同的是当一个接口初始化时,并不要求其父接口必须初始化,只有真正使用父接口时才会初始化。
类加载过程
-
加载
- 通过一个类的全限定名来获取此类的class字节码二进制流
- 将这个字节码二进制流中的静态存储结构转化为方法区中的运行时数据结构
- 在内存中生成一个代表该类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口
-
验证
- 文件格式验证 该阶段主要在字节流转化为方法区中的运行时数据时,负责检查字节流是否符合Class文件的规范,保证其可以正确的被解析并存储于方法区中。后面的检查都是基于方法区的存储结构进行检验,不会再直接操作字节流。
- 元数据验证 该阶段负责分析存储于方法区的结构是否符合Java语言规范的要求,如该类是否继承了不允许继承的类(被final修饰的类)、是否包含父类等。此阶段进行数据类型的校验,保证符合不存在非法的元数据信息。
- 字节码验证 元数据验证保证了字节码中的数据类型符合语言的规范,该阶段则负责分析数据流和控制流,确定方法体的合法性,保证被校验的方法在运行时不会危害虚拟机的运行。
- 符号引用验证 最后一个阶段发生在链接的解析阶段。在解析阶段,会将虚拟机中的符号引用转化为直接引用,该阶段则负责对各种符号引用进行匹配性校验,保证外部依赖真实存在,并且符合外部依赖类、字段、方法的访问性。
-
准备
-
数据类型 初始值 boolean false byte (byte) 0 char \u0000 short (short) 0 int 0 long 0L float 0F double 0D reference null
2.当类字段为常量类型时(static,final修饰),由于字段的值已经确定,并不会在后面修改,此时会直接赋值为指定的值。
-
-
解析
- 解析阶段将常量池中的符号引用替换为直接引用。在字节码文件中,类、接口、字段、方法等类型都是由一组符号来表示,其形式由Java虚拟机规范中的Class文件格式定义。在虚拟机执行特定指令之前,需要将符号引用转化为目标的指针、相对偏移量或者句柄,这样可以通过此类直接引用在内存中定位调用的具体位置。
-
初始化
-
在类的class文件中,包含两个特殊的方法:和。这两个方法由编译器自动生成,分别代表类构造器和构造函数。其中构造函数可以由变成人员实现,而类构造器则由编译器自动生成。而初始化阶段则负责调用类构造器,来初始化变量和资源。
方法由编译器自动收集类的赋值动作和静态语句块(static{}块)中的语句合并生成的。
- 编译器收集的顺序由源文件中语句的顺序决定,静态语句块只能访问到它之前定义的变量,在它之后定义的变量,它只能进行赋值操作,但不能访问。
- 虚拟机保证在子类的方法执行之前,父类的方法已经执行完毕。因此父类中的操作对于子类都是可见的。
- 接口的方法执行之前,不需要先执行父接口的方法,只有父接口中定义的变量被使用时,父接口才会初始化。同时接口的实现类在初始化时也不会执行父接口的方法。
- 方法不是必须的,如果一个类或者接口没有变量赋值和静态语句块,则编译器可以不生成方法。
- 虚拟机会保证方法在多线程中被正确的加锁、同步。如果多个线程同时去初始化一个类,那么只有一个线程去执行方法,其他线程会被阻塞。
-
-
双亲委派模型
- 启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用C++实现的,主要负责加载
<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它等于是所有类加载器的爸爸。 - 扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载
<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。 - 应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。
双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。
双亲委派有啥好处呢?它使得类有了层次的划分。就拿
java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar里面的java.lang.Object加载到JVM中。这样如果有不法分子自己造了个
java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个java.lang.Object。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了。 - 启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用C++实现的,主要负责加载
-
双亲委派怎么实现的
-
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) { long t0 = System.nanoTime(); 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. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父加载器。 4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
-
-
如何主动破坏双亲委派机制
-
重写其中的loadClass方法
- loadClass(): 进行类加载的方法,默认的双亲委派机制就实现在这个方法中
- findClass(): 根据名词或位置加载 .class字节码
- defineclass(): 把字节码转化成Class
-