JVM--类加载

1  类加载时机

类从被加载到内存中开始,到卸载出内存为止,要经历7个阶段:

Java虚拟机没有强制约束什么情况下需要开始类加载的第一个阶段--加载,但Java虚拟机强制约束了类的初始化的开始时间(而加载、验证、准备自然在初始化之前进行)。

有且只有以下5种情况对类进行初始化:

  1. 使用new关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法
  2. 使用java.lang.reflect包的方法对类进行反射调用时
  3. 当初始化一个类时,如果其父类还没有初始化,则需要先初始化父类
  4. 虚拟机启动时,虚拟机会先初始化主类(含有main()的哪个类)
  5. 当使用动态语言支持时(JDK1.7以上)可能会对类进行初始化

这5种场景中的行为称为对一个类的主动引用。除此以外,所有引用类的方式都不会出发初始化,称为被动引用。下面是三种被动引用的例子:

1、对于静态字段,只有直接定义这个字段的类才会被初始化。

因此通过子类引用父类中定义的静态字段,只会触发父类初始化。

可以看出,并没有输出“SubClass",说明SubClass没有被初始化。

2、创建一个类的数组,不会触发该类的初始化。

可以看出,没有任何输出,说明SuperClass没有被初始化。

3、引用一个类的常量,不会触发该类的初始化。

可以看出,没有输出”ConstClass“,说明ConstClass没有被初始化。

接口的初始化:

接口的加载过程与类的加载过程稍微有一点不同:接口也有初始化过程,这点和类一样。接口和类的区别在与5种场景的第三种:当一个类初始化时,要求其父类都初始化了,但一个接口初始化时,并不要求父接口初始化,只有在真正使用到父接口时才进行初始化。

2  类加载过程

加载:

在加载阶段,虚拟机需要完成3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

验证:

验证是连接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流合法,并且不会威胁到虚拟机自身的安全。验证分为四个小阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证:

第一阶段要验证字节流是否符合Class文件的格式规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确的解析并存储与方法区内。

元数据验证:

对字节码描述信息进行语义分析,保证其描述的信息符合Java语言规范:

  • 这个类是否有父类;
  • 这个类的父类是否继承了不被允许继承的类(final类);
  • 如果这个类不是抽象类,是否实现了其父类和接口中要求的所有方法;
  • 类中字段、方法是否与父类产生矛盾;
  • 等等......

字节码验证:

该阶段是最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。在第二阶段对元数据类型校验之后,该阶段对类的方法体进行校验,保证类的方法在运行时的安全。

符号引用验证:

最后一个阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作在解析阶段发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

准备:

准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,这些变量所使用的内存都将在方法区进行分配。

  • 这时候进行内存分配的仅包括类变量(static修饰),不包括实例变量。实例变量将会在对象实例化时随对象一起分配在Java堆中;
  • 这里的初始值”通常情况“下一般指零值,程序员指定的值在初始化阶段才会生效;
  • 特殊情况是:当一个变量被final修饰,那么准备阶段该变量就会被初始化为指定的值。

解析:

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 符号引用:是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中了。
  • 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位目标的句柄。直接引用和内存布局相关,如果有了直接引用,那么目标一定已经在内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。后三者和JDK1.7新增的动态语言支持相关。

初始化:

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

  • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,可以赋值但不能访问。见下方代码块1
  • <clinit>()方法与类的构造函数(实例构造器<init>()方法)不同,它不需要显式地调用父类构造器就可以保证在子类<clinit>()方法执行之前父类<clinit>()已经执行完毕。
  • 由于父类<clinit>()方法先执行,也就是父类的静态语句块要优于子类的变量赋值操作。见下方代码块2
  • <clinit>()方法对于类和接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么就不会为这个类生成<clinit>()方法。
  • 接口中不能用静态语句块,但仍然有类变量初始化操作,所以接口也会生成<clinit>()方法。但与类不同的是,执行接口的<clinit>()方法之前不需要执行父接口的<clinit>()方法。只要当父接口定义的变量被使用时才会执行父类<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程下被正确的加锁、同步。

代码块1:

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

代码块2:

class SuperClass{
    public static int A = 1;    //语句1
    static {  A = 2; }          //语句2
	
}

class SubClass extends SuperClass{
    public static int B = A;
}

public class Test{
    public static void main(String[] args) {
        System.out.println(SubClass.B);
    }
}

上方代码输出2,如果将语句1 和语句2 交换,则输出1.

3  类加载器

虚拟机团队把类加载阶段中的“通过一个类的权限定名来获取描述此类的二进制字节流“这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为”类加载器“。

对任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的命名空间。换句话说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才能成立。否则,集是这两个类来源同一个class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必然不相同。

这里所谓的“相等”,包括代表类的class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

双亲委派模型

从Java虚拟机角度来看,只有两类类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
    • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
    • 用户自定义类加载器。

双亲委派模型要求除了顶层的启动类加载器外,其他类加载器必须有自己的父类加载器。这里的父子关系一般使用组合实现而不是继承。

双亲委派模型的工作模式是:如果一个类加载器收到了类加载请求,它首先不会自己去加载这个类,而是把这个请求委派给自己的父类来完成。因此所有的类加载请求都会传递到顶层的启动类加载器中。只有在父类加载器无法加载时,才会由子类加载器加载。

双亲委派模型的好处是所有类加载器都有了一种层次关系。比如我要加载一个Object对象,那么必然会是顶层启动类加载器加载,这样在程序中无论哪里加载的Object类,都会是同一个类。如果不使用双亲委派模型,那么可能程序中不同地方的Object会不相同,应用程序会变得一片混乱。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java虚拟机--类加载的时机

    SuperHeroes
  • 设计模式----适配器模式

    SuperHeroes
  • Java--泛型程序设计语法

    SuperHeroes
  • 图片预加载和懒加载

    对于前端性能来说,图片是一个过不去的坎,又想能页面美观,又想页面响应速度快,那么这时候就有了两个技术,图片懒加载和预加载。在这边我只介绍一些方法和原理,不具体把...

    wade
  • JVM 类加载机制详解

    jvm将class文读取到内存中,经过对class文件的校验、转换解析、初始化最终在jvm的heap和方法区分配内存形成可以被jvm直接使用的类型的过程。

    用户1637228
  • Chrome将内置原生的懒加载功能

    未来Google Chrome的某个版本将支持懒加载,这是一种延迟加载图像和iframe的机制,如果它们加载时在用户的屏幕上不可见的话。

    疯狂的技术宅
  • java面试小题系列(一)

    哲洛不闹
  • 类加载机制总结

    虚拟机把描述类的数据从class文件加载到内存,并且进行校验、解析、初始化。最终形成可以直接使用的Class对象,这就是类加载机制。

    用户5325874
  • Java 类加载机制及双亲委派模型

    即 加载 → \rightar...

    Steve Wang
  • Java虚拟机(四):JVM类加载机制

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

    朝雨忆轻尘

扫码关注云+社区

领取腾讯云代金券