深入理解jvm——类的加载机制

2021-06-07  本文已影响0人  Peakmain

经典面试题

第一种情况

class SingleTon {
    public static int count1;
    public static int count2 = 0;
    private static SingleTon singleTon=new SingleTon();
    private SingleTon(){
        count1++;
        count2++;
    }
    public  static SingleTon getInstance(){
        return singleTon;
    }
}
public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println(SingleTon.count1);//1
        System.out.println(SingleTon.count2);//1
    }
}

第二种情况

class SingleTon {
    private static SingleTon singleTon=new SingleTon();
    public static int count1;
    public static int count2 = 0;
    private SingleTon(){
        count1++;
        count2++;
    }
    public  static SingleTon getInstance(){
        return singleTon;
    }
}
public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println(SingleTon.count1);//1
        System.out.println(SingleTon.count2);//0
    }
}

学习类加载之后再来分析一下结果

什么是类的加载机制

java内存模型.png
类的生命周期.png

类的初始化时机

java虚拟机规范严格规定有且只有5种情况立即对类进行初始化(加载、验证、准备自然需要在之前开始)

举例分析

举例一

public class SuperClass {
    static {
        System.out.println("com.peakmain.jvm.SuperClass");
    }
    public static int value = 234;
}
public class SubClass extends SuperClass {
    static {
        System.out.println("com.peakmain.jvm.SubClass");
    }
}

上述代码结果:


image.png

结论:通过子类引用父类的静态字段,不会导致子类初始化

举例二
修改superClass

public class SuperClass {
    static {
        System.out.println("com.peakmain.jvm.SuperClass");
    }
    public static final int value = 234;
}
public class Test {
    public static void main(String[] args) {
        System.out.println(SuperClass.value);
    }
}

运行结果:


image.png

结论:java源码中引用的常量value 在编译阶段通过常量传播优化,已经将常量的值234存储到Test类的常量池,以后Test对常量SuperClass.value的引用实际都转化为Test类对自身常量池的引用

举例三

public class SuperClass {
    static {
        System.out.println("com.peakmain.jvm.SuperClass");
    }
}
public class SubClass extends SuperClass {
    static {
        System.out.println("com.peakmain.jvm.SubClass");
    }
    public static int value = 234;
}
public class Test {
    static {
        System.out.println("Test");
    }
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

结果:


image.png

结论:
1、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
2、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

类初始化字节码分析

上面我们分析了java类的初始化时机,那么我们来看下一个最简单的类的初始化字节码分析

public class Test {
    public static void main(String[] args) {

    }
}
Classfile /D:/javaProject/leetcode/out/production/leetcode/com/peakmain/jvm/Test.class
  Last modified 2021-6-3; size 389 bytes
  MD5 checksum 6200800a2a37017e4a779eaa366b30ec
  Compiled from "Test.java"
public class com.peakmain.jvm.Test //类名
  minor version: 0 //副版本号
  major version: 52 //主版本号
  flags: ACC_PUBLIC, ACC_SUPER //访问权限
Constant pool: //常量池
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // com/peakmain/jvm/Test
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/peakmain/jvm/Test;
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               args
  #14 = Utf8               [Ljava/lang/String;
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               com/peakmain/jvm/Test
  #19 = Utf8               java/lang/Object
{
  public com.peakmain.jvm.Test();//Test的构造函数
    descriptor: ()V//返回void
    flags: ACC_PUBLIC //访问权限pubilc
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0 //把this装载到操作数栈中
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V    
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable://局部变量表
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/peakmain/jvm/Test;

  public static void main(java.lang.String[]);//main方法
    descriptor: ([Ljava/lang/String;)V//返回值void 
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable://局部变量表
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
}
SourceFile: "Test.java"

我们会发现Test和main函数都在栈中

类的加载过程

加载
-XX:+TraceClassLoading

在加载过程,虚拟机需要完成以下三件事情

public class Test {
    public static void main(String[] args) {
          SuperClass[] superClass=new SuperClass[10];
    }
}

我们会发现SuperClass类并没有进行初始化,我们直接看字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: bipush        10   //常数10推送到操作数栈
         2: anewarray     #2        // 创建class com/peakmain/jvm/SuperClass的一维数组并添加到栈顶(创建的new对象在堆中)
         5: astore_1 //将操作数栈中栈顶元素(这里就是superclass)放到局部变量表下表1的位置
         6: return
      LineNumberTable:
        line 6: 0
        line 7: 6
      LocalVariableTable://局部变量表
        Start  Length  Slot  Name   Signature
            0       7     0  args   [Ljava/lang/String;
            6       1     1 superClass   [Lcom/peakmain/jvm/SuperClass;

我们发现它只是加载SuperClass,并没有进行初始化


image.png
验证
准备
解析
初始化

初始化阶段实际就是类构造器<cinit>()方法的过程

面试题分析

第一种情况

class SingleTon {
    public static int count1;
    public static int count2 = 0;
    private static SingleTon singleTon=new SingleTon();
    private SingleTon(){
        count1++;
        count2++;
    }
    public  static SingleTon getInstance(){
        return singleTon;
    }
}
public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println(SingleTon.count1);//1
        System.out.println(SingleTon.count2);//1
    }
}

首先连接状态,静态变量初始化:count1=0,count2=0,singleTon=null
初始化阶段:从上到下执行赋值和静态代码块:count1=0,count2=0,创建对象后count1=1,count2=1

第二种情况

class SingleTon {
    private static SingleTon singleTon=new SingleTon();
    public static int count1;
    public static int count2 = 0;
    private SingleTon(){
        count1++;
        count2++;
    }
    public  static SingleTon getInstance(){
        return singleTon;
    }
}
public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println(SingleTon.count1);//1
        System.out.println(SingleTon.count2);//0
    }
}

首先连接状态,静态变量初始化:singleTon=null,count1=0,count2=0
初始化阶段:从上到下执行赋值和静态代码块:先初始化对象,count1=1,count2=1,之后再赋值,count1没有赋值所以还是1,count2重新赋值变成0

参考文献

上一篇下一篇

猜你喜欢

热点阅读