JVM类加载器详解
前言
在上一篇中,通过下面这幅图大致了JVM整体的内部运行结构图,在JVM的结构中,类加载子系统作为连接外部class文件与真正将class文件加载到运行时数据区,承担着重要的作用
类加载器是什么?有什么作用?
- 读取字节码文件,以二进制流的形式读取
- 解析字节码二进制流的静态数据,转换为运行时JVM方法区的数据
- 生成类的java.lang.class对象,放入堆中,作为方法区的访问入口
- 在加载某个类的过程中,如果存在继承父类的情况,会同时触发父类加载
通俗来讲就是,类加载器确保加载到JVM内存中的字节码信息符合JVM运行时的规范,以便正确的执行class中的信息
下面是一副类加载器加载字节码的简易流程图过程,下面将结合这张图,通俗的对类加载器的执行过程做简单的说明
1、加载阶段(loading)
以某个具体的Class文件加载为例,当程序需要XXX.Class文件时,类加载器将该类的字节码文件读取之后加载到运行时数据区,具体加载到哪里去呢?很明显,首先会去堆中查找一下当前class的实例是否已存在?如果存在了,就直接从堆区取出来加载到方法区即可,否则执行上一个图的加载过程,在反射的使用中,经常看到 XXX.getClass()获取Class的信息就是这个道理
说到这,顺便总结下,Class的实例什么时候创建呢?主要包括下面几种:
- 使用new 关键字
- 反射Class.forName(“类的全限定名”)
- 子类加载时同时加载父类
- JVM启动时,包含main方法的主类
2、链接(linking)
该阶段主要做的事情包括:
- 验证:确保字节码符合虚拟机的规范要求
- 准备:为字段赋予初始值
- 解析:符号引用转为直接引用
验证:
准备:
为类中相关的字段赋予初始值,比如像下面这样的,为类的静态变量赋予初始值
public class A {
private static int XXX =10;
}
解析:
将字节码中的静态字面关联转换为JVM内存中动态指针关联,举例来说,当A类extendsB类的时候,从语义上分析来说,表现出下面这样
即类加载器加载class时候读取到的A类和B类之间的关系可以理解为使用字面量的表达,但是JVM并不认识啊,在JVM中,需要使用一种新的形式去描述它们之间的关系,即动态指针的关联
而具体解析字节码中的信息包括下面几种:
- 类解析
- 字段解析
- 方法解析
- 接口解析
3、初始化Initialization
初始化阶段要做的事情主要是:执行类的构造方法()的过程
clint完成类的初始化工作,该方法不需要显式声明,由Java编译器自动生成,这里和上面的,加载/验证/准备/解析有所不同的是,上面几个阶段是由虚拟机主导的,与代码无关,
而初始化则是通过代码生成clint来完成类的初始化过程
初始化过程总结:
- 初始化阶段对类(静态变量)赋值与执行static代码块
- 子类初始化过程会优先执行父类初始化clint
- 没有类变量及static代码块就不会产生初始化的clint
- 使用TraceClassLoading可以查看类的加载过程
- clint方法会默认增加同步锁,确保clint的初始化只进行一次
类加载器
上面简单聊了一下JVM类加载器加载时的几个重要的执行步骤,既然说到类加载器,不得不谈谈在JVM中,类加载具体有哪些呢?
按照层次划分,类加载器大概可以分为下面几种:
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtentionClassLoader
- 应用程序类加载器 AppClassLoader
它们之间的关系可以用下图表示:
其中,启动类加载器,扩展类加载器,应用程序类加载器属于JVM自身的加载器,而自定义加载器在使用中更加灵活,不仅可以加载自定义目录中的class,还能加载来自网络输入的二进制流,需要说明的是,它们之间并非是自上而下的继承关系,仅为上下级
启动类加载器:
- 使用C语言开发
- 用于加载Java核心类库
比如 : JAVA_HOME/jre/lib/rt.jar …
${sun.boot.class.path}路径下的jar - 基于沙箱机制,只加载java,javax,sun包开头的类
扩展类加载器:
- Java语言编写
- 上级加载器为启动类加载器
- 加载${JAVA_HOME}/jre/lib/ext扩展目录下的类库,如果你开发的Jar,放入该目录,也会被扩展类加载器加载
应用程序类加载器 (AppClassLoader )
- Java语言编写,由sun.misc.Launcher$AppClassLoader 实现
- 应用程序类加载器是默认的加载器,绝大多数类都是由它加载
- 上级加载器为扩展类加载器
- 它负责加载classpath下的应用程序(三方)类库
下面来简单看一段具体的实例代码
public class TestClassLoader {
public static void main(String[] args) {
//自定义的类,默认使用应用类加载器
ClassLoader classLoader = TestClassLoader.class.getClassLoader();
System.out.println(classLoader);
//扩展类加载器
ClassLoader extClassLoader = SunEC.class.getClassLoader();
System.out.println(extClassLoader);
//启动类加载器,由于启动类加载器是由C语言编写的,不能被JVM管理,所以返回null
ClassLoader bootstrapClassLoader = Object.class.getClassLoader();
System.out.println(bootstrapClassLoader);
}
}
通过定位,可以依次找到各个加载器的位置
类加载器的双亲委派模型
上面我们了解了JVM中常用的几种类加载器,细心的小伙伴应该留意到,3者之间存在着一种上下级关系,这种关系的结构通俗解释就是“双亲委派”
何为“双亲委派”?
- 加载类时,加载器逐级将需要加载的任务向上委派至引导类加载器,然后再逐级向下尝试加载,直至加载完成
- 优点:该机制保护了类不会被重复加载,同时该机制提供了沙箱机制,禁止用户污染java开头的核心包
比如说,当JVM需要运行一个自定义的类时,自定义加载器由于存在上级扩展类加载器和引导类加载器,因此会逐级向上加载,但是上面的2级加载器真正开始加载时,发现自定义的类中的相关信息不在自己这一层加载器所能服务的范围,因此又自顶向下逐级加载,最终来到能够处理的加载器中进行处理
从下面这个简单的案例,可以发现,当我们自定义的类和系统的加载器中的核心包里面的类发生冲突时就报出下面的错误了,这也是双亲委派的沙箱机制的好处
本篇主要讲述了类加载器相关的内容,希望对看到的同学有用,本篇到此结束最后感谢观看!
还没有评论,来说两句吧...