JVM学习.03 类加载机制
Posted 有一只柴犬
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习.03 类加载机制相关的知识,希望对你有一定的参考价值。
1、前言
从事Java开发工作的都知道,Java程序提交到JVM运行时,需要编译成Class文件,才能被JVM加载运行。那么这些Class文件进入到虚拟机后会发生什么?以及Class是如何被加载的?这些都是本文要讲解的部分。
2、类加载时机
所谓类装载机制,就是虚拟机把class文件加载到内存,并对数据进行校验,转换解析,初始化,形成可以虚拟机直接使用的java类型,即java.lang.Class。
一个类从被加载到虚拟机内存开始,到卸载出内存位置,他都会经历加载,验证,准备,解析,初始化,使用,卸载七个阶段。其中验证、准备、解析三个部分称为连接。
类的生命周期如下,网上借来的图:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。特别需要注意的是,类的加载过程必须按照这种顺序按部就班地“开始”,而不是按部就班的“进行”或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。
2.1、“加载”时机
类加载过程的第一个阶段加载,通常是交由虚拟机具体的实现来自由把握,《Java虚拟机规范》并没有强制约束。
2.2、“初始化”时机
《Java虚拟机规范》虽然对加载没有强制性约束,但是却严格规定了有且只有六种情况下必须立即对类进行“初始化”,这里加载,验证,准备需要在此之前开始。
1、遇到new、getstatic、putstatic或invokestatic这四条字节码指令,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
使用new关键字实例化对象的时候;
读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5、 当使用JDK7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
6、当一个接口定义了JDK8新加入的默认方法(被default关键字修饰的接口方法时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化)。
以上6中场景中的行为称为对一个类型进行的主动引用。
除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。何为被动引用?看下面代码:
示例代码一:
/**
* 通过子类引用弗雷静态字段,子类不会初始化
*/
public class SuperClass
static
System.out.println("super class init");
public static int valueOf = 123;
public class SubClass extends SuperClass
static
System.out.println("sub class init");
// 主函数调用
public class Test
public static void main(String[] args)
System.out.println(SubClass.valueOf);
运行结果,只触发了父类的初始化:
示例代码二:
/**
* 常量在编译阶段会进入调用类的常量池中,本质上没有直接引用定义常量的累,所以不会触发常量定义累的初始化
*/
public class ConstClass
static
System.out.println("ConstClass init");
public static final String CONSTANTS = "hello world";
// 主函数调用
public class Test
public static void main(String[] args)
// System.out.println(SubClass.valueOf);
System.out.println(ConstClass.CONSTANTS);
运行结果:
3、类加载过程
3.1、加载
加载阶段,主要完成以下三件事:
1、通过一个类的全限定名来获取定义此类的二进制流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
简单的说,该过程就是查找并通过类加载器将class文件导入到内存中。
3.2、验证
该阶段的目的是确保class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身安全。
3.2.1、文件格式验证
该阶段的主要目的是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机所处理。保证输入的字节流能正确地解析并存储与方法区之内,格式上符合描述一个Java类型信息的要求。
验证比如:
1、验证主、次版本号是否在当前Java虚拟机接受范围之内。
2、常量池的常量中是否有不被支持的常量类型。
......
3.2.2、元数据验证
该阶段是对字节码描述信息进行语义分析,以保证符合《Java语言规范》要求。
验证比如:
1、这个类是否有父类。
2、这个类的父类是否继承了不允许被继承的类(如final修饰的类)。
3、如果这个类不是抽象类,是否实现了父类或接口中要求实现的所有方法。
......
3.2.3、字节码验证
该阶段的目的是通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。元数据验证是对元数据信息中的数据类型校验,而该阶段则是要对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
验证比如:
1、如在操作栈防止了一个int类型数据,使用时却按long类型加载入本地变量表。
2、保证任何跳转之历经都不会跳转到方法体之外的字节码指令上。
......
3.2.4、符号引用验证
该阶段的目的是确保解析行为能正常执行,如果无法通过验证,将抛出Java.lang.IncompatibleClassChangeError的子类异常,如常见的IllegalAccessError,NoSuchFieldError,NoSuchMethodError等。
验证比如:
1、符号应用的类、字段、方法的可访问性,是否可以被当前类访问(private,public等等)。
2、符号引用中通过字符串描述的全限定名是否能找到对应的类。
......
3.3、准备
该阶段正式为类中定义的变量(静态变量)分配内存并给类变量设值初始值。
注:该阶段进行内存分配的仅仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆。
如:
public static int value = 123;
变量value在准备阶段后,初始值是0,而不是123。因为这时候还未开始执行任何java方法,而value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法中,所以value赋值为123的动作要到类的初始化阶段才会被执行。
当然,有些“意外情况”。如类字段的字段属性表中存在ConstantValue属性,那在准备阶段就会被初始化为ConstantValue属性所指定的初始值。
如:
public static final int value = 123;
加上final之后,编译时会为value生成ConstantValue属性,也会在初始化时直接设置value的值为123。
3.4、解析
该阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
主要解析类型有:
类或接口的解析
字段解析
方法解析
接口方法解析
3.5、初始化
该阶段是类加载过程的最后一个步骤。前面几个类加载动作中,出了在加载阶段用户可以通过自定义类加载器的方式局部参与外,其余全部都由JVM自主控制完成。直到初始化阶段,JVM才真正开始执行类中编写的Java程序代码。
3.6、小结
前面几点说了那么多,简单的说就是,当我们程序定义了一个类,当我们需要使用到这个类的时候,JVM会从相应的class字节码文件中去加载,期间进行语义检查,权限的校验,预先的初始处理等,最终初始化应用程序中的构造。就完成了类在JVM中的整个加载过程,也可以直接被JVM所正常运行。
初始化过程:
如果类还没有被加载和连接,那就先进行加载和连接
如果类存在父类,并且父类没有初始化,那就先初始化直接父类
如果类中存在初始语句,顺序执行初始化语句
类的初始化阶段是执行类构造器方法clinit()的过程
1、类加载就是执行Java程序编译之后在字节码文件中生成的clinit()方法(称之为类构造器),clinit()方法由静态变量和静态代码块组成。
2、子类的加载首先需要先加载父类,如果父类为接口。则不会调用父类的clinit方法。一个类中可以没有clinit方法。
3、clinit方法中的执行顺序为:父类静态变量初始化,父类静态代码块,子类静态变量初始化,子类静态代码块。
4、clinit()方法只执行一次。
4、类加载器
实现类加载阶段中“通过一个类的全限定名来获取描述该类的二进制字节流”的动作的代码,称为类加载器。
对于Java中任意一个类,都必须由加载他的类加载器和这个类本身一起共同确立其在JVM中的唯一性,每一个类加载器都拥有一个独立的类名称空间(后续如果接触到模块化系统,如OSGi中,每一个Bundle就具有一个类加载器,这个时候不同类加载器就算再同一个JVM中,上下文也不会共享)。
通常,我们会描述两个类比较是否相等,这个比较的前提是只有这两个类再同一个类加载器加载才有意义,否则就算这两个类是来源同一个class文件,被同一个JVM加载,只要类加载器不同,那就必定不相等(这里的相等包括equals()方法,isInstance()方法,当然也包括了instanceof关键字)。
如:
import java.io.IOException;
import java.io.InputStream;
/**
* @author Shamee loop
* @date 2023/3/23
*/
public class ClassLoaderDemo
public static void main(String[] args) throws Exception
Object classLoaderDemo2 = createOneClassLoader().loadClass("ClassLoaderDemo").newInstance();
System.out.println("classLoaderDemo2实例的类加载器:" + classLoaderDemo2.getClass().getClassLoader());
System.out.println("ClassLoaderDemo的类加载器:" + ClassLoaderDemo.class.getClassLoader());
System.out.println("两个类是否相等:" + (classLoaderDemo2 instanceof ClassLoaderDemo));
/**
* 模拟一个新的类加载器
* @return
*/
static ClassLoader createOneClassLoader()
return new ClassLoader()
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException
String fieldName = name.substring(name.lastIndexOf(".") + 1) + ".class";
try (InputStream inputStream = getClass().getResourceAsStream(fieldName);)
if(inputStream == null)
return super.loadClass(name);
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
catch (IOException e)
throw new ClassNotFoundException(name);
;
运行结果:
4.1、三层类加载器
从JDK1.2以来,Java一直保持着三层类加载器以及双亲委派的类加载结构。我们先来说什么是三层类加载器。
4.1.1、启动类加载器 Bootstrap Class Loader
JVM自带的引导类加载器,由C/C++语言实现。
该类加载器主要负责加载存放在\\lib目录,或被-Xbootclasspath参数指定的路径中存放的,而且是JVM能够识别的类库,如rt.jar,tools.jar等。
只能加载java,javax,sun开头的包名类,如果自定义java,sun开头的包名类会直接报错。
4.1.2、扩展类加载器 Extension Class Loader
Java代码形式实现的sun.misc.Launcher$ExtClassLoader。
加载\\lib\\ext目录,或被java.ext.dirs系统变量所指定的路径中的类库。
允许用户将类库放置在ext目录以扩展JavaSE功能。
指定Bootstrap Class Loader为父加载器,通过getParent()可以获取Bootstrap Class Loader。
4.1.3、应用程序类加载器 Application Class Loader
Java代码形式实现的sun.misc.Launcher$AppClassLoader。
负责加载用户类路径(ClassPath)上的所有类库,Java程序中可以直接使用这个类加载器。
指定Extension Class Loader为父加载器,通过getParent()可以获取Extension Class Loader。
默认的类加载器,Java应用的类都是该类加载器加载的。
4.1.4、如何自定义Class Loader
什么时候需要自定义ClassLoader?
1、修改类的加载方法,如tomcat中多个war工程可以独立运行;保证了各个war中的jar不会冲突。
2、防止源码泄露,对class字节码进行编码加密,再在laod过程中对其解密。
......
如何自定义Class Loader?
1、继承ClassLoader,重写loadClass方法
2、继承UrlClassLoader
5、双亲委派
5.1、双亲委派模型
从4.1小节中可以看出三层类加载器的一定关系。当然我们还可以加入自己定义的类加载器来进行扩展。因此就有了如下的类加载器协作关系(也就是经常被提到的双亲委派模型)。
网上借来的图:
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都必须有自己的父类加载器。
5.1.1、双亲委派的加载过程
1、如果一个类加载器(比如User ClassLoader)收到了类加载请求,首先不会自己尝试加载这个类;
2、把这个请求委托给父亲加载器(如Application Class Loader)去完成;
3、父加载器会继续委托给上一层类加载器(如Extension Class Loader)去完成;
4、最终都会传送到顶层的启动类加载器(Bootstrap Class Loader)中;
5、只有当父加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成加载。
简而言之,也就是逐层向上寻找合适的加载器进行加载,从而保证此类所有的加载器只加载一次。从java.lang.ClassLoader源码中我们也可以看到:
5.1.2、双亲委派的好处
1、Java中的类随着他的类加载器一起具备了一种有优先级的层次关系。能够保证类不会被重复加载。
2、保护程序安全,防止核心Java语言环境被破坏。比如定义一个java.lang.String,在定义一个static语句,你会发现永远无法执行你定义的static内容。如下:
/**
* @author Shamee loop
* @date 2023/3/23
*/
public class String
static
System.out.println("我是自定义的String");
public static void main(String[] args) throws ClassNotFoundException
String s = new String();
执行结果:
5.2、打破双亲委派
既然前面讲到了双亲委派的诸多好处,那么这里为什么要破坏这样的一个环境呢?
试想一下这样一种“例外”情况。双亲委派机制很好的解决了各个类加载器写作时基础类型一致性的问题(越基础的类越往上层加载)。但是如果有基础类型有需要回调用户的代码呢?该如何处理?
比如JNDI服务,JNDI服务存在的目的就是为了对资源进行查找和几种管理,他需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供接口(SPI)的代码。为了解决这个问题,Java设计团队引入了线程上下文类加载器(Thread Context Class Loader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法来设置。显然这个有点类似“舞弊”的操作。正是如此,JNDI可以通过这个线程上下文类加载器去加载所需的SPI服务代码,实际上打通了双亲委派模型的层次结构,来逆向使用类加载器。
所以Java中涉及SPI的加载基本都是采用该方式,如后面的JDBC,JAXB等。
直到JDK6时,JDK提供了java.util.ServiceLoader类来替代前面不太优雅的SPI硬编码的方式,可以通过META-INF/services中的配置信息来解决。
6、模块化中的类加载器
JDK9开始模块化的引入,是为了能够实现模块化的“可配置封装隔离机制”。而该机制首先要解决的便是JDK9之前基于类路径查找以来的可靠性问题。
在这之前,如果类路径中确实了运行时依赖的类型,那就只能等程序运行到发生该类型的加载,连接时才会报运行异常。
在JDK9之后,如果启用了模块化进行封装,模块就可以声明对其他模块的显式依赖,这样JVM就能够在启动时验证应用程序的完备性。
JDK9中,为了使得可配置封装隔离机制能够兼容传统的类路径查找机制,提出了与“类路径(ClassPath)”相对应的“模块路径(ModulePath)”。简单的说,就是某个类库到底是在模块还是在传统的jar包,只取决于他存放在哪种路径上。
模块化系统除了JDK9以外,还有不得不提的OSGi模块化服务了。OSGi的热部署成为当下流行的一项优势。它通过自定义类加载机制实现,每一个程序模块(Bundle)都有一个属于自己的类加载器,当需要更换一个Bundle时,就把Bundle联通类加载器一起换掉,以实现热替换。在此环境下,类加载器不再需要双亲委派模型的树状结构,二十进一步发展为更加复杂的网状结构。
附带一张osgi类加载器(网上借的图):
最后说一下JDK9中类加载器的变化:
1、扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
2、Java类库不再保留\\lib\\ext,JDK已基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD)。
3、取消了\\jre目录,因为随时可以组合构建出程序运行所需的jre,如我们只需要使用java.base模型中的类型,那么随时可以打包出一个jre,需要如下命令:
jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre
4、平台类加载器(Platform Class Loader)和应用类加载器(Application Class Loader)都不再派生自java.net.URLClassLoader,而全部继承jdk.internal.loader.BuiltinClassLoader。
5、当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,先判断该类是否能够归属到某个系统模块中,如果可以找到归属系统模块,就优先委派给负责那个模块的加载器加载。
因此模块化中的类加载委派关系如下:(与三层类加载器图对比)
网上借的图:
7、小结
本篇整理了类的整个加载机制,流程,以及JVM进行了那些动作,加载原理以及加载对于整个程序的意义。希望对于Java程序在JVM内的执行有了更深层次的了解。后续还会努力更新中......一起加油学习吧。
一夜搞懂 | JVM 类加载机制
前言
本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
学习导图
一.为什么要学习类加载机制?
今天想跟大家唠嗑唠嗑Java
的类加载机制,这是Java
的一个很重要的创新点,曾经也是Java
流行的重要原因之一。
Oracle
当初引入这个机制是为了满足Java Applet
开发的需求,JVM
咬咬牙引入了Java
类加载机制,后来的基于Jvm
的动态部署,插件化开发包括大家热议的热修复,总之很多后来的技术都源于在JVM
中引入了类加载器。
如今,类加载机制也在各个领域大放异彩,在面试中,由类加载机制所衍生出来各类面试题也层出不穷。
所以,我们要了解下类加载机制,为工作中或者是面试中实际的需要打好良好的基础。
二.核心知识点归纳
2.1 概述
Q1:JVM
类加载机制定义:
虚拟机把描述类的数据从Class
文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java
类型的过程
Q2:特性
运行期类加载。即在Java
语言里面,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java
程序的高度灵活性
什么是运行期,什么是编译期?
- 编译期是指编译器将源代码翻译为机器能识别的代码,
Java
被编译为Jvm
认识的字节码文件- 运行期则是指
Java
代码的运行过程
JVM
运行期动态加载+动态连接->Java
的动态扩展特性
2.2 类加载的过程
类从被加载到虚拟机内存中开始、到卸载出内存为止,整个生命周期包括七个阶段:
-
加载
-
验证
-
准备
-
解析
-
初始化
-
使用
-
卸载
其中,验证、准备、解析这3个部分统称为连接,流程如下图:
注意:
- 『加载』->『验证』->『准备』->『初始化』->『卸载』这五个阶段的顺序是确定的,而『解析』可能为了支持
Java
的动态绑定会在『初始化』后才开始- 上述阶段通常都是互相交叉地混合式进行的,比如会在一个阶段执行的过程中调用、激活另外一个阶段
想要了解Java
动态绑定和静态绑定区别的话,可以看下这篇文章:理解静态绑定与动态绑定
2.2.1 加载
Q1:任务
- 通过类的全限定名来获取定义此类的二进制字节流。如从
ZIP
包读取、从网络中获取、通过运行时计算生成、由其他文件生成、从数据库中读取等等途径......
想要详细了解类的全限定名的知识,可以看下这篇文章:全限定名、简单名称和描述符是什么东西?
- 将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义
- 在内存中生成一个代表这个类的
java.lang.Class
对象,它将作为程序访问方法区中的这些类型数据的外部接口
2.2.2 验证
- 是连接阶段的第一步,且工作量在
JVM
类加载子系统中占了相当大的一部分 - 目的:为了确保
Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
由此可见,它能直接决定
JVM
能否承受恶意代码的攻击,因此验证阶段很重要,但由于它对程序运行期没有影响,并不一定必要,可以考虑使用-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
-
检验过程包括下面四个阶段:
A.文件格式验证:
-
内容:验证字节流是否符合
Class
文件格式的规范、以及是否能被当前版本的虚拟机处理 -
目的:保证输入的字节流能正确地解析并存储于方法区之内,且格式上符合描述一个
Java
类型信息的要求。只有保证二进制字节流通过了该验证后,它才会进入内存的方法区中进行存储,所以后续3个验证阶段全部是基于方法区而不是字节流了 -
例子:
-
是否以魔数
0xCAFEBABE
开头 -
主次版本号是否在
JVM
接受范围内 -
索引值是否有指向不存在/不符合类型的常量
......
-
B.元数据验证:
-
内容:对字节码描述的信息进行语义分析,以保证其描述的信息符合
Java
语言规范的要求 -
目的:对类的元数据信息进行语义校验,保证不存在不符合
Java
语言规范的元数据信息 -
例子:
-
类是否有父类(除了
java.lang.Object
之外,所有类都应有父类) -
父类是否继承了不允许被继承的类(
final
修饰的类) -
如果该类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
......
-
? C.字节码验证:
-
是验证过程中最复杂的一个阶段
-
内容:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
-
目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
-
例子:
-
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现“在操作数栈的数据类型中放置了
int
类型的数据,使用时却按long
类型来载入本地变量表中” -
保证任何跳转指令都不会跳转到方法体外的字节码指令上
......
-
? D.符号引用验证:
- 内容:对类自身以外(如常量池中的各种符号引用)的信息进行匹配性校验
- 目的:确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个
java.lang.IncompatibleClassChangeError
异常的子类 - 注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即『解析』阶段
-
2.2.3 准备
Q1:任务
- 为类变量(静态变量)分配内存:因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在
Java
堆中 - 设置类变量初始值:通常情况下零值
2.2.4 解析
之前提过,解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:以一组符号来描述所引用的目标
- 可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
- 与虚拟机实现的内存布局无关,因为符号引用的字面量形式明确定义在
Java
虚拟机规范的Class
文件格式中,所以即使各种虚拟机实现的内存布局不同,但是能接受符号引用都是一致的
- 直接引用:
- 可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
- 与虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不同
- 发生时间:
JVM
会根据需要来判断,是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析 - 解析动作:有七类符号及其对应在常量池的七种常量类型
- 类或接口(
CONSTANT_Class_info
)- 字段(
CONSTANT_Fieldref_info
)- 类方法(
CONSTANT_Methodref_info
)- 接口方法(
CONSTANT_InterfaceMethodref_info
)- 方法类型(
CONSTANT_MethodType_info
)- 方法句柄(
CONSTANT_MethodHandle_info
)- 调用点限定符(
CONSTANT_InvokeDynamic_info
)
举个例子,设当前代码所处的为类D
,把一个从未解析过的符号引用N
解析为一个类或接口C
的直接引用,解析过程分三步:
- 若
C
不是数组类型:JVM
将会把代表N
的全限定名传递给D
类加载器去加载这个类C
。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作。一旦这个加载过程出现了任何异常,解析过程就宣告失败。- 若
C
是数组类型且数组元素类型为对象:JVM
也会按照上述规则加载数组元素类型- 若上述步骤无任何异常:此时
C
在JVM
中已成为一个有效的类或接口,但在解析完成前还需进行符号引用验证,来确认D
是否具备对C
的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError
异常
Q1:字段(成员变量/域)和属性有什么区别?
- 属性,是指对象的属性,对于
JavaBean
来说,是getXXX
方法定义的- 字段,是成员变量
class Person{
private String mingzi; //mingzi是字段,一般来说字段和属性是相同的,但是这个例子是特例
public String getName(){ //name是属性
return mingzi:
}
public void setName(){
mingzi= "张三";
}
}
2.2.5 初始化
- 是类加载过程的最后一步,会开始真正执行类中定义的
Java
代码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制 - 与『准备』阶段的区分:
- 准备阶段:变量赋初始零值
- 初始化阶段:根据Java程序的设定去初始化类变量和其他资源,或者说是执行类构造器
clinit
的过程
clinit
:由编译器自动收集类中的所有类变量(静态变量)的赋值动作和静态语句块static{}
中的语句合并产生
- 是线程安全的,在多线程环境中被正确地加锁、同步
- 对于类或接口来说是非必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成
clinit
- 接口与类不同的是,执行接口的
clinit
不需要先执行父接口的clinit
,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的clinit
想详细了解clinit
以及其与init
的区别的读者,可以看下这篇文章:深入理解jvm--Java中init和clinit区别完全解析
- 在虚拟机规范中,规定了有且只有五种情况必须立即对类进行『初始化』:
- 遇到
new
、getstatic
、putstatic
或invokestatic
这4条字节码指令时- 使用
java.lang.reflect
包的方法对类进行反射调用的时候- 当初始化一个类的时候,若发现其父类还未进行初始化,需先触发其父类的初始化
- 在虚拟机启动时,需指定一个要执行的主类,虚拟机会先初始化它
- 当使用
JDK1.7
的动态语言支持时,若一个java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,且这个方法句柄所对应的类未进行初始化,需先触发其初始化。
2.3 类加载器&双亲委派模型
每个类加载器,都拥有一个独立的命名空间,它不仅用于加载类,还和这个类本身一起作为在
JVM
中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个类加载器加载,即使它们来源于同一个Class
文件且被同一个JVM
加载,只要加载它们的类加载器不同,这两个类就必定不相等
2.3.1 类加载器
从JVM
的角度,可将类加载器分为两种:
- 启动类加载器
- 由
C++
语言实现,是虚拟机自身的一部分- 负责加载存放在
<JAVA_HOME>lib
目录中、或被-Xbootclasspath
参数所指定路径中的、且可被虚拟机识别的类库- 无法被
Java
程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用null
代替
- 其他类加载器:由
Java
语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
,可被Java
程序直接引用。常见几种:
扩展类加载器
A.由
sun.misc.Launcher$ExtClassLoader
实现B.负责加载
<JAVA_HOME>libext
目录中的、或者被java.ext.dirs
系统变量所指定的路径中的所有类库应用程序类加载器
A.是默认的类加载器,是
ClassLoader#getSystemClassLoader()
的返回值,故又称为系统类加载器B.由
sun.misc.Launcher$App-ClassLoader
实现C.负责加载用户类路径上所指定的类库
自定义类加载器:如果以上类加载起不能满足需求,可自定义
需要注意的是:虽然数组类不通过类加载器创建而是由
JVM
直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建
2.3.2 双亲委派模型
- 定义:表示类加载器之间的层次关系
- 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承关系来实现,而是通过组合关系来复用父加载器的代码
- 工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
- 注意:不是一个强制性的约束模型,而是
Java
设计者推荐给开发者的一种类加载器实现方式 - 优点:类会随着它的类加载器一起具备带有优先级的层次关系,可保证
Java
程序的稳定运作;实现简单,所有实现代码都集中在java.lang.ClassLoader的loadClass()
中
比如,某些类加载器要加载
java.lang.Object
类,最终都会委派给最顶端的启动类加载器去加载,这样Object
类在程序的各种类加载器环境中都是同一个类。相反,系统中将会出现多个不同的
Object
类,Java
类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱
三.课堂小测试
恭喜你!已经看完了前面的文章,相信你对
JVM
类加载机制已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!
Q1:类加载的全过程是怎样的?
Q2:什么是双亲委派模型?
Q3:String
类如何被加载的
上面问题的答案,在前文都提到过,如果还不能回答出来的话,建议回顾下前文
Q4:请你谈谈类加载过程,以Person a = new Person();
为例进行说明
这道题是在牛客的暑假实习
Tencent
一面的面筋上找的,附上标准答案:类的加载过程,Person person = new Person();为例进行说明
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
- 《深入理解Java虚拟机》第3版
- 理解静态绑定与动态绑定
- 全限定名、简单名称和描述符是什么东西?
- 要点提炼| 理解JVM之类加载机制
- 深入理解jvm--Java中init和clinit区别完全解析
- 2019校招Android面试题解1.0(下篇)
- 类的加载过程,Person person = new Person();为例进行说明
- 编译器和运行期
- 腾讯 暑期实习 安卓 一面二面三面 面经分享
以上是关于JVM学习.03 类加载机制的主要内容,如果未能解决你的问题,请参考以下文章