jvm_类加载

在Java代码中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

  • 加载:查找并加载类的二进制数据;
  • 连接:
    • 验证:确保被加载的类的正确性;
    • 准备:为类的静态变量分配内存,并将其初始化为默认值;
    • 解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值;
public class test {
    public static int a = 1;
    ...
}

例如上面的代码,是先将 0 赋值给 a,然后在初始化的时候将 1 赋值给 a;

类的加载:

将类的 .class 文件二进制数据读入到内存中,将其放置在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构;
加载方式:

  • 从本地系统中直接加载;
  • 通过网络下载.class文件
  • 从.zip,.jar 等归档文件中加载;
  • 从专有数据库中提取.class文件;
  • 将 Java 源文件动态的编译为.class文件

Java虚拟机结束生命周期:

  • 执行了 System.exit() 方法;
  • 程序正常执行结束;
  • 程序在运行过程中遇到了异常或错误而异常终止;
  • 由于操作系统出现错误而导致Java虚拟机进程终止;

Java程序对类的使用方式有两种

  • 主动使用(七种)
    • 创建类的实例
    • 访问某个类或接口的静态变量/静态方法,或者对该静态变量进行赋值;(getstatic, putstatic)
    • 调用类的静态方法;(invokstatic)
    • 反射(如Class.forName("com.test.Test"));
    • 初始化一个类的子类;
    • Java虚拟机启动时,被标注为启动类的类;
    • JDK1.7开始提供对动态语言的支持:java.lang.invoke.MethodHandle 实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化;
  • 被动使用:除上面的七种方式,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化;
    所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才会初始化他们;

例子1:父类

/**
 * 1. 对于静态字段来说,只有直接定义了该字段的类才会被初始化
 * 2. 当一个类初始化时,要求其父类已经全部初始化完成
 * +XX:TraceClassLoading:用于追踪类的加载信息并打印出来
 */
public class MyTest1 {
    public static void main(String[] args) {
        /**
         * MyParent1 static block
         * hello world
         */
        System.out.println(MyChild1.str);
        /**
         * MyParent1 static block
         * MyChild1 static block
         * welcome
         */
        // System.out.println(MyChild1.str2);
    }
}
class MyParent1 {
    public static String str = "hello world";
    static {
        System.out.println("MyParent1 static block");
    }
}
class MyChild1 extends MyParent1 {
    public static String str2 = "welcome";
    static {
        System.out.println("MyChild1 static block");
    }
}

控制台输出信息

[Loaded com.godfunc.jvm.classloader.MyParent1 from file:/Users/godfunc/WorkSpace/IdeaSpace/learn-java/jvm/build/classes/java/main/]
[Loaded com.godfunc.jvm.classloader.MyChild1 from file:/Users/godfunc/WorkSpace/IdeaSpace/learn-java/jvm/build/classes/java/main/]
MyParent1 static block
hello world

JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载中遇到了.class文件缺失或者存在错误,类加载器必须在程序“首次主动使用”该类时才报告错误(LinkageError错误),如果这个类一直没有被主动使用,那么类加载器就不会报告这个错误。

例2:常量

/**
 * 常量在编译阶段会被存入到调用这个常量的方法所在的类的常量池当中,本质上,调用类并没有直接引用到定义常量的类,
 * 因此并不会触发定义常量的类的初始化
 * 注意:这里的指的是将常量放到了MyTest2的常量池中,之后MyTest2和MyParent2就没有任何关系了
 * 甚至可以删除MyParent2的class文件
 * 助记符:
 * ldc表示将int, float, 或者String的常量值从常量池推送至栈顶。
 * bipush表示将单字节(-128 ~ 127)的常量推送至至栈顶
 * sipush表示将将一个整型(-32768 ~ 32767)常量值推送至栈顶
 * iconst_1表示将int型的1推送至栈顶。-1到5 (iconst_m1 ~ iconst_5)
 */
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}
class MyParent2 {
    public static final String str = "hello world";
    static {
        System.out.println("MyParent2 static block");
    }
}

例3:不确定值的常量类

/**
 * 当一个常量的值并非编译器可以确定的,那么其值就不会被放到调用类的常量池中,
 * 这时程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。
 */
public class MyTest3 {
    public static void main(String[] args) {
        /**
         * MyParent3 static block
         * 59ddfbea-ed41-4ab9-a945-88466d6a5ead
         */
        System.out.println(MyParent3.str);
    }
}
class MyParent3 {
    public static final String str = UUID.randomUUID().toString();
    static {
        System.out.println("MyParent3 static block");
    }
}

例4:类的准备和初始化

/**
 * 在准备阶段,counter1=0 counter=0,然后进入初始化阶段时counter1初始化为1,执行new Singleton(),counter1 + 1 = 2,counter2 + 1 = 1
 * 然后初始化counter2,counter2的值被初始化为0,最后输出的结果为 2 0
 */
public class MyTest6 {
    public static void main(String[] args) {
        Singleton signleton = Singleton.getSingleton();
        // 2 1
        System.out.println(Singleton.counter1 + " " + Singleton.counter2);
    }
}
class Singleton {
    public static int counter1 = 1;
    private static Singleton singleton = new Singleton();
    private Singleton() {
        counter1 += 1;
        counter2 += 1;
        System.out.println(counter1);
        System.out.println(counter2);
    }
    public static int counter2 = 0;
    public static Singleton getSingleton() {
        return singleton;
    }
}

例5:接口

/**
 * 当一个类被初始化的时候,它所实现的接口是不会被初始化的
 */
public class MyTest5 {
    public static void main(String[] args) {
        // 6
        System.out.println(MyChild5.b);
    }
}
interface MyParent5 {
    public static Thread thread = new Thread() {
        {
            System.out.println("MyParent5 invoked");
        }
    };
}
class MyChild5 implements MyParent5 {
    public static int b = 6;
}

类的初始化步骤

  • 假如这个类还没有被加载和连接,那就先进行加载和连接;
  • 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类;
  • 假如类中存在初始化语句,那就依次执行这些初始化语句;

类加载器

根类(启动类)加载器 -> 扩展类加载器 -> 系统类加载器 -> 用户自定义类加载器

posted @ 2019-05-28 10:06  Godfunc  阅读(217)  评论(0编辑  收藏  举报