关于Java的类加载机制

1、概述

        类会在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,会占用很多的内存。

2、类的生命周期

关于Java的类加载机制_第1张图片

 

类的生命周期包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

2.1类的加载过程

包含:加载、验证、准备、解析和初始化 ,一共包括5 个阶段。

2.1.1加载

(1)加载过程

  1. 通过类的完全限定名称获取定义该类的二进制字节流
  2. 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
  3. 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。

(2)二进制字节流获取方式

  • 从 ZIP 包读取,成为 JAR、EAR、WAR格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件或容器生成,例如由 JSP 文件生成对应的 Class 类。

2.1.2验证

确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

2.1.3准备

使用的是元空间区的内存,为类变量(被 static 修饰的变量)分配内存并设置初始值。

实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。

注意:根据不用类型初始值不同,int初始值一般为 0 值。

例如:下面的类变量 value 被初始化为 0 而不是 123

public static int value = 100;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。

public static final int value = 123;

2.1.4解析 

 将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

2.1.5初始化

初始化阶段是虚拟机执行类构造器 ()方法的过程。准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

()是由编译器自动收集类中所有类变量的赋值动作静态代码块中的语句合并产生的,编译器收集的顺序与源文件顺序一致。

所以,静态语句块只能访问定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类 。

在接口中有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,但接口中不可以使用静态语句块。接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。

此外一个类的 () 方法是线程安全的。多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。

3、类加载的时机

3.1主动引用

虚拟机规范中并没有强制约束何时进行加载,但是严格规定了下列六种情况必须对类进行加载:

3.1.1遇到特殊指令时

演示需要类

class Prent{
    static int A = 23;
    static final int VALUE = 10;
    static {
        System.out.println("Prent被加载");
    }
    public static void dosth(){};
}
class Son extends Prent{
    static {
        System.out.println("Son被加载");
    }
}

(1)当 jvm 执行 new指令时会加载类。即:当程序创建一个类的实例对象

public class Demo01 {
    public static void main(String[] args) {
        // new
        Prent p = new Prent();
}

(2)当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)

// getstatic
System.out.println(Prent.A);

(3)当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值。

        // putstatic
        Prent.A = 188;

(4) 当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法。

// invokestatic
        Prent.dosth();

 3.1.2反射

使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。

3.1.3继承

加载一个类,如果其父类还未加载,则先触发该父类的加载。

3.1.4main方法所在类

当虚拟机启动时,用户需要定义一个要执行的主类 ,虚拟机先加载这个类。

3.1.5接口实现

当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载

3.2被动引用

除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用

(1)通过子类引用父类的静态字段,不会导致子类加载。

System.out.println(Son.A);

(2)数组类型

通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。

//array继承自object与Prent无继承关系
Prent[] array = new Prent[10];

(3)类中常量

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类

System.out.println(Prent.VALUE);

你可能感兴趣的:(java,开发语言)