JVM类加载详解

举报
共饮一杯无 发表于 2023/01/30 14:20:24 2023/01/30
【摘要】 类加载的时机遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时,如果类没有进行初始化,先出发初始化。public class Student{ private static int age ; public static void method() { }// Student.age//Student. method() ;//new Stud...

类加载的时机

  1. 遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时,如果类没有进行初始化,先出发初始化。
public class Student{
	private static int age ;
	public static void method() {
	}
// Student.age
//Student. method() ;
//new Student( ) ;
  1. 使用java.lang.reflect包的方法反射调用的时候。
Classc=Class.forname("com.zjq.Student");
  1. 初始化类时,父类尚未初始化,先初始化父类。
  2. 虚拟机启动时指定要执行的主类,虚拟机先初始化这个主类。

类加载过程

image.png

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析三个阶段统称为连接。
其中解析的阶段的顺序可能会发生变化,某些情况下可能会在初始化后再开始,另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载(class文件–>Class对象)

image.png

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口

加载源

  • 本地class文件
  • zip包

Jar、 War、Ear等

  • 其它文件生成

由JSP文件中生成对应的Class类.

  • 数据库中

将二进制字节流存储至数据库中,然后在加载时从数据库中读取。有些中间件会这么做,用来实现代码在集群间分发

  • 网络

从网络中获取二进制字节流。典型就是Applet.

  • 运行时计算生成

动态代理技术,用ProxyGenerator .generateProxyClass为特定接口生成形式为"$Proxy"的代理类的二进制字节流.

类和数组加载的区别

数组也有类型,称为“数组类型”。如:

String[] str=newString[10];

这个数组的数组类型是java. lang. String,而String只是这个数组的元素类型。
数组类和非数组类的类加载是不同的,具体情况如下:

  • 非数组类:是由类加载器来定成。
  • 数组类:数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但数组类与类加载器有很密切的关系,因为数组类的元素类型最终要靠类加载器创建。

类加载过程的注意点

加载阶段和链接阶段是交叉的
类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:

** **加载-> 链接-> 初始化

但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

校验(各种检查)

验证阶段比较耗时,它非常重要但不一定必需(因为对程序运行期没有影响",如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none 参数关闭,以缩短类加载时间。
**验证目的:**保证二进制字节流中的信息符合虚拟机规范,并没有安全问题。
验证的过程:

  • 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.
本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。
印证【加载和验证】是交叉进行的:

1.加载开始前,⼆进制字节流还没进⽅法区,⽽加载完成后,⼆进制字节流
已经存⼊⽅法区
2.⽽在⽂件格式验证前,⼆进制字节流尚未进⼊⽅法区,⽂件格式验证通过
之后才进⼊⽅法区

  • 元数据验证

对字节码描述信息进行语义分析,确保符合Java语法规范.

  • 字节码验证

本阶段是验证过程的最复杂的一个阶段。对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不 会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。

  • 符号引用验证

发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行。

准备(为静态成员变量分配内存并初始化0值)

准备阶段主要完成两件事情:

  • **为己在内存的类的静态成员变量分配内存 **
  • 为静态成员变量设置初始值,初始值为0,false,null等

image.png

仅仅[为类变量(即static修饰的字段变量)分配内存]并且[设置该类变量的初始值,即零值],这⾥不包含⽤final修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这⾥也[不会为实例变量分配初始化]。
[类变量(静态变量)]会分配在⽅法区中,⽽[实例变量]是会随着对象⼀起分配到[Java堆]中。
比如:
public static int x = 1000;
注意:

实际上变量x在准备阶段过后的初始值为0,而不是1000
将x赋值为1000的putstatic指令是程序被编译后,存放于类构造器方法之中

但是如果声明为:
public static final int x = 1000;
在编译阶段会为x⽣成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将x赋值为1000。

解析(将符号引用替换为直接引用)

解析是虚拟机将常量池的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行 , 分 别 对 应 于 常量 池 中 的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

  1. 类或接口的解析:

判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。

  1. 字段解析:

会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段
如果有,则查找结束;
如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个 接口和它们的父接口,
还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。(优先从接口来,然后是继承的父类。理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译)

  1. 类方法解析:

对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

  1. 接口方法解析:

与类方法解析步骤类似,只是搜索的是接口不会有父类,因此,只递归向上搜索父接口就行了。

初始化(调用<clinit>方法)

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。
在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器()方法的过程。
其实初始化过程就是调用类初始化方法的过程,完成对static有修饰的类变最的手动赋值还有主动调用静态代码块。

初始化过程的注意点

  • 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语旬在源文件中出现的顺序所决定的.
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
public class Test {
     static {
         i=0;
         System.out.println(i); //编译失败:"⾮法向前引⽤"
     } 
     static int i = 1; 
}
  • 实例构造器需要显式调用父类构造函数,而类的不需要调用父类的类构造函数,虚拟机会确保子类的方法执行前已经执行完毕父类的方法.因此在JVM中第一个被执行的方法的类肯定是java.lang.Object.
  • 如果一个类或接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会为此类生成方法.
  • 接口也需要通过方法为接口中定义的静态成员变量显示初始化。

接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成方法。不同的是,执行接口的方法不需要先执行父接口的方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的方法.

  • 虚拟机会保证在多线程环境中一个类的方法被正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的方法,其它 线程都被阻塞等待,直到活动线程执行方法完毕.其他线程虽会被阻塞,只要有一个方法执行完,其它线程唤醒后不会再进入方法.同一个类加载器下,一个类型只会初始化一次.

使用静态内部类的单例实现:

public class Student {
     private Student() {
     }
     /*
     * 此处使⽤⼀个内部类来维护单例 JVM在类加载的时候,是互斥的,所
    以可以由此保证线程安全问题
     */
     private static class SingletonFactory {
     	private static Student student = new Student();
     }
     /* 获取实例 */
     public static Student getSingletonInstance() {
     	return SingletonFactory.student;
     } 
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。