类加载

JVM整体的一个运行原理。首先从“.java”代码文件,编译成“.class”字节码文件, 然后类加载器把“.class”字节码文件中的类给加载到JVM中,接着是JVM来执行我们写好的那些类中的代码,整体是这么个顺序。如下图
notion image

类加载的概念

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)
类的加载的最终产品是位于内存中的Class对象,Class对象封装了类在方法区内的数据结构,并向程序员提供了访问方法区内数据结构的接口(反射实现的基础)

类加载的生命周期

在java代码中,类型的加载,连接与初始化过程都是在程序运行期间完成的 提供了更大的灵活性,增加了更多的可能性
notion image

加载

  • 查找并加载类的二进制数据,把二进制的java类型读入java虚拟机中
  • 加载.class文件的方式
      1. 从本地系统中直接加载
      1. 通过网络下载.class文件
      1. 从zip,jar等归档文件中加载.class文件
      1. 从专有数据库中提取.class文件
      1. 将java源文件动态编译为.class文件(动态代理,jsp转换为servlet)

连接

连接就是将已经读入到内存的类的二进制数据合并到虚拟机运行时环境中去

验证

  1. 确保被加载类的正确性
  1. 类文件的结构检查
  1. 语义检查
  1. 字节码验证
  1. 二进制兼容性的验证

准备

为类的静态变量分配内存,并将其初始化为默认值,在到达初始化之前,类变量都没有初始化为真正的初始值
 
例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值 0
public class Sample{ private static int a = 1; public static long b; static { b=2; } }

解析:

  • 在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用转换为直接引用

初始化

  • 在初始化阶段,java虚拟机执行类的初始化语句,
  • 变量的初始化有两种途径:
      1. 在静态变量的声明处进行初始化
      1. 在静态代码块中进行初始化.如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有显示初始化,将保持默认值0;
        1. public class Sample { private static int a = 1;//在静态变量的声明处进行初始化 public static long b; public static c; static { b=2;//在静态代码块中进行初始化 } }
  • 初始化阶段是执行类构造器方法的过程。 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。
  • 虚拟机会保证子方法执行之前,父类的方法已经执行完毕.
    • 对于类或接口来说并不是必须的,如果一个类 没有静态语句块或对类变量的赋值操作,那么编译器可以不为这个类生成方法.
      虚拟机会保证一个类的方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程需要阻塞等待
       
类的初始化步骤
  1. 假如这个类还没有被加载和连接,那就先进行加载和连接
  1. 假如类存在直接父类,并且这个父类还没有初始化,那就先初始化直接父类(不适用于接口)
  1. 假如类中存在初始化语句,那就依次执行这些初始化语句
类的初始化时机
  1. 所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们
  1. 初始化一个类时,要求它的所有父类都已经被初始化
      • 在初始化一个类时,并不会先初始化它所实现的接口
      • 在初始化一个接口时,并不会初始化它的父接口
      • 只有当程序首次主动使用接口的运行时常量时,才会导致接口的初始化
       
java程序对类的使用方式可分为2种
  • 主动使用
      1. 创建类的实例
      1. 访问某个类或接口的静态变量(接口的必须是运行时常量),或者对该静态变量赋值
      1. 调用类的静态方法
      1. 反射(如Class.forName("com.test.Test"))
      1. 初始化一个类的子类
      1. java虚拟机启动时被表明为启动类的类(Java Test)
      1. JDK1.7开始提供的动态语言支持
        1. java.lang.invoke.MethodHandle实例的解析结果REF_getStatic, REF_putStatic , REF_ invokeStatic 句柄对应的类没有初始化,则初始化
  • 被动使用 除了7种主动使用情景,其余都是被动使用
 

案例: 不同变量的初始化时机

静态成员

  • 对于静态字段或者静态方法来说,只有直接定义了该字段或者方法的类才会被初始化
  • 当一个类在初始化时,要求其父类都初始化完毕
public class ClassA { public static void main(String[] args) { /* output MyParent1 static block hello world */ System.out.println(MyChild1.str);  //str为父类的静态字段,只会初始化父类 } } class MyParent1 { public static String str = "hello world"; static {    System.out.println("MyParent1 static block"); } } class MyChild1 extends MyParent1 { static {    System.out.println("MyChild1 static block"); } }
 
public class ClassA { public static void main(String[] args) { System.out.println(MyChild1.str2); //str2为子类的静态字段,会先初始化其父类再初始化其子类 /* MyParent1 static block MyChild1 static block welcome */ } } 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"); } }
用虚拟机参数-XX:+TraceClassLoading去追踪类的加载信息
  • 在idea中配置
  • 显示类加载信息,最先加载Object类
  • 先加载ClassA(标识的启动类),然后父类,子类 关于JVM参数
  • -XX:+ ,表示开启option选项
  • -XX:- ,表示关闭option选项
  • -XX:+= ,表示将option选项的值设置为value

常量

编译期常量

  • 常量在编译阶段就被存入到调用这个常量的方法所在类的常量池中
  • 本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化
package com.google.it; /* 这里str常量其实时存放到调用它的main方法所在的类TestB的常量池中,之后TestB与MyParent2无任何关系了 甚至可以将MyParent2的class文件删除, */ public class TestB { public static void main(String[] args) {    //hello world System.out.println(MyParent2.str); //从TestB的常量池中取值 }} class MyParent2 { public static final String str = "hello world"; static {    System.out.println("MyParent2 static block"); }}
反编译class文件
D:\study\知识体系\java基础\jvm\classload\out\com\google\it>javap -c Test B 警告: 二进制文件TestB包含com.google.it.TestB Compiled from "TestB.java" public class com.google.it.TestB { public com.google.it.TestB(); 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 hello world 5: invokevirtual #5 // Method java/io/PrintStream .println:(Ljava/lang/String;)V 8: return }
助记符ldc 表示将int,float或String类型的常量值从常量池中推送至栈顶bipush 表示将单字节(-128~127)的常量值推送支栈顶sipush 表示将一个短整型常量值(-32768~32767)iconst 表示将int类型1推送至栈顶newarray 表示创建一个指定的原始类型(如int,float,char等)的数组,并将其引用压入栈顶anewarray 表示创建一个引用类型的(如类,接口,数组)数组,并将其压入栈顶

运行时常量

当一个常量的值并非编译期可以确定的,那么其值就不会放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类的初始化
package com.google.it; import java.util.UUID; public class TestC { public static void main(String[] args) { System.out.println(MyParent3.str); } } class MyParent3 { //编译期无法确定,不会放入调用类TestC的常量池 public static final String str = UUID.randomUUID().toString();//要初始化后才能确定值,然后放入MyParent的常量池中 static { System.out.println("MyParent3 static code"); } }

数组类型

对于数组实例来说,其类型是由jvm在运行期动态生成的,表示为[Lcom.google.it.D这种形式,其父类型就是Object 对于数组来说,JavaDoc经常将构成数组的元素称为Compoment,实际上就是将数组降低一个维度后的类型 数组类型的实例化并不是对其构成类的主动使用
public class ClassD { public static void main(String[] args) { // D d = new D(); D[] ds = new D[1]; // class [Lcom.google.it.D JVM运行期创建 System.out.println(ds.getClass()); D[][] ds1 = new D[1][1]; // class [[Lcom.google.it.D; System.out.println(ds1.getClass()); // class java.lang.Object System.out.println(ds.getClass().getSuperclass()); // class java.lang.Object System.out.println(ds1.getClass().getSuperclass()); int[] ints = new int[1]; // class [I System.out.println(ints.getClass()); // class java.lang.Object System.out.println(ints.getClass().getSuperclass()); } } class D { static { System.out.println(111111); } } 类加载器源码注释 /** *objects for array classes are not created by class * loaders, but are created automatically as required by the Java runtime. */

接口

  1. 当一个接口在初始化时,并不要求其父接口都完成了初始化
  1. 只有在真正使用到父接口的时候(如引用接口中所定义的运行期常量时),才会初始化)
  1. 初始化一个接口的实现类的时,不会初始化该接口
案例1.1
public class Test5 { public static void main(String[] args) { //删除MyParent5.class和MyChild5.class程序都正常运行,因为接口中都是常量 //在这个案例中都是编译期常量被存入使用这个常量的方法所在类Test5的方法区内的常量池中 System.out.println(MyChild5.b); } } interface MyParent5 { public static int a = 6; } interface MyChild5 extends MyParent5 { public static int b = -5; }
案例1.2
package com.google.it; import java.util.UUID; public class Test5 { public static void main(String[] args) { //主动使用了子接口,父接口并没有初始化 //output: 0bde16cf-b6b1-4480-a694-bfe413fd2933 System.out.println(MyChild5.b); } } interface MyParent5 { public static Thread a = new Thread() { { System.out.println("MyParent5 invoked"); } }; } interface MyChild5 extends MyParent5 { public static final String b = UUID.randomUUID().toString(); }
案例2
public class Test5 { public static void main(String[] args) { //主动使用了父接口的运行期常量 //output: MyParent5 invoked //Thread[Thread-0,5,main] System.out.println(MyChild5.a); } } interface MyParent5 { public static Thread a = new Thread() { { System.out.println("MyParent5 invoked"); } }; } interface MyChild5 extends MyParent5 { public static final String b = UUID.randomUUID().toString(); }
案例3
public class Test5 { public static void main(String[] args) { System.out.println(MyChild5.b); } } interface MyParent5 { //运行时常量,初始化的时候才确定 public static Thread a = new Thread() { { //对匿名内部类对象构建之前会执行构造代码块的内容 System.out.println("MyParent5 invoked"); } }; } class MyChild5 implements MyParent5{ public static int b = -5; } //结果只打印 -5 说明接口并没有初始化
综合案例
public class Test6 { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("counter1, " + Singleton.counter1); // 1 System.out.println("counter2, " + Singleton.counter2); // 0 } } class Singleton { public static int counter1; //single对象构建在Single类初始化过程中完成 private static Singleton singleton = new Singleton(); private Singleton() { counter1++; counter2++; System.out.println(counter1); // 1 System.out.println(counter2); // 1 } public static int counter2 = 0; public static Singleton getInstance() { return singleton; } }

反射

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
public class MyTest12 { public static void main(String[] args) throws ClassNotFoundException { ClassLoader loader = ClassLoader.getSystemClassLoader(); // 用类加载器加载,不会初始化 Class<?> clazz = loader.loadClass("com.google.it.CL"); System.out.println(clazz); System.out.println("=============="); // 反射,用Class对象去获取类的数据结构 clazz = Class.forName("com.google.it.CL"); System.out.println(clazz); } } class CL { static { System.out.println("Class CL"); } }

类的使用

包括类的实例化等使用类的场景
类实例化
  1. 为新的对象分配内存
  1. 为实例变量(成员变量)赋默认值,执行构造(实例)代码块,按先后顺序
  1. 为实例变量赋予正确的初始值
  1. java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的.class文件中,这个实例的初始化方法被称为"",对于源码中的每一个类的构造方法,java编译器都产生一个""方法
  1. 类的实例化并不要求类已经初始化完毕,类的首次实例化会触发类的初始化

类的卸载

  • 当一个类被加载,连接,初始化后,它的生命周期就开始了,当代表该类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,该类再方法区内的数据也会被卸载,从而结束该类的生命周期
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
  • 由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载,jvm会始终引用这些类加载器,而这些类加载器会始终引用它们所加载的类的Class对象,由用户自定义的类加载器加载的类可以被卸载
  • 类的卸载过程
    • 加上vm参数 -XX:+TraceClassUnloading
      public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); System.out.println("=========="); loader1 = null; clazz = null; object = null; System.gc(); loader1 = new MyTest16("loader1"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); object = clazz.newInstance(); System.out.println(object); } //输出 //findClass invoked: com.google.it.ClassA //class loader name: loader1 //class:356573597 //com.google.it.ClassA@677327b6 //========== [Unloading class com.google.it.ClassA 0x00000007c0061028] //findClass invoked: com.google.it.ClassA //class loader name: loader1 //class:1836019240 //com.google.it.ClassA@135fbaa4
      使用JVisualVM工具,将当前线程sleep1秒去观察

      java虚拟机与程序的生命周期

      在如下的几种情况下,Java虚拟机将结束生命周期
    • 执行了System.exit()方法
    • 程序正常执行结束
    • 程序在执行过程中遇到了异常或错误而异常终止
    • 由于操作系统出现错误而导致java虚拟机进程终止