Java中的类加载机制详解

问题:java是如何加载一段代码的?

当我们写了一个.java文件后,编译之后会形成一个.class的字节码文件,而程序运行时,JVM虚拟机就会将这个.class文件加载入JVM内存中,形成一份描述该Class各种信息的元信息对象。
Java语言允许通过程序化方式间接对该Class进行操作,包括获取构造函数、属性、方法等,同时用户也可以借由这个与Class相关的元对象间接调用Class对象的各种方法。

Java将.class文件加载入JVM内存的操作就叫做类装载,而用户通过代码访问调用这些class中的属性、方法就叫做反射(java反射机制详解)。

1.类加载过程

类装载器就是寻找类的字节码文件并构造出类在JVM 内部表示对象的组件。在Java中,类装载器吧一个类装入JVM 中需要经过以下步骤:

1.装载:查找和导入Class文件

加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。类的加载由类加载器完成,类加载器通常由JVM提供

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

2.链接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段,其中解析步骤是可以选择的

(1) 检查:检查载入的class文件数据的正确性 , 其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

(2) 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

(3) 解析: 将类的二进制数据中的符号引用替换成直接引用。

符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。

直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

3.初始化:对静态变量,静态代码块执行初始化工作

准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

2、类加载时机

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(“com.lyj.load”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

除此之外,下面几种情形需要特别指出:

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

3.类加载器

类装载工作由ClassLoder和其子类负责。ClassLoder 是一个重要的Java运行时系统组件,它负责在运行时查找和装入字节码文件。

JVM在运行时会产生三个ClassLoader:

1.根装载器

2.ExtClassLoader(扩展类装载器)

3.AppClassLoader(应用类装载器)

根装载器不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar,charsets.jar等。

ExtClassLoader是ClassLoder的子类,负责装载JRE扩展目录ext下的jar类包;

AppClassLoader负责装载classpath路径下的类包,这三个类装载器存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。

默认情况下使用AppClassLoader装载应用程序的类

4.类加载机制

1. JVM的类加载机制主要有如下3种。

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

特别强调下双亲委派机制: 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式 。

该机制除了能避免类重复加载外,还很好的解决了类加载的安全问题。若一个人写了个恶意的基础类(如java.lang.String) ,在该加载机制的限制下,JVM的 java.lang.String 类就永远是由根装载器来装载,该恶意基础类并不会被加载,成功避免了 java 核心API库被人恶意篡改了。

猜你喜欢

转载自blog.csdn.net/weixin_43828467/article/details/111929483