美文网首页java收录
JVM学习(一):Java类的加载机制

JVM学习(一):Java类的加载机制

作者: J先生有点儿屁 | 来源:发表于2018-08-01 14:09 被阅读16次

    目录

    目录

    一、类加载机制

    1、类加载?

    1.1 什么是类加载机制?

    首先,在代码被编译器编译后生成的二进制字节流(.class)文件;
    然后,JVM把Class文件加载到内存,并进行验证、准备、解析、初始化;
    最后,能够形成被JVM直接使用的Java类型的过程。
    --这就是类加载机制

    类加载器并不需要等到某个类被“首次主动使用”时才加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载。
    如果预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器不会马上报告错误;类加载器必须在程序【首次主动使用】该类时才报告错误(LinkageError错误)。

    1.2 加载.class 文件的方式
    • 从本地系统中直接加载
    • 通过网络下载.class文件
    • 从zip、jar等文件中加载
    • 从专有数据库中提取.class文件
    • 将Java远文件动态编译为.class文件

    2、类加载流程图

    类加载机制

    二、类加载机制阶段详解

    1、类的加载

    类的加载是类加载机制过程的第一个阶段,该阶段主要完成三件任务:

    • ①. 通过类的全限定名来获取类的二进制字节流。
    • ②. 将字节流中所有代表的静态存储结构转化为【方法区】的运行时数据结构。
    • ③. 在内存Java堆中生成一个代表这个类的Java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

    2、连接

    在经历类的加载过程后,生成了类的java.lang.Class对象,接着会进入连接阶段。连接阶段负责将类的二进制数据合并如JRE(Java运行时环境)中。类的连接大致分为三个阶段。

    2.1 验证阶段

    验证:确保被加载的类符合JVM规范和安全。
    验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

    • 文件格式验证:
      验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    • 元数据验证:
      对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    • 符号引用验证:确保解析动作能正确执行。

    验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

    2.2 准备(重点!!)

    准备阶段:为类的静态变量(static filed)在【方法区】分配内存,并附上默认初始值(0或者null值)。

    • 静态变量在方法去分配内存
    • 静态变量在分配内存后,附上初始值。

    静态常量(static final filed)会在准备阶段直接将程序设定的值附上。
    例如:

    static final int a = 10; 
    // 该静态常量a 会在【准备阶段】直接将10赋值。
    static int b = 11;
    // 该静态变量b 在【准备阶段】只会赋值初始值0,等到了【初始化】阶段会将真正的11赋值给静态变量b。
    

    2.3 解析

    解析:把类中的符号引用转换为直接引用。

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    • 符号引用,就是一组符号来描述目标,可以是任何字面量。
    • 直接引用,就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

    3、初始化(重点!!)

    初始化,为类的静态变量赋予正确的初始值。
    初始化阶段是执行类构造器<clinit>()方法。

    3.1 在Java中堆类变量惊喜初始值设定有两种方式:
    • ①声明类静态变量是制定初始值。
    • ②使用静态代码块为类静态变量制定初始值。
    3.2 JVM初始化步骤
    • ① 如果这个类还没有被加载和连接,则程序先加载并连接该类。(其实就是执行上面的类加载、连接两步骤)
    • ② 如果的直接父类还没有被初始化,则先初始化其直接父类。
    • ③ 如果这个类中有初始化语句,则系统会一次执行这些初始化语句。
    3.3 类初始化时机

    类初始化时机,有且只有主动引用时才会触发类的初始化。
    被动引用则不会触发类初始化。
    类初始化时间后面单独详细说明。

    4、使用

    类的正常使用。

    5、卸载

    类的卸载需要根据【该类对象不再被引用+GC回收 】来判断何时被卸载。

    • ①由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为一直被引用着。
    • ②由用户自定义的类加载器加载的类是可以被卸载。

    三、类的加载时机与初始化时机

    (一)类的加载时机

    当应用程序启动的时候,所有的类都会被一次性加载吗?
    答案是否定的。不能,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。

    类是什么时候i被加载的呢?
    当一个类真正被加载的时机是在创建对象的时候,才会执行类加载。
    例如:A a= new A();该类的加载,只有在创建对象的时候才加载类。
    其中,最先加载拥有main方法的主线程所在的类。

    (二)类的初始化时机(重要!!)

    引用方式主要分为两种:主动引用被动引用
    有且只有主动引用才会触发类初始化的过程。被动引用不会触发类初始化过程。

    主动引用

    有且只有主动引用才会触发类初始化的过程。触发主动引用的方式有以下五种:

      1. 创建类的实例。即通过new的方式,new一个对象。
        例如:
      A a = new A();
      
      1. 调用类的静态变量(非final修饰的常量) 和静态方法。
        代码示例:
      • Test3.class代码如下
      public class Test3 {
          public static final int A = 10;// 静态常量, 不会触发初始化,该代码在连接准备阶段已赋值。
          public static int B = 11;// 静态变量, 在被外界调用时,会主动触发类的初始化。
      
          static{//静态代码块,如果类初始化,则一定会执行该代码块
              System.out.println("test3..print~~~");
          }
      
          public void fun() {//实例方法,只有实例化后代码才能调用此方法
              System.out.println("test3..fun~~~");
          }
      
          public static void fun2() {//静态方法,在被外界调用时,会主动触发类的初始化。
              System.out.println("test3..static fun2...");
          }
      }
      
      • mian()方法测试代码:
      public class TestStatic {
          public static void main(String[] ags) {
      //        申明类时,不会主动触发初始化流程.
              Test3 test3;
      
      //        类的静态常量,不会触发初始化,该代码在连接准备阶段已赋值。
              System.out.println("test3 A "+Test3.A);
      
      //        类的静态变量,在被外界调用时,会主动触发类的初始化。【注意,此时会初始化Test3】
              System.out.println("test3 B " + Test3.B);
      
      //      直接调用test3的静态方法fun(),也会主动触发类的初始化工作。
              test3.fun();
          }
      }
      
      • 输出内容:
      test3 A 10
      test3..print~~~
      test3 B 11
      

      这里会发现,static静态代码块先执行,然后再执行static变量初始化。

      1. 通过反射对类进行调用。
        例如:Class.forName("com.jx.Test2");
      • Test3.class示例代码:
        public class Test3 {
            public static final int A = 10;// 静态常量, 不会触发初始化,该代码在连接准备阶段已赋值。
            public static int B = 11;// 静态变量, 在被外界调用时,会主动触发类的初始化。
        
            static{//静态代码块,如果类初始化,则一定会执行该代码块
                System.out.println("test3..print~~~");
            }
        
            public void fun() {//实例方法,只有实例化后代码才能调用此方法
                System.out.println("test3..fun~~~");
            }
        
            public static void fun2() {//静态方法,在被外界调用时,会主动触发类的初始化。
                System.out.println("test3..static fun2...");
            }
        }
        
        • mian()方法类,测试代码:
        public class TestStatic {  
            public static void main(String[] ags) {
                try {
                    Class<?> aClass = Class.forName("com.jx.Test3");//调用此代码,则类会初始化。
                    System.out.println("aClass " + aClass);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
        
        • 输出内容:
        test1..print~~~
        aClass class com.jx.Test1
        
      1. 初始化某个类的子类,则父类也会被初始化。
      • Test4.class示例代码:
          public class Test4 extends Test3 { // 继承了Test3
              public void fun4() {
                  System.out.println("test4...fun4...");
              }
          }
      
      • 测试代码:
          public class TestStatic {
              public static void main(String[] ags) {
                  Test4 test4 = new Test4();
                  test4.fun4();
              }
          }
      
      • 输出内容:
      test3..print~~~
      test4...fun4...
      
      1. Java虚拟机启动时,指定的main方法所在的类,需要被提前初始化。
      • 测试代码:
          public class TestStatic {
              static {
                  System.out.println("test static ...");
              }
              public static void main(String[] ags) {
                  Test4 test4 = new Test4();
                  test4.fun4();
              }
          }
      
      • 输出内容:
      test static ...
      test3..print~~~
      test4...fun4...
      
    被动引用

    被动引用,不会发生类的初始化过程。
    被动引用又分为三种方式:

      1. 当访问一个类的静态变量时(该静态变量是父类所持有),只有真正声明这个变量的类才会初始化。
        子类调用父类的静态变量,只有父类初始化,而子类不会进行初始化。
      • 代码示例:
        public class SuperClass { // 父类
            public static int A = 7; // 父类静态变量
        
            static { // 静态代码块在初始化时执行
                System.out.println("super class static ...");
            }
        }
        
        public class SubClass extends SuperClass { // 子类继承父类
            static { // 静态代码块在初始化时执行
                System.out.println("sub class static ...");
            }
        }
        
        public class TestStatic2 {
            public static void main(String[] args) {
                System.out.println("A =" + SubClass.A);  // 调用子类继承的父类静态变量
            }
        }
        
      • 输出内容:
        super class static ...
        A =7
        
      1. 通过数据定义引用类,不会触发类的初始化。
        因为是数据进行new,而对应的应用类没有被new,所以该类没有触发任何主动引用。
      • 代码示例
        public class TestStatic3 {
            public static void main(String[] args) {
                SuperClass[] superClasses = new SuperClass[3];
                System.out.println(superClasses);
            }
        }
        
      • 输出内容:
      [Lcom.jx.SuperClass;@193b845
      
      1. final 常量不会触发类的初始化,因为编译阶段就存储在常量池中。
        //常量类
        public class ConstClass {
            static{
                System.out.println("常量类初始化!");
            }
            
            public static final String HELLOWORLD = "hello world!";
        }
         
        //主类、测试类
        public class NotInit {
            public static void main(String[] args){
                System.out.println(ConstClass.HELLOWORLD);
            }
        }
        

    四、类生命周期与JVM生命周期

    (一) 类的生命周期

    当一个类被加载、连接、初始化后,它的生命周期就开始了。
    当这个类的class对象不再被引用,即类不可触及时,Class对象就会结束生命周期。这个类在方法区的数据也会被卸载,从而结束这个类的生命周期。
    所以,一个类结束生命周期,取决于代表它的Class对象何时结束生命周期。

    (二)JVM生命周期

    Java虚拟机结束生命周期的情况:

    • 1.执行了System.exit()方法.
      1. 程序正常执行结束。
      1. 程序在执行过程中遇到了异常或错误而并未处理,导致异常终止。
      1. 由于依赖的操作系统出现错误,而导致Java虚拟机进程终止。

    参考

    http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html
    https://blog.csdn.net/xorxos/article/details/80490240
    https://www.cnblogs.com/qiuyong/p/6407418.html?utm_source=itdadao&utm_medium=referral

    相关文章

      网友评论

      • n油炸小朋友:“final 常量不会触发类的初始化,因为编译阶段就存储在常量池中”--》编译阶段应该是准备阶段?
        J先生有点儿屁:@油炸小居崽 笔误,应该是连接阶段的准备阶段。感谢指正...
      • n油炸小朋友:(二)类的初始化时机下的test3的例子得出的结论:“这里会发现,static静态代码块先执行,然后再执行static变量初始化。”这里不妥吧,我认为那里只能说明main函数的输出晚于test3的静态代码块,不能说明static变量初始化比静态代码块晚。
        J先生有点儿屁:@油炸小居崽 是的,这个确实不能说明static变量初始化比静态代码块晚。谢谢指正..

      本文标题:JVM学习(一):Java类的加载机制

      本文链接:https://www.haomeiwen.com/subject/ycrbmftx.html