JVM系列之类加载机制(一)

类加载机制

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。如果一次性加载,就会占用很多内存。

JVM类加载过程分为五个部分:加载、验证、准备、解析、初始化。

加载

加载是类加载的第一个过程,不表示整个类加载。

加载阶段是可以用户参与的阶段,可以自定义类加载器,去实现自己的类加载过程。

在该阶段,会完成以下三件事:

1. 通过一个类的全限定名获取该类的二进制流。

实现通过类的全限定名获取该类的二进制流的代码块叫做类加载器

主要有以下四种类加载器

  • 启动类加载器(Bootstrap ClassLoader):用来加载 Java 核心类库,无法被 Java 程序直接引用。
  • 扩展类加载器(Extensions ClassLoader):用来加载 Java的扩展库,Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(System ClassLoader):根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
  • 用户自定义类加载器:通过继承 java.lang.ClassLoader 类的方式实现。

双亲委派机制(也在这个阶段发生):当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

2. 将该二进制流中的静态存储结构转化为方法区的运行时数据结构。(如果这个类还有父类没有加载,那么先加载父类)

3. 在堆内存中生成一个代表该类的 Class 对象,通过该Class对象来访问方法区中该类的各种field数据。

验证

验证的目的是确保 Class 文件的字节流中的信息符合当前虚拟机的要求,不会危害虚拟机,该阶段主要完成以下四种验证:

  • 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
  • 字节码验证(验证过程中最复杂的一个阶段):通过验证数据流和控制流,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
  • 符号引用验证:这个动作会在解析过程发生,主要是为了确保解析动作能正确执行。

准备

该阶段会为类的静态变量分配内存,并将其初始化为默认值,将在方法区中进行分配。

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

实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

//该类变量test初始值一般为 0 值,被初始化为 0 而不是 1
public static int test = 1;

//如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。常量 test 被初始化为 1 而不是 0
public static final int test = 1;

解析

该阶段是将常量池的符号引用替换为直接引用的过程。

解析动作并不一定在初始化动作完成之前,也有可能在初始化之后,在初始化阶段之后再开始是为了支持 Java 的动态绑定。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,使用符号引用时,被引用的目标不一定已经加载到内存中。

各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。

直接引用:可以是直接指向目标的指针、相对偏移量、一个能间接定位到目标的句柄,使用直接引用时,引用的目标必定已经存在于虚拟机的内存中了。

初始化

前面的类加载过程,除了在加载阶段用户应用程序可以自定义类加载器之外,其余完全由虚拟机主导和控制。

到了初始化阶段,才真正开始执行类中定义的Java 程序代码。

初始化阶段是虚拟机执行类构造器方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,可以通过程序设定去初始化类变量和其它资源。

<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。

静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

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

由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

class Parent {
    public static int I = 1;
    static {
        I = 2;
    }
}

class Per extends Parent {
    public static int J = I;
}

public static void main(String[] args) {
    //2
    System.out.println(Per.J);
}

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

虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()方法,其它线程都会阻塞等待,直到执行完 <clinit>() 方法。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中这种阻塞很隐蔽。

类初始化时机

1. 主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有以下五种情况必须对类进行初始化(一个类进行主动引用(加载、验证、准备都会随之发生):

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,若没有初始化,则会触发。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,若没有初始化,则会触发。
  • 当初始化一个类的时候,若父类没有初始化,则会先触发父类初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有初始化,则会触发。

2. 被动引用

所有引用类的方式都不会触发初始化。被动引用的常见例子如下:

  • 通过子类引用父类的静态字段,不会导致子类初始化。
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。例如:User[] user= new User[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。System.out.println(Constant.TEST);

类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。因为每一个类加载器都拥有一个独立的类名称空间。

这里的相等,必须满足以下条件:

类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true;

使用 instanceof 关键字做对象所属关系判定结果为 true。

四种类加载器

启动类加载器(Bootstrap ClassLoader)

是extclassloader的父类加载器,使用 C++ 实现,是虚拟机自身的一部分;

加载%JAVA_HOME%/lib下的jar包和class文件和被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。

启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。启动类加载器getClassLoader()返回的是null。

扩展类加载器(Extension ClassLoader)

是APPclassloader的父类加载器,负责

使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。

这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将%JAVA_HOME%/lib/ext下的jar包和class文件或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。扩展类加载器getClassLoader()返回的是ExtClassLoader。

应用程序类加载器(Application ClassLoader)

是自定义类加载器的父类,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。

这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。

它负责加载加载classpath下的类文件,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。应用程序类加载器getClassLoader()返回的是AppClassLoader。

自定义类加载器

需要继承classloader来实现

双亲委派模型

每一个类加载器都有缓存,每个类加载器都会将自己加载的东西放到缓存中

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

双亲委派模型的好处:

  • 主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如 String。
  • 同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的ClassLoader加载就是不同的两个类

java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

在java中所有以java.开头的类都不会被加载,保证其安全性

类加载器父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值