类加载进JVM的大致过程

导读:本篇文章讲解 类加载进JVM的大致过程,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

编译和运行

一个 Java 文件从编码完成到最终执行,一般主要包括两个过程:

  1. 编译,即把我们写好的 Java 文件,通过javac 命令编译成字节码,也就是我们常说的 .class文件。
  2. 运行,则是把编译生成的.class文件交给 Java 虚拟机( JVM )执行。

而类加载过程,即是指 JVM 虚拟机把 .class文件 中类信息加载进内存,并进行解析生成对应的 Class 对象的过程。

大白话:JVM 在执行某段代码时,遇到了class A, 然而,此时内存中并没有class A的相关信息,于是 JVM 就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

JVM 并不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次,类似于懒加载。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内出为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7 个阶段。

其中 (验证、准备、解析) 3 个部分统称为 连接,如图:

https://upload-images.jianshu.io/upload_images/14923529-ac753500687cf9d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

注:为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,除解析过程外的其他加载过程必须按照如图顺序开始。

加载

加载指的就是把class字节码文件从各个来源通过类加载器装载入 JVM 中。

  • 字节码来源:一般的加载来源包括从本地路径下编译生成的 .class 文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译。

  • 类加载器:一般包括启动类加载器,扩展类加载器,应用类加载器以及用户的自定义类加载器。

注:为什么会有自定义类加载器?

  • 一方面是由于 Java 代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

在加载阶段,JVM 主要完成了 3 件事情:

  1. 通过全类名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为元数据区的运行时数据结构;
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为元数据区这些数据的访问入口。

验证

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

验证阶段主要会完成以下4个检验:

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

注:验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

准备

类变量分配内存并设置类变量默认值,这些变量所使用的内存都将在方法区中进行分配。

//在准备阶段 count 初始值为0,在初始化阶段才会变为 1 
public static int count=1;

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

  • 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

  • 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

初始化

就是根据程序中的赋值语句主动为类变量赋值。

初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。

使用

程序之间的相互调用。

销毁

即销毁一个对象,一般情况下中有JVM垃圾回收器完成。代码层面的销毁只是将引用置为null。


类加载测试题

分析以下类的加载过程,给出最终打印结果。

class MyClass {
	private static MyClass instance = new MyClass();
	public static int count1;
	public static int count2 = 0;
 
	private MyClass() {
		count1++;
		count2++;
	}
}
public class Test {
	public static void main(String[] args) {
		System.out.println("count1=" + MyClass.count1);
		System.out.println("count2=" + MyClass.count2);
	}
}

分析

  1. MyClass.count1 首先调用了 MyClass 类的 static 属性,触发 MyClass 的初始化

恰好不是 final 修饰,即使是final 修饰,如果 有 new 关键字也会触发 MyClass 的初始化。

  1. 类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 count1=0,count2=0;
  2. 类初始化化,为类的静态变量赋值和执行静态代码块,
  3. 按照 从上往下 static 属性的顺序,依次赋值;
  4. 调用类的构造方法后count=1;count2=1;
  5. 继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0。

所以,最终的结果是,输出 1 和 0。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/69716.html

(1)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!