jvm(一)之类加载与初始化

类加载过程

  1. 类从被加载到JVM内存中,一直到卸载出内存,整个生命周期包括

    • 加载(Loading):类加载器读取二进制流程到JVM内部,并存储在方法区(或者元数据空间)内。并且转换成一个与目标类型对应的java.lang.Class对象实例。

    • 连接(Linking)阶段:

      • 验证(Verification):(1)验证字节码文件是否符合JVM规范,如果不是一个有效的字节码文件,会抛出java.lang.VerifyError异常。(2)验证字节码信息是否符合java语法规则 (3)操作验证:对类型的方法执行验证,确保一个类的方法在执行时,不会对jvm产生不良的影响
      • 准备(Preparation):静态变量分配内存空间,并设置默认初始值
      • 解析(Resolution):将字节码常量池中的符号引用全部转换为直接引用,包括类、接口、方法和字段的符号引用。解析阶段在初始化后执行
    • 初始化(Initiazation):执行所有static修饰的代码,包括变量和代码块。如果是静态变量,使用用户指定的值覆盖默认的值。如果是静态代码块,则按照顺序依次执行所有静态代码块里的所有操作。所有的类变量初始化语句和静态代码块都会在java源码执行字节码编译时,被编译器放在收集器里,存在一个叫做<clinit>()方法,此方法可以被称为类初始化方法。对于接口来说就是接口初始化方法。

      class init.StaticParent {
        static int a;
      
        init.StaticParent();
          Code:
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
      
        static void println();
          Code:
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String parent println
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
      
        static {};
          Code:
             0: bipush        20
             2: putstatic     #5                  // Field a:I
             5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             8: ldc           #6                  // String parent static block
            10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            13: return
      }
      
      static {}就是<clinit>(),javac编译器将其名字进行了修改。
      
    • 使用

    • 卸载:Class的卸载是不可控的。JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载.

      • 该类所有的实例都已经被GC。
      • 加载该类的ClassLoader实例已经被GC。
      • 该类的java.lang.Class对象没有在任何地方被引用。
  2. 有JVM自带的类加载器(根、扩展、系统)所加载的类,在JVM的生命周期中,始终不会被卸载。jvm会始终引用自带的类加载器,这些类加载器也会始终引用它们所加载的类的Class对象。

初始化

  1. JVM规范明确规定了类的初始化时机,即一个类或者接口应该在首次主动使用时执行初始化操作。主动使用包括以下情况

    • 为一个类创建一个新的对象实例时(new、反射、序列化)
    • 调用一个类型的静态方法(在字节码中执行invokestatic指令)
    • 调用一个类型或接口的静态字段或者对这些静态字段执行赋值操作(字节码中执行getstatic或者putstatic),对于final修饰的常量表达式除外(编译阶段即可确认值的表达式)
    • 调用java的反射方法(调用java.lang.Class中的方法或者java.lang.reflect包中类的方法)
    • 初始化一个类的派生类(子类初始化必须所有父类先完成初始化)
    • 直接运行一个main函数入口的类
    • jdk7提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStaticREF_putStaticREF_invokeStatic句柄对应的类没有初始化,则执行初始化
  2. 一个类在初始化之前必须要求它的所有超类都完成初始化操作。但是对于接口而言,此规则不适用。只有在某个接口中声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的派生接口或派生类要初始化而被初始化。

  3. 如果一个类拥有超类,如果超类还没有被执行初始化,优先对超类初始化,但是在<clinit>()方法内部不会显式调用超类<clinit>()方法,会由JVM负责保证在一个类的<clinit>()方法执行之前,它的超类<clinit>()方法已经被执行。

  4. JVM必须保证一个类在被初始化的过程中,如果有多个线程同时初始化它,仅仅只能允许其中一个线程对其进行初始化操作,而其余线程必须等待。

  5. 如果一个类中并没有声明任何的类变量,也没有静态代码块,那么这个类在编译为字节码后,字节码文件中将不会包含<clinit>()方法。如果一个类中声明了静态变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作,编译的字节码文件中也不包含<clinit>()方法

类中常量的初始化

  1. 验证常量的初始化

    /**
     * 1. 增加jvm参数
     * -XX:+TraceClassLoading   跟踪类的加载(包括动态生成的类的加载)
     * -XX:+TraceClassUnloading 跟踪类的卸载(包括动态生成的类的卸载)
     */
    public class ConstantTest {
        public static void main(String[] args) {
            System.out.println(Parent.msg);
        }
    }
    
    class Parent {
        public static final String msg = "I am parent init.constant";
    
        static {
            System.out.println("I am parent static block");
        }
    }
    

    输出可以看出,Parent类并没有加载,即此时删除编译后的Parent.class文件也不会影响ConstantTest.class的执行

    ...省略...
    [Loaded init.ConstantTest from file:/Volumes/O/java-classloader/out/production/classes/]
    ...省略...
    I am parent init.constant
    ...省略...
    

    查看字节码文件,ldc 表示将int,float或String类型的常量值从常量池中推送至栈顶。

    $ javap -c ConstantTest
    Compiled from "ConstantTest.java"
    public class ConstantTest {
      public ConstantTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           3: ldc           #4                  // String I am parent init.constant
           5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
           8: return
    }
    
    • 常量(确定的)在编译阶段(Parent的msg常量)会存入到调用这个常量的方法所在类的常量池(ConstantTest类的常量池)中
    • 对于以上程序,调用类并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化
    • 对于以上程序,Parent的msg常量值会存放到ConstantTest类的常量池中,之后ConstantTest与Parent就没有任何关系了
  2. 验证在编译阶段无法确认常量值的情况

    /**
     * 1. 增加jvm参数
     * -XX:+TraceClassLoading   跟踪类的加载(包括动态生成的类的加载)
     * -XX:+TraceClassUnloading 跟踪类的卸载(包括动态生成的类的卸载)
     */
    public class ConstantTest {
        public static void main(String[] args) {
            System.out.println(Parent.msg);
        }
    }
    
    class Parent {
        //public static final String msg = "I am parent init.constant";
        public static final String msg = UUID.randomUUID().toString();
    
        static {
            System.out.println("I am parent static block");
        }
    }
    

    输出可以看出,如果一个常量在编译期间不能确认值,即不会放到调用类的常量池中,此时运行会导致主动使用拥有此常量的类(Parent被加载并且被初始化了)

    ...省略...
    [Loaded init.ConstantTest from file:/Volumes/O/java-classloader/out/production/classes/]
    [Loaded init.Parent from file:/Volumes/O/java-classloader/out/production/classes/]
    
    I am parent static block
    e2f1daa2-615a-4d9d-9037-9115732dce02
    
    ...省略...
    

    查看字节码,执行的指令是getstatic

    $ javap -c ConstantTest
    Compiled from "ConstantTest.java"
    public class init.ConstantTest {
      public init.ConstantTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           3: getstatic     #3                  // Field init/Parent.msg:Ljava/lang/String;
           6: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
           9: return
    }
    
    

静态变量的初始化

  1. 静态变量的主动使用

    public class StaticInitTest {
        public static void main(String[] args) {
            /**
             *   这里是对StaticParent的主动使用,而不是StaticChild.
             *   因为访问的都是父类静态变量或者静态方法
             */
            System.out.println(StaticChild.a);
            StaticChild.println();
        }
    }
    
    class StaticParent {
        static int a = 20;
    
        static {
            System.out.println("parent static block");
        }
    
        static void println() {
            System.out.println("parent println");
        }
    }
    
    class StaticChild extends StaticParent {
        static {
            System.out.println("child static block");
        }
    }
    
  2. 输出

    parent static block
    20
    parent println
    

类的加载与初始化

  1. java系统加载并初始化某个类时,总是保证该类所有父类(直接父类和间接父类)全部加载并初始化。

    public class ClassTest {
        public static void main(String[] args) {
            new Man();
            System.out.println("-----");
            new Man();
        }
    }
    
    class Human {
        static {
            System.out.println("0.human static block");
        }
    
        {
            System.out.println("1.human 构造块 ");
        }
    
        public Human() {
            System.out.println("2.human 构造方法 ");
        }
    }
    
    
    class Man extends Human {
        static {
            System.out.println("0.Man static block");
        }
    
        {
            System.out.println("1.Man 构造块 ");
        }
    
        public Man() {
            System.out.println("2.Man 构造方法 ");
        }
    }
    

    输出,静态代码块只会执行一次,构造块优先于构造方法执行,父类构造块和构造方法优先于子类的构造块和构造方法执行

    0.human static block
    0.Man static block
    1.human 构造块 
    2.human 构造方法 
    1.Man 构造块 
    2.Man 构造方法 
    -----
    1.human 构造块 
    2.human 构造方法 
    1.Man 构造块 
    2.Man 构造方法 
    

接口的加载与初始化

  1. 验证接口的加载

    /**
     * 1. 增加jvm参数
     * -XX:+TraceClassLoading   跟踪类的加载(包括动态生成的类的加载)
     * -XX:+TraceClassUnloading 跟踪类的卸载(包括动态生成的类的卸载)
     */
    public class InterfaceLoaderTest {
        public static void main(String[] args) {
            System.out.println(ChildInterface.b);
        }
    }
    interface ParentInterface {
        public final static int a = 20;
    }
    
    interface ChildInterface extends ParentInterface {
        public final static int b = 30;
    }
    

    以下输出可以看出,ParentInterfaceChildInterface都没有被加载,只有InterfaceLoaderTest被加载

    ...省略...
    [Loaded init.InterfaceLoaderTest from file:/Volumes/O/java-classloader/out/production/classes/]
    30
    ...省略...
    
  2. 验证接口的初始化

    /**
     * 1. 增加jvm参数
     * -XX:+TraceClassLoading   跟踪类的加载(包括动态生成的类的加载)
     * -XX:+TraceClassUnloading 跟踪类的卸载(包括动态生成的类的卸载)
     */
    public class InterfaceInitTest {
        public static void main(String[] args) {
            System.out.println(ChildInitInterface.b);
        }
    }
    
    interface ParentInitInterface {
        /**
         * 1. 这里定义一个Object的子类,并且使用初始化代码进行初始化
         * 2. 如果此obj的初始化代码被执行,则表示ParentInitInterface被初始化了
         */
        public final static Object obj = new Object() {
            {
                System.out.println("ParentInitInterface 初始化了");
            }
        };
    }
    
    interface ChildInitInterface extends ParentInitInterface {
        //匿名内部类
        public final static Object b = new Object() {
            {
                System.out.println("ChildInitInterface 初始化了");
            }
        };
    }
    

    以下输出可以看出,ParentInitInterfaceChildInitInterface和匿名内部类都加载了,但是主动使用ChildInitInterface时,ParentInitInterface并没有初始化,即一个接口在初始化时,父接口并不一定要初始化(当然如果接口引用父接口时,父接口会初始化)

    ...省略...
    [Loaded init.ParentInitInterface from file:/Volumes/O/java-classloader/out/production/classes/]
    [Loaded init.ChildInitInterface from file:/Volumes/O/java-classloader/out/production/classes/]
    [Loaded init.ChildInitInterface$1 from file:/Volumes/O/java-classloader/out/production/classes/]
    ChildInitInterface 初始化了
    init.ChildInitInterface$1@45ee12a7
    ...省略...
    
  3. 验证实现类与接口的初始化化

    /**
     * 1. 增加jvm参数
     * -XX:+TraceClassLoading   跟踪类的加载(包括动态生成的类的加载)
     * -XX:+TraceClassUnloading 跟踪类的卸载(包括动态生成的类的卸载)
     */
    public class ClassInitTest {
        public static void main(String[] args) {
            System.out.println(ChildClassInit.b);
        }
    }
    
    interface ParentClassInitInterface {
        public final static int a = 20;
        /**
         * 1. 这里定义一个Object的子类,并且使用初始化代码进行初始化
         * 2. 如果此obj的初始化代码被执行,则表示ParentInitInterface被初始化了
         */
        public final static Object obj = new Object() {
            {
                System.out.println("ParentClassInitInterface 初始化了");
            }
        };
    }
    
    class ChildClassInit implements ParentClassInitInterface {
      // 此处不要加final
       public static int b = 30;
    }
    

    输出结果可以看出,当类被初始化的,类所实现的接口不会被初始化

    ...省略...
    [Loaded init.ClassInitTest from file:/Volumes/O/java-classloader/out/production/classes/]
    [Loaded init.ParentClassInitInterface from file:/Volumes/O/java-classloader/out/production/classes/]
    [Loaded init.ChildClassInit from file:/Volumes/O/java-classloader/out/production/classes/]
    30
    ...省略...
    
  4. 总结:java系统加载并初始化某个类时,总是保证该类所有父类(直接父类和间接父类)全部加载并初始化。但是此规则不适用于接口。在初始化一个类时,并不会初始化它所实现的接口。在初始化一个接口时,并不会先初始化它的父接口。只有当程序首次使用特定接口的静态变量时,才会导致接口的初始化

综合案例

  1. 初始化案例一

    class Singleton {
    
        public static Singleton singleton = new Singleton();
        public static int a;
        public static int b = 0;
    
        private Singleton() {
            super();
            a++;
            b++;
        }
    
        public static Singleton GetInstence() {
            return singleton;
        }
    
        /**
         * 输出
         * 1
         * 0
         */
        public static void main(String[] args) {
            Singleton singleton = Singleton.GetInstence();
            System.out.println(singleton.a);
            System.out.println(singleton.b);
        }
    
    }
    
    
  2. 初始化案例二

    class Singleton2 {
    
        public static int a;
        public static int b = 0;
        public static Singleton2 singleton = new Singleton2();
    
        private Singleton2() {
            super();
            a++;
            b++;
        }
    
        public static Singleton2 GetInstence() {
            return singleton;
        }
    
        /**
         * 输出
         * 1
         * 1
         */
        public static void main(String[] args) {
            Singleton2 singleton2 = Singleton2.GetInstence();
            System.out.println(singleton2.a);
            System.out.println(singleton2.b);
        }
    }
    
  3. 以上两端代码的区别就是顺序的区别,为什么顺序会决定输出的结果呢?这与JVM加载类的原理有直接的关系

    案例一
    public static Singleton singleton = new Singleton();
    public static int a;
    public static int b = 0;
    
    案例二
    public static int a;
    public static int b = 0;
    public static Singleton2 singleton = new Singleton2();
    
  4. java类在被使用前要经过如下几个阶段

    • 类加载:将磁盘中的class文件加载到内存中,在堆中创建一个对应的java.lang.Class的对象
    • 连接
      • 验证:验证内存的字节码文件是否符合JVM规范
      • 准备:将类的静态变量分配空间并初始化默认值。此时对象没有生成,所以没有实例变量什么事情
      • 解析:把类的符号引用转为直接引用
    • 类的初始化:将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。
  5. 案例一解析,main方法调用Singleton.GetInstence()属于主动使用,所以jvm会按照如下方式处理

    • 加载Singleton类

    • 连接

      • 验证字节码文件

      • Singleton的所有静态变量分配内存空间并赋默认值

        此时静态变量
        singleton = null
        a = 0 
        b = 0
        
      • 解析

    • 初始化:此时对静态变量赋值,即手动赋予的初始值

      //singleton被赋值,构造方法调用a++ b++ ,所以此时a=1 b=1 
      public static Singleton singleton = new Singleton();
      //a 没有被赋值,所以此时a = 1
      public static int a;
      //b 被重新赋值为0,即覆盖了上面的b=1的值
      public static int b = 0;
      所以最终的结果是
      a = 1
      b = 0
      
  6. 案例二解析,案例二与案例一不同之处在于初始化阶段

    //a没有被赋值,所以保持默认初始值0
    public static int a;
    //b被赋值,b = 0
    public static int b = 0;
    //singleton被赋值,构造方法调用a++ b++ ,所以此时a=1 b=1 
    public static Singleton2 singleton = new Singleton2();
    
    所以最终的结果是
    a = 1
    b = 1
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值