参考:
https://blog.csdn.net/qq_31806155/article/details/109669037
https://www.zhihu.com/question/60885673/answer/1226356110
https://developer.ibm.com/zh/articles/j-lo-classloader/
一、类加载顺序主要分为三个大阶段:
loading -> linking -> initializing
具体细节见下图
类加载顺序loading:加载
a)通过一个类的全限定名来获取描述该类的二进制字节流。
b)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
c)在内存中(堆中)生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
verification:验证
这一阶段用于确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致分为:文件格式验证、元数据验证、字节码验证、符号引用验证。
preparation:准备
静态变量分配内存,并赋默认值。
这里会导致一个问题:(半初始化状态,DCL加volatile问题,双重检查单例高并发时出现指令重排序问题)。
resolution:解析
把Class文件中常量池中的符号引用转换为直接引用。(在linking阶段中的被称为静态解析,也有可能在initailizing后,被称为动态链接)
initializing:静态变量赋值为初始值,执行静态代码块。
直接引用和符号引用的概念:
参考https://blog.csdn.net/maihilton/article/details/81531878
1.符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
2.直接引用:
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
二、类加载器
类加载阶段通过一个类的全限定名来获取描述该类的二进制字节流,这个过程在JVM外部实现,以便让应用程序自己决定如何去获取所需的类,实现这个动作的代码被称为“类加载器”。
2.1 双亲委派模型和非双亲委派模型
在常规情况下,类加载器是遵从双亲委派模型的,但在默写特定业务场景下双亲委派模型并不能达到想要的结果,又出现了非双亲委派模型的类加载器。
2.1.1 双亲委派模型
双亲委派模型是Java设计者推荐的一种类加载方式,该模型可以保证Java程序的稳定运行,防止重复加载和任意修改。解决了基础类的统一问题,保证了虚拟机的安全性
其工作过程如下:
如上图所示,如果一个类加载器收到了类加载的请求,它不会自己尝试加载这个类,而是把请求委派给父类加载器,每一层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,当父类反馈自己无法完成这个加载请求时(即它的搜索范围没有找到指定的类),子加载器尝试处理加载请求。
如上图所示,大致有四种类加载器:
启动类加载器:这个类加载器负责加载放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的类,用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader ,用户不能使用。
扩展类加载器:这个类加载器由sun.misc.Launcher$ExtClassLoader实现。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类,用户可以使用。
应用类加载器:也叫做系统类加载器,这个类由sun.misc.Launcher$AppClassLoader实现。通过ClassLoader.getSystemClassLoader()获取。它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的,用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
自定义类加载器:开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器。
2.1.2 非双亲委派模型
线程上下文类加载器(context class loader):是从 JDK 1.2 开始引入的。类 java.lang.Thread 中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
在2.1.1中提及的类加载器并不能解决全部的实际问题,具体问题如下:
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供。而这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。,从而增加了线程上下文类加载器(context class loader)。
2.1.3 helloworld小程序main启动过程
下图引用图灵学院诸葛老师的jvm课程:
网友评论