JVM类加载机制
类加载
在JVM虚拟机实现规范中,通过ClassLoader
类加载把*.class
字节码文件(文件流)加载到内存,并对字节码文件内容进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class
对象,这个过程被称作类加载
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么占用很多的内存
类的生命周期
类加载过程
可以通过一句谐音来记忆:“家宴准备了西式菜”,家(加载)宴(验证)准备(准备)了西(解析)式(初始化)菜
加载
在加载阶段,JVM主要完成以下三件事:
- 通过类的完全限定名称获取定义该类的
*.class
字节码文件的二进制字节流 - 将该字节流表示的静态存储结构转换为
Metaspace
元空间区的运行时存储结构。 - 在内存中生成一个代表该类的
class
对象,作为元空间区中该类各种数据的访问入口。
由于 JVM 虚拟机对加载*.class
字节码文件的来源并未做限制,所以出现了以下的 *.class 字
节码文件加载方式:
-
从 JAR 、 EAR 、 WAR 等压缩文件中读取
-
由其他文件或容器生成。例如: 由tomcat将
*.jsp
文件翻译成*.java
文件后,编译生成对应的.Class
字节码文件
在加载阶段完成之后,*.class
字节码文件的类信息数据就会存储在元空间,同时在JVM虚拟机堆中生成一个该类的Class对象
验证
在验证阶段,JVM主要确保*.class
字节码文件中包含的信息复合当前虚拟机的要求,并不会危害虚拟机的安全
验证阶段会完成下面四个阶段的检验:
- **文件格式验证:**验证字节流,是否符合
*.class
字节码文件 格式的规范,且能被当前版本的虚拟机处理 - **元数据验证:**对字节码描述的信息进行语义分析,以保证其描述的信息符合
Java
语言规范的要求 - **字节码验证:**通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的
- **符号引用验证:**生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段一一解析阶段中发生。确保解析动作能正常执行。
Java
语言本身是相对安全的语言,但*.class
字节码文件并不一定要求用Java
源码编译而来,可以使用任何途径,甚至可以用十六进制编译器直接编写来产生*.class
字节码文件
类的加载是 JVM
针对 *.class
字节码文件的读取加载机制,所以虚拟机如果不检查输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
另外,通过类加载机制的验证环节,可以增强解释器的运行期执行性能。因为,解释器在运行期间无需再对每条执行指令进行检查。
准备
- 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存
- 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,且类加载只执行一次,实例化可以进行多次
- 初始值一般为0值
public static int value =123;
例如:下面的常量value被初始化为123而不是0
public static final int value =123;
解析
将常量池的符号引用替换为直接引用。(就是把常量池中值的地址直接替换为值)
初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器<clinit>()
方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序指定的主观计划去初始化类变量和其他资源
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问
例如:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后开你
public class Test{
static{
i=0; // 给变量赋值可以正常编译通过
System.out.print(1);// 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
由于父类的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如下面代码:
public class Parent{
public static int A =1 ;
static {
A=2;
}
static class Sub extends Parent{
public static int B=A;
}
public static void main(String[] args){
System.out.println(Sub.B); // 2
}
}
虚拟机会保证一个类<clinin>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类<clinit>()
方法,其他线程都会阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉
类加载的时机
主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:
-
当遇到
new、getstatic、putstatic、invokestatic
这4条字节码指令时,比如new
一个对象,读取一个静态字段(未被final修饰)、或调用一个类的静态方法时 -
加载一个类,如果其父类还未加载,则先触发该父类的加载
-
当一个接口中定义了 JDK8 新加入的默认方法 (被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载
被动引用
除主动引用之外,所有引用类的方式都不会触发加载,成为被动引用
-
System.out.println(SubClass.value); // value字段在SubClass类的父类中定义
-
通过数组定义引用类,不会触发此类的加载。该过程会对数组进行加载,数组类是一个由虚拟机自动生成的,直接继承自Object的子类,其中包含了数组的属性和方法
SuperClass[] sca = new SuperClass[10];
-
常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的加载
System.out.println(ConstClass.HELLOWORLD);
类加载器
类加载器
在类加载过程的加载阶段,通过类的完全限定名,获取描述 类的二进制流的实现类,被称为“类加载器”
类加载器分类
从JVM虚拟机的角度来讲,只存在两种不同的类加载器:
从Java开发人员的角度来看,类架子啊其可以划分得更细致一些:
-
启动类加载器:
类加载器负责将存放在
<JRE_HOME> lib
目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar ,名字不符合的类库即使放在 ib 目录中也不会被加载)类库加载到虚拟机内存中。例如java.util.*
,java.io.**
,java.lang.*
类等常用基础库都是由启动类加载器加载。启动类加载器无法被 java程序直接引用。 -
扩展类加载器:
该类加载器是由 ExtClassLoader ( sun.misc.Launcher$ExtclassLoader ) 实现,负责将
<JRE HOME>/lib/ext
或者被java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,例如 swing 系列、内置的 js引擎、xm 解析器等以 javax 开头的扩展类库都是由扩展类加载器加载,开发者可以直接使用扩展类加载器。 -
应用程序类加载器:
类加载器是由 AppclassLoader(sun.misc.Launcher$AppclassLoader ) 实现。由于这个类加载器是 ClassLoader 中的 getSystemclassLoader() 方法的返回值,因此也被称为系统类加载器。它负责加载用户类路径 ( ClassPath ) 上所指定的类库,比如: 我们自己编写的自定义类或第三方 jar 包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
什么情况下需要自定义类加载器
- 隔离加载类。在某些框架内进行中间件与应用的模块之间进行隔离,把类加载到不同的环境
- 修改类加载方式
- 扩展加载源。比如: 从数据库、网络、电视机顶盒进行类加载
- 防止源码泄漏。比如: 编译时字节码进行加密,需要通过自定义类加载器对字节码进行解密还原
双亲委派模型
应用程序是由三种类加载器相互配合,从而实现类加载,除此之外还可以加入自己定义的类加载器
类加载器之间的层级关系,称之为双亲委派模型。该模型要求除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系来实现,而不是继承关系
双亲委派工作机制
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载
双亲委派的作用
例如:java.lang.Object存放在 rt.jar 中,如果编写另外一个java.lang.Object 并放到ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在ClassPath 中的 Obiect 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。 rt.jar 中的 object 优先级更高,那么程序中使用的所有的Object,都是由启动类加载器所加载的Object。
双亲委派的实现源码
以下是抽象类java.lang.ClassLoadr的代码片段,其中的loadClass()方法允许过程如下:先检查是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException,此时尝试自己去加载
SPI打破双亲委派
SPI是一种服务发现机制,它通过ClassPath路径下的META-INF/services
文件夹查找文件,自动加载文件里所定义的类
如下图:SPI核心类定义在rt.jar
中(例如:java.lang.Driver接口),所以java.lang.Driver接口本身是由启动类加载器加载,调用java.lang.Driver接口的实现时,启动类加载器无法加载实现类,这个时候就提供了线程上下文类加载器(Thread Context ClassLoader)加载实现类,ThreadContextClassLoader是可以通过java.lang.Thread
,#setContextClassLoader
方法设置类加载器,这样就打破了双亲委派的类加载模式
对象的创建过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有“指针碰撞”和“空闲列表” 两种
选择以上两种方式中的哪一种,取决于Java堆内存是否规整,而Java堆内存是否规整,取决于GC收集器的算法是‘标记-清楚’,还是‘标记–整理’
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。另外根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行init构造方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <int>
构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来
原文地址:https://blog.csdn.net/HakerDONG/article/details/134663223
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_21982.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!