Java虚拟机(三)——类加载子系统概述
文章目录
- 类加载子系统
- 虚拟机执行流程
- 类加载子系统作用
- 类加载器角色
- 类加载过程
- 1. 加载(Loading)
- 2. 链接(Linking)
- 验证(verify)
- 准备(Prepare)
- 解析(Resolve)
- 初始化(Initialization)
- 类加载器的分类
- 引导类加载器(启动类加载器)
- 应用程序类加载器(AppClassLoader)
- 扩展类加载器
- 用户自定义类加载器概述
- 实现步骤:
- 关于ClassLoader
- 双亲委派机制
- 工作原理
- 优势
- 打破双亲委派机制的方法
- 沙箱安全机制
- 判断class对象是否为同一个类的两个必要条件
- JVM必须知道由启动还是用户类加载器加载
- Java程序对类的使用方式
- 附:永久代与元空间区别
- 元空间好处:
类加载子系统
本文只简单描述加载类的过程,并不探讨某些过程的细节。
虚拟机执行流程
- 如果想自己手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
类加载子系统作用
- 负责从文件系统或网络中加载Class文件,class文件在开头有特定的标识(即
CA FE BA BE
字节码开头) - ClassLoader只负责Class文件的加载,至于是否可以运行,由Execution Engine决定。
- 加载的类信息存放在方法区中。除了类的信息外,方法区中还会存放运行时的常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器角色
- ClassFile存在本地硬盘上,最终将要加载到JVM中来,根据文件实例化N个一模一样的实例
- ClassFile加载到JVM中,被称为DNA元数据模板,放在方法区
- 在.class文件->JVM ->元数据模板,此过程需要类装载器
类加载过程
1. 加载(Loading)
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象。作为方法区中这个类的各种数据的访问接口
类的全限定名其实就是类的绝对路径,如:Date类的全限定类名就是
java.util.Date
方法区在JDK7以前是永久代(PermGen),JDK8开始,为元空间。附录为永久带与元空间(MetaSpace)区别
加载.Class文件的方式
- 本地系统中直接加载
- 网络获取,如:Web Applet
- 从zip压缩包中读取,成为日后jar、war的基础
- 运行时计算生成,使用最多的是动态代理技术
- 由其它文件生成,如:JSP应用
- 从专有数据库中提取.Class文件,较为少见
- 从加密文件中获取,有防Class文件被反编译的保护措施
2. 链接(Linking)
验证(verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载的正确性,不会危害虚拟机自身安全
- 主要包括 文件格式验证、元数据验证、字节码验证、符号引用验证四种验证
准备(Prepare)
- 为类变量分配内存并设置该类变量的默认初始值,即零值
- 这里不包含用
final
修饰的static
,因为final
在编译的时候就会分配,准备阶段会对其进显式初始化 - 不会为实例变量分配初始化,类变量会分配在方法区中,实例变量是会随着对象一起分配到Java堆中
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》中的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型等,应对常量池中的
CONSTANT_Class_info
、CONSTAN_Fieldref_info
、CONSTANT_Methodref_info
等
初始化(Initialization)
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程 - 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,若不需要合并,则字节码文件中不存在
<clinit>()
方法 - 构造器方法中指令按照语句在源文件中出现的顺序执行
<clinit>()
不同于类的构造器- 构造器是虚拟机视角下的
<init>()
- 构造器是虚拟机视角下的
- 若该类具有父类,JVM会保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕 - 虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁,也就是说,<clinit>()
只会被执行一次
public class ClassInitTest{private static num =1;static{num =2;number = 20;System.out.println(num);//正常System.out.println(number);//非法的前向引用}private static int number = 10;}
public class ClassInit{private static int num =1;static{num =2;number =20;}private static int number = 10;public static void main(String [] args){System.out.println(ClassInitTest.num);//2System.out.println(ClassInitTest.number);//10}
}
上面这个程序中,number的值由如下变化:
- 在连接的prepare步骤中,将其初始化为0
- 在initial步骤中,将其赋值为20
- 然后将其赋值为10
public class ClinitTest{private int a =1;public static void main(String[]args){int b =2;}
}
类加载器的分类
严格来讲分为两类(Java虚拟机规范),引导类加载器(bootstrap class loader)和自定义类加载器(User-Defined ClassLoader)
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将 所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
无论类加载器如何分,最常见的类加载器始终只有3个:
引导类加载器(bootstrap class loader)----> C/C++ 实现的
扩展类加载器(Extension Class Loader)—Java实现
系统类加载器(System ClassLoader)—Java实现
后两者都属于用户自定义类加载器,这四者(bootstrap Class Loader
、Extension Class Loader
、System Class Loader
、User Defined Class Loader
)是包含被包含关系,并非上层下层、继承的关系
public class ClassLoaderTest{public static void main(String [] args){//获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2//获取其上层,扩展类加载器ClassLoader extClassLoader= systemClassLoader.getParent();System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1b6d3586//获取上层引导类加载器ClassLoader bootstrapClassLoader = extClassLoader.getParent();//获取不到,为nullSystem.out.println(bootstrapClassLoader);//对用户自定义类来说,默认使用系统类加载器进行加载System.out.println(ClassLoaderTest.class.getClassLoader()); //sun.misc.Launcher$AppClassLoader@18b4aac2//String由引导类加载器加载--->Java核心类库由引导类加载器加载System.out.println(String.class.getClassLoader()); //null}
}
引导类加载器(启动类加载器)
- 由C/C++ 实现,嵌套在JVM内部
- 用来加载Java的核心类库:
JAVAHOME/jre/lib/rt.jar
、resources.jar
、sun.boot.class.path
路径下的内容 - 用于提供JVM自身需要的类
- 并不集成自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 出于安全考虑,BootStrap启动类加载器只加载包名包含java、javax、sun等开头的类
应用程序类加载器(AppClassLoader)
- Java语言编写,由
sun.misc.Launcher$AppClassLoader
实现 - 派生于ClassLoader类
- 父加载器为扩展类加载器
- 负责加载环境变量Classpath或系统属性
java.class.path
指定路径下的类库 - ** 是程序中默认的类加载器**,java的程序一般都是由它来进行加载
- 通过
ClassLoader.getSystemClassLoader()
方法可以获取该类加载器
扩展类加载器
Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现派生于ClassLoader类
父类加载器为启动类加载器(由启动类加载器加载的,并非是类的继承关系,而是包含关系)
从
java.ext.dirs
系统属性所制定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。 如果用户创建的JAR在此目录下,也会自动由扩展类加载器加载通过
ClassLoader.getSystemClassLoader()
方法可以获取该类加载器
用户自定义类加载器概述
为什么要自定义类加载器?
- 隔离加载类(防止不同框架类路径冲突等)
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
实现步骤:
- 通过继承抽象类
java.lang.ClassLoader
类的方式,实现自己的类加载器 - 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已经不再建议用户去覆盖loadClass方法,而是建议把自定义的类加载逻辑写在findClass()方法中
- 在编写自定义类加载器时,如果没有太过复杂的需求,可以直接继承URLClassLoader类,这样可以避免自己去编写findClass()方法以及获取字节码流的方式,使得自定义类加载器编写更加简洁
关于ClassLoader
- 是一个抽象类,除了启动类加载器,所有类加载器都派生自它
获取classloader的途径
- 获取当前类的ClassLoader:
clazz.getClassLaoder
- 获取当前线程上下文的ClassLoader:
ClassLoader.getSystemClassLoader
- 获取系统的ClassLoader:
ClassLoader.getSystemClassLoader
- 获取调用者的ClassLoader:
DriverManager.getCallerClassLoader
双亲委派机制
Java虚拟机对class文件采用 按需加载的方式,也就是说当需要使用该类时才会将class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是 双亲委派机制 即把请求交由父类处理,它是一种任务委派机制。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
- 如果父类加载器可以完成类加载任务(是该加载器的加载范围),就成功返回,不再使用下层加载器加载;倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。
(引导类加载器--------->扩展类加载器------------> 系统类加载器)
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String----找不到main方法,不会从这里获取String
- 自定义类:Java.lang.testAPI—安全问题,阻止包名定义
- 以上运行均报错
打破双亲委派机制的方法
自定义类加载器,重写loadClass方法;
使用线程上下文类加载器;
沙箱安全机制
public class String {static {System.out.println("this is String ");}public static void main(String [] args){System.out.println("String.main()");}
}
自定义String类,在加载定义String类的时候回率先使用引导类加载器,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中的java/lang/String.class)报错信息说没有main方法,就是因为加载的是rt.jar中的String类。这样可以保证对java核心源码的保护,这就是沙箱安全机制
判断class对象是否为同一个类的两个必要条件
- 类的完整类名必须一致(包括包名)
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
也就是说,即使两个类来源同一个Class文件,但是只要加载的ClassLoader实例对象不同,那么这两个类对象就是不等的
JVM必须知道由启动还是用户类加载器加载
- 如果一个类型是由用户类加载器加载的,那么JVM会 将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
Java程序对类的使用方式
分为主动使用和被动使用
- 主动使用有七种情况:
- 创建类的实例
- 访问某个类或者接口的静态变量
- 调用类的静态方法
- 反射
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:
java.lang.invoke.MethodHandle
实例的接卸结果REF_getStatic
、REF_pubStatic
、REF_invokeStatic
句柄对应的类没有初始化,则初始化
除了以上七种情况,其它使用Java类的方式都被看做是类的被动使用。都不会导致类的初始化
附:永久代与元空间区别
永久代与元空间区别:
永久代使用的是JVM内存,元空间使用的是本地内存,元空间的数据分配只受到本地内存大小限制
元空间与永久代区别是其内存空间直接使用的是本地内存,元空间中没有了字符串常量池;
元空间其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,如Method,Field理论上都与永久代一样,只是划分上更趋于合理,比如说类及相关的元数据的生命周期与类加载器一致,每个加载器都会分配一个单独的存储空间。
元空间好处:
字符串常量池存在于永久代中,容易出现性能问题和内存溢出
类的方法的信息大小又难以确定,因此给永久带的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久带会为GC带来不必要的复杂性,并且回收效率偏低,在永久代中元数据可能会随着每一次赋GC发生而进行移动,而 hotspot虚拟机每种类型的垃圾回收器都要特殊处理永久代中的元数据,分离出来以后可以简化赋GC,以及以后并发隔离元数据等方面进行优化。
Java虚拟机(三)——类加载子系统概述相关推荐
- 《探秘Java虚拟机:类加载子系统的神秘世界》
类加载子系统 类加载器子系统作用 类加载器子系统负责从文件系统或者网络中加载c1ass文件,class文件在文件开头有特定的文件标识. classLoader只负责class文件的加载,至于它是否可以 ...
- Java虚拟机中类加载机制详解
Java虚拟机中类加载机制详解 1,什么是java类加载机制 **首先在java中,是通过编译来生成.class文件(可能在本地,或者网页下载),java的类加载机制就是 将这些.class文件加载到 ...
- 深入Java虚拟机之类加载
深入Java虚拟机_ClassLoader 类加载器深入剖析 Java虚拟机与程序的生命周期 在如下几种情况下,Java虚拟机将结束生命周期 执行了System.exit()方法 程序正常执行结束 程 ...
- 深入理解java虚拟机(三)GC垃圾回收-对象存活算法
文章目录 前言 一.引用计数算法 二.可达性分析算法 三.了解引用 结尾 前言 在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还&qu ...
- 深入理解Java虚拟机(类加载机制)
文章首发于微信公众号:BaronTalk 上一篇文章我们介绍了「类文件结构」,这一篇我们来看看虚拟机是如何加载类的. 我们的源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行.虚拟机 ...
- 深入理解Java虚拟机——JVM类加载机制(类加载过程和类加载器)
一.什么是类加载机制? 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 二.类加载的时机 类 ...
- Java虚拟机:类加载机制详解
版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 大家知道,我们的Java程序被编译器编译成class文件,在class文件中描述的各种信息,最终都需要加载到虚拟机内存才能运行和使用,那么 ...
- 没有为 ucrtbase.dll 加载符号_深入理解Java虚拟机(类加载机制)
上一篇文章我们介绍了「类文件结构」,这一篇我们来看看虚拟机是如何加载类的. 我们的源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行.虚拟机把描述类的数据从 Class 文件加载到内 ...
- 获取虚拟机的唯一标识_JVM笔记:Java虚拟机的类加载机制(附详细思维导图)...
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类加载的流程 类从被加载到虚拟机内存中开始, ...
最新文章
- Error: module pages/utils/util is not defined
- C++11:Lambda表达式(匿名函数)理解
- 【Machine learning】余弦相似度
- 1.QML语法、属性和元素
- sql根据类别拆分上下级_运用SQL对黑五销售数据进行分析
- leetcode309. 最佳买卖股票时机含冷冻期
- php解决mysql主从同步_Mysql读写分离,主从同步实现
- 在 Sublime Text 中使用 SFTP 插件快速编辑远程服务器文件 Sublime-text with SFTP plugin ...
- 轻量化版本优于MobileNet系列 | Tokens-to-Token ViT: Training Vision Transformers from Scratch on ImageNet
- springboot整合 beatlsql
- std::deque简单使用
- oracle表修改语句,Oracle的常用修改表及字段的语句
- js 数组转带空格字符串
- 企业网站排名,关键词选择原则,6个基本策略
- 数据平滑处理——log1p()和exmp1()
- 三星s4流量显示无服务器,揭开隐藏功能的面纱 GALAXY S4使用指南
- JavaScript 中创建对象的方法(读书笔记思维导图)
- word怎么在下一页添加表头_word表格在换页时能否自动在新换页上带上表头,如何设置...
- 5.11 Go语言文本大数据处理(2):文件分割与入库
- (2022 IV) RCBEV
热门文章
- 【BZOJ3997】组合数学,总之是DP就对了
- python counter用法_10个易被忽视但应掌握的Python基本用法
- python处理ppt的插件_几款PPT神器插件,千万不能错过!
- java操作sql数据库_java-JDBC连接数据库并进行SQL操作
- Android WebView:这是一份全面 详细的WebView学习指南
- 五子棋服务端程序java_9网上五子棋对战(java)服务端源码
- 瀑布流式页面布局_微信小程序——实现简单的瀑布流式布局
- 微信小程序动态更改标题栏_微信小程序实现动态设置页面标题的方法【附源码下载】...
- python十进制转换_Python方法如何将普通IP转换为十进制IP
- 可合并堆1:二项堆(Binominal Heap)