虚拟机类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载过程。
我们知道一个Java类要想运行,必须由jvm将其加载到内存中才能运行,加载的目的就是把Java
字节代码转换成JVM
中的java.lang.Class
类的对象。
类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每个类加载器都拥有一个独立的类名称空间。也就是说:比较两个类是否「相等」,只要在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
类加载器的隔离问题
每个类加载器都有一个独立的类命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全限定名(包名+类名) 进行搜索来检测这个类是否已经被加载了。
JVM
及 对类的唯一性是 根据加载它的类加载器和这个类本身来确定的; 一个Java虚拟机中有可能存在两个由不同类加载器所加载的类,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类,这就是 类加载器隔离性。
为了解决类加载器的隔离问题,JVM
引入了双亲委托机制。
双亲委派模型
在JDK9引入之前,绝大多数Java程序会用下面三个类加载器进行加载
- 启动类加载器(Bootstrap Class Loader):由C++编写,负责加载
<JAVA_HOME>\jre\lib
目录下的类,例如最基本的Object,Integer,这些存在于rt.jar文件中的类,一般这些类都是Java程序的基石。 - 扩展类加载器(Extension Class Loader):负责加载
<JAVA_HOME>\jre\lib\ext
目录下的类,通用性的类库放在ext目录来扩展JAVA的功能,但实际的工程都是通过maven引入jar包依赖。 - 应用类加载器(Application Class Loader):负责加载
ClassPath
路径下的类,通常开发人员编写的大部分类都是由这个类加载器加载。
上图中所呈现出的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。
双亲委派模型的工作过程:
一个类加载器收到类加载的请求时,它首先会把这个请求委派给父类加载器去完成(每一个层次的类加载器都是如此),因此所有的加载请求最终都会传递到最顶层的启动类加载器中;只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
目的
-
避免一个类被重复加载;
-
防止java核心类被恶意篡改,例如通过自定义类加载器加载 自定义的String类 到JVM中;
有了双亲委派模型,攻击者自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
缺点:
-
最底层的类加载器需要将自己负责加载的这些类 委派给最顶层的父类去尝试加载一遍,最后再自己加载;
-
程序启动加载的时候,加载整个rt.jar包中的所有类,一些后续可能不会被使用到的类也被 JVM 加载到内存中了,浪费资源;
1类加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其类信息放在运行时数据区的方法区内,然后在堆区创建一个这个类对应的Class 类型的实例,用来封装类在方法区内的对象。
类的加载的最终产品是位于堆区中的Class实例对象。 Class实例对象封装了类在方法区内的数据结构,并且向开发人员提供了访问方法区中数据结构的入口。
图片来源: https://blog.youkuaiyun.com/fuzhongmin05/article/details/59109617
2类加载的过程
Java虚拟机中类加载过程分为三个步骤:加载,连接(验证、准备、解析),初始化;如下图 , 是一个类从加载到使用及卸载的全部生命周期;
类加载的时机
Java类的加载是动态的,为了节省内存开销,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类完全加载到JVM中,至于其他类,则在需要使用到的时候才加载。
**引导类加载器(Bootstrap Classloader)**负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。
2.1 加载( 装载)
在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
第一步 “通过一个类的全限定名来获取定义此类的二进制字节流” 并没有强制要求二进制字节流 要从哪里获取、如何获取
2.2 验证
验证是连接阶段的第一步,这一步骤的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,确保这些信息被当作代码运行后不会危害虚拟机自身的安全
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
2.5.类的初始化
类的初始化阶段是类加载过程的最后一个步骤,在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化 类变量和其他资源;
类初始化步骤
- 1、假如这个类还没有被加载和连接,则程序先加载并连接该类
- 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
初始化阶段是执行方法的过程,方法即类/接口初始化方法;
所有类变量(静态变量)的赋值动作和静态代码块static{}中的语句都会在编译时被编译器自动收集,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
-
编译器收集的顺序是由语句在源文件中出现的顺序所决定的,在初始化的时候,静态域变量的初始化和静态代码块的执行会从上到下依次执行
-
JVM保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其余线程需要阻塞等待,直到活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用类加载这一同步控制机制 实现线程安全的单例模式)
执行时机
类/接口初始化方法执行时机: 类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。
**主动使用: **包括以下场景:
-
遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令;
为一个类创建一个新的对象实例时(比如使用new关键字实例化一个对象时);
读取一个类的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令);
调用一个类的静态方法时(即在字节码中执行invokestatic指令);
-
使用 java.lang.reflect 包的方法对类进行反射调用的时候(如果该类还没有进行过初始化,需对其进行初始化);
-
当初始化一个类的时候,发现其父类还没有进行初始化的时候,需要先触发其父类的初始化( JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。);
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类;
被动引用:
-
通过子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
-
通过数组定义来引用类,不会触发此类的初始化;
-
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;
考虑下面的代码:
public class SuperClass {
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("SuperClass Constructor invoke");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("SubClass Constructor invoke");
}
}
public class InitTest {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
在上述代码中,类InitTest
通过SubClass.value
引用了类SuperClass
中声明的静态域value
。由于value
是在类SuperClass
中声明的,只有类SuperClass
会被初始化,而类SubClass
则不会被初始化。
当访问一个Java
类或接口中的静态字段的时候,只有直接定义这个字段的类或接口才会被初始化。
因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
Q: 类初始化和 类构造函数的执行顺序?
只有类的初始化执行完成之后,才会执行类的构造函数 (使用一个类之前,JVM会确保类的初始化已完成)
public class InitTest {
public static void main(String[] args) {
SubClass subClass=new SubClass();
}
}
参考文章:
《深入理解java虚拟机》周志明著