Java笔记简要总结-类在Java虚拟机中如何玩耍的
Posted 风一样的美狼子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java笔记简要总结-类在Java虚拟机中如何玩耍的相关的知识,希望对你有一定的参考价值。
🍎 博客主页:@风一样的美狼子
🍎 欢迎关注:👍点赞🍃收藏🔥留言
🍎系列专栏: 《云平台实战》、《Linux随你玩-实操》
🍎在阳光下灿烂,风雨中奔跑,泪水中成长,拼搏中展望。🍎
🍎一起加油,去追寻、去成为更好的自己!🍎
文章目录
- 前言
- 1、类
前言
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,那我们使用的类是如何进入JVM 这个神秘世界的了?它在里面都发生了什么?今天我就带领大家揭晓它的 神秘面纱,GO!
1、类
1.1、类装载的执行过程
类装载分为以下 5 个步骤:
- 加载: 根据查找路径找到相应的 class 文件中的二进制数据读入到内存中,并为之创建一个java.lang.Class对象; 通过使用不同的类加载器可以从不同来源加载类的二进制数据,
通常有如下几种来源:
a.从本地文件系统加载
b.从jar包加载
c.通过网络加载
d.把一个Java源文件动态编译,并执行加载- 检查: 检查加载的 class 文件的正确性;(文件格式验证、元数据验证、字节码验证、符号引用验证)
- 准备: 给类中的静态变量分配内存空间,并设置默认初始值;
- 解析: 虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,> >而在直接引用直接指向内存中的地址;
- 初始化: 对静态变量和静态代码块执行初始化工作。
类其实还有其它过程:
使用: 对象都出来了,业务系统直接调用阶段。
卸载: 用完了,可以被GC回收了。
1.2、类加载器种类以及加载范围
类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。
1.2.1、启动类加载器(Bootstrap ClassLoader)
最顶层类加载器,该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader。
负责加载jvm的核心类库,比如java.lang.*等,从系统属性中的sun.boot.class.path所指定的目录中加载类库。他的具体实现由Java虚拟机底层C++代码实现。
1.2.2、扩展类加载器(Extension ClassLoader)
它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录。
从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果把用户的jar文件放在这个目录下,也会自动由扩展类加载器加载。继承java.lang.ClassLoader。
1.2.3、应用程序类加载器(Application ClassLoader)
它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path所指定的目录中加载类,继承自java.lang.ClassLoader。
1.2.4、自定义类加载器(User ClassLoader)
除了上面三个自带的以外,用户还能制定自己的类加载器,但是所有自定义的类加载器都应该继承自java.lang.ClassLoader。比如热部署、tomcat都会用到自定义类加载器。
1.2.5、模块化中的类加载器
JDK9开始引入模块化,是为了能够实现模块化的“可配置封装隔离机制”。
某个类库到底是在模块还是在传统的jar包,只取决于他存放在哪种路径上。
在这之前,如果类路径中确实了运行时依赖的类型,那就只能等程序运行到发生该类型的加载,连接时才会报运行异常。
在JDK9之后,如果启用了模块化进行封装,模块就可以声明对其他模块的显式依赖,这样JVM就能够在启动时验证应用程序的完备性。
1.2.5.1、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、当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,先判断该类是否能够归属到某个系统模块中,如果可以找到归属系统模块,就优先委派给负责那个模块的加载器加载。
因此模块化中的类加载委派关系如下:(与三层类加载器图对比)
1.2.5.2、OSGi模块化服务
在JDK9之后OSGi的热部署成为当下流行的一项优势。
它通过自定义类加载机制实现,每一个程序模块(Bundle)都有一个属于自己的类加载器,当需要更换一个Bundle时,就把Bundle联通类加载器一起换掉,以实现热替换。
在此环境下,类加载器不再需要双亲委派模型的树状结构,而是进一步发展为更加复杂的网状结构。
1.3、 双亲委派是什么
如果一个类加载器收到了类加载的请求,他首先会从自己缓存里查找是否之前加载过这个class,加载过直接返回,没加载过的话他会把这个请求委派给父类加载器去完成,每一层都是如此,类似递归,一直递归到顶层父类。
也就是Bootstrap ClassLoader,只要加载完成就会返回结果,如果顶层父类加载器无法加载此class,则会返回去交给子类加载器去尝试加载,若最底层的子类加载器也没找到,则会抛出ClassNotFoundException。
1.4、为什么需要有双亲委派呢
主要还是防止内存中出现多份同样的字节码,安全。
比如自己重写个java.lang.Object并放到Classpath中,没有双亲委派的话直接自己执行了,那不安全。双亲委派可以保证这个类只能被顶层Bootstrap Classloader类加载器加载,从而确保只有JVM中有且仅有一份正常的java核心类。如果有多个的话,那么就乱套了。比如相同的类instance
of可能返回false,因为可能父类不是同一个类加载器加载的Object。
1.5、既然需要有双亲委派为何还要破坏双亲委派模型呢
Jdbc为什么要破坏双亲委派模型?
以前的用法是未破坏双亲委派模型的,比如Class.forName(“com.mysql.cj.jdbc.Driver”);
而在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个,然后使用的时候就不需要我们手动的去加载驱动了,我们只需要直接获取连接就可以了。
Connection con = DriverManager.getConnection(url, username, password);
为什么JDBC需要破坏双亲委派模式呢?
**原因是原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。**例如,MySQL的mysql-connector-*.jar中的Driver类具体实现的。
原生的JDBC中的类是放在rt.jar包的,是由Bootstrap加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-*.jar中的Driver类是用户自己写的代码,那Bootstrap类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由Application类加载器去进行类加载。
这个时候就引入线程上下文件类加载器(Thread Context ClassLoader),通过这个东西程序就可以把原本需要由Bootstrap类加载器进行加载的类由Application类加载器去进行加载了。
Tomcat为什么要破坏双亲委派模型?
因为一个Tomcat可以部署N个web应用,但是每个web应用都有自己的classloader,互不干扰。
如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果没打破双亲委派模型的话,那么web1加载完后,web2在加载的话会冲突。
因为只有一套classloader,却出现了两个重复的类路径,所以tomcat打破了,他是线程级别的,不同web应用是不同的classloader。
Java spi 方式,比如jdbc4.0开始就是其中之一。
热部署的场景会破坏,否则实现不了热部署。
1.6、如何破坏双亲委派模型
重写loadClass方法,别重写findClass方法,因为loadClass是核心入口,将其重写成自定义逻辑即可破坏双亲委派模型。
1.7、如何自定义一个类加载器
只需要继承java.lang.Classloader类,然后覆盖他的findClass(String name)方法即可,该方法根据参数指定的类名称,返回对应 的Class对象的引用。
1.8、 热部署原理
采取破坏双亲委派模型的手段来实现热部署,默认的loadClass()方法先找缓存,你改了class字节码也不会热加载,所以自定义ClassLoader,去掉找缓存那部分,直接就去加载,也就是每次都重新加载。
1.9、结语
各位看官今天就到此为止,此系列简要笔记后续会不断更新,🍎 欢迎关注:👍点赞🍃收藏🔥留言!
在这里插入图片描述
深入理解JAVA虚拟机读书笔记——虚拟机类加载过程和双亲委派模型
学习参考资料:周志明老师的著作《深入理解Java虚拟机(第3版)》
我们知道Java代码经编译后会生成Class文件,然后都需要加载到虚拟机中才能被运行和使用。
而虚拟机如何加载这些Class文件,Class文件中的信息进入到虚拟机会发生什么变化,接下来就会围绕这两个问题展开。
1.类的生命周期
一个类型从被加载到虚拟机中开始,到卸出内存,它的生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,如下图。
图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班的开始,而解析则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也成为动态绑定)。
注意:这里的加载只是类加载过程的一个阶段
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,也因此为Java应用提供了极高的可扩展性和灵活性,Java天生可以动态扩展的语言特征就是依赖运行期动态加载和动态连接这个特点实现的
2.类加载的过程
类加载的全过程即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。
加载
在加载阶段,Java虚拟机主要完成下面三件事情:
1)通过类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表此类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
对于数组类而言,数组类本身不需要类加载器创建,它是由Java虚拟机直接在内存中创建。但是数组类中的元素还是需要类加载器来完成加载。
验证
验证是连接阶段的第一步,这一阶段的主要目的是确保Class文件的字节流符合《Java虚拟机规范》的全部约束要求,保证这些代码运行后不会危害虚拟机的自身安全。大致会完成下面四个阶段的验证动作:
-
文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理。
主要目的是保证输入的字节流能正确地解析并存储于方法区之内, 格式上符合描述一个Java类型信息的
要求。 这阶段的验证是基于二进制字节流进行的, 只有通过了这个阶段的验证后, 字节流才会进入内存的方法区中
进行存储, 所以后面的3个验证阶段全部是基于方法区的存储结构进行的, 不会再直接操作字节流。 -
元数据验证
第二阶段是对字节码描述的信息进行语义分析, 以保证其描述的信息符合Java语言规范的要求。
主要目的是对类的元数据信息进行语义校验, 保证不存在不符合Java语言规范的元数据信息。
-
字节码验证
第三个阶段将对类的方法体进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
主要目的是通过数据流和控制流分析, 确定程序语义是合法的、 符合逻辑的。
-
符号引用验证
第四个阶段可以看做是对类自身以外( 常量池中的各种符号引用) 的信息进行匹配性校验。
这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候, 这个转化动作将在连接的第三阶段——解析阶段中发生。
对于虚拟机的类加载机制来说, 验证阶段是一个非常重要的、 但不是一定必要( 因为对程序运行期没有影响)的阶段。 如果所运行的全部代码( 包括自己编写的及第三方包中的代码) 都已经被反复使用和验证过, 那么在实施阶段就可以考虑使用参数来关闭大部分的类验证措施, 以缩短虚拟机类加载的时间。
准备
准备阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设初始值的阶段(静态变量分配在方法区)。
需要注意的是:这里仅仅指的是静态变量,而实例变量分配内存要在实例化的时候进行了。这里的初始值并不是将值直接赋值,而是数据类型的”零值“,如下:
public static int value=123;
value的初始值不会被赋成123
,而是0
;赋值成123
要在初始化阶段才能完成。
但假如将代码改成,如下:
public static final int value=123;
这样子就会在准备阶段,为静态常量value
赋值成123了。
解析
解析阶段是将常量池中的符号引用改为直接引用的过程,解析动作主要针对类或接口、 字段、 类方法、 接口方法、 方法类型、 方法句柄和调用点限定符7类符号引用进行。
- 符号引用( Symbolic References) : 符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面
量, 只要使用时能无歧义地定位到目标即可。 - 直接引用( Direct References) : 直接引用可以是直接指向目标的指针、 相对偏移量或是一个能间接定位到
目标的句柄。
初始化
前面几个阶段中,都是有虚拟机主导和控制(除了在加载阶段用户程序可以通过自定义类加载器)。到了初始化这个阶段,就真正开始执行类中定义的Java程序。
在准备阶段, 类变量已经赋过一次系统要求的初始值, 而在初始化阶段, 则根据程序员通过程序制定的主观计划去初始化类变量和其他资源, 或者可以从另外一个角度来表达: 初始化阶段是执行类构造器<clinit>() 方法的过程。根据准备阶段的例子:
public static int value=123;
在这个阶段才会被赋值为123;
3.类加载器
3.1类与类加载器
一个类本身和加载这个类的加载器共同确立才能在Java虚拟机中具有唯一性,也就是说,即使来自同一份Class文件,被不同的加载器加载,两个类也是不”相等“的(这里的相等指的是instanceof和Class类的equals的返回值)。
绝大多数Java程序都会使用到以下3个系统提供的类加载器来加载:
1)启动类加载器(Bootstrap ClassLoader)
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2)扩展类加载器(Extension ClassLoader)
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)应用程序类加载器(App ClassLoader)
负责加载classpath中指定的jar包及目录中class
另外,JVM的类加载机制主要有如下3种:
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
3.2双亲委派模型
如图,各类加载器之间的层次关系就被称为加载器的双亲委派模型。
双亲委派模型的工作过程:如果一个类加载器收到了加载类的请求,它首先会委托其上层类加载器加载,上层接到请求再交给它的上层,依次递归下去,直到启动类加载器。如果本层类加载器不能加载才会让其子层进行加载。
使用双亲委派模型的好处:无论哪一个类加载器加载这个类,最终都会委派给最顶端的启动类加载器,因此Object类在各种加载环境下都能保证是同一个类。假如没有采用这种双亲委派模型进行加载,而是由各自的类加载器进行加载,那么如果自定义了一个java.lang.Object
,那么系统中就可能会有多个Object,这就会导致Java类型体系中最基础的行为都无法保证。
后面还会陆陆续续更新这系列的读书笔记,期待您的关注~~
以上是关于Java笔记简要总结-类在Java虚拟机中如何玩耍的的主要内容,如果未能解决你的问题,请参考以下文章