死磕jvm体系第(1)篇——《类加载子系统》

jvm内存结构概述

废话不多说,先画图来理解一下万年拦路虎,jvm内存结构:
在这里插入图片描述

抒一下情

说起来入行也已经很久了,java这门语言可谓博大精深,有些东西你自己不去学,那可能一辈子都用不上(别说用不上,可能连碰都碰不上),之前听很多前辈都在敲黑板,要想职业技能上升,就必须要有自驱人格。我等凡人,自驱人格有限,jvm属于java稍微高层次的东西了(说也讽刺。编程技术高层次的都是底层),经常吃不透,所以打算正儿八经的写几篇文章整理一下,本人能力有限,如有不对,请海涵。
jvm因为概念繁多,平时同学们在开发阶段,又多半是写一写业务,接触的比较少,零星的接触一下,又是不成体系,所以打算从上面那张图开始,咱们一部分一篇文章,发扬一下死磕精神,好好讲一讲。
本系列总共有四篇文章,每一篇都会尽量把东西将透彻,所以可能也会比较长,希望大家耐心阅读。
这篇文章即是jvm的开篇,也是系列第一篇,《类加载子系统》。

本篇内容

在这里插入图片描述

一、什么是JVM?

我们从接触java开始,就一天到晚听人家说jvm,但直到入行了n年,写了M多行代码,仍然是但闻其名而不知其所以然,jvm到底什么什么呢?

现实举例

如果你是一家公司的老板,打算把业务拓展到海外(比如英国),如果你不会英语,那就无法直接跟外国公司谈生意,这时候,你肯定会想到找翻译。
在这里插入图片描述
只要有了会英语的翻译,那就能跟英国公司进行沟通了。但如果,随着业务持续扩大,你的生意又扩展到了德国,法国,那么你又必须要找到会德语和法语的翻译了。对不对?
那么,在这个例子当中,你就相当于是操作系统执行引擎,而英国,德国,法国,就相当于java程序逻辑字节码文件,而这些会英语,德语,法语的翻译们就是jvm。外国老板们想要跟你交流,那就必须通过翻译们,把他们的语言转化成汉语,你才能听得懂,才能知道接下来他们要你做什么。

二、JVM和操作系统的关系?

如下图所示:
在这里插入图片描述
不同的翻译对应着不同的语言,而且要相互适配,才能翻译的当,不可能让英语翻译去翻译德语,也不能让德语翻译去翻译法语,翻译们最终的目标就是吧外国语言翻译成你能听得懂的汉语。放在Java中,也就是说jvm的任务就是要让不同系统去解读.class文件,并将这些文件翻译成可统一执行的机器码(可理解为二进制),并驱动它们被执行。操作系统拿到机器码的时候,就会开始执行(可理解成你得到外国公司老板的订单需求后,开始生产)。所以.class文件是在,jvm上被解释才能执行的。
这就是为什么我们老说,java 可以跨平台,可以在不同的操作平台上运行,因为sun(oracle)官方针对不同的操作系统开发出了不同的jvm进行适配(也就是相当于各种翻译)。

三、JVM类加载的过程

既然.class文件需要被JVM解释后才能执行,那.class文件总有个加载的过程吧。
没错,.classs文件当然需要被加载。如下图:
在这里插入图片描述
这个加载的过程很有讲究,图中绿色的部分被称作jvm内部的一个类加载子系统。其中把类加载到jvm的是类加载器。jvm内置了三种类加载器。分别是启动类加载器(也称作根加载器BootstrapClassLoader)、扩展类加载器(ExtentionClassLoader)、系统类加载器(AppClassLoader)、自定义类加载器。这几个东西比较繁琐,估计很多同学有搞不明白了,其实也很正常,这个跟java体系的设计有关。请慢慢往下看。

3.1 启动类加载器(BootstrapClassLoader)

启动类加载器很特殊,因为这三种加载器当中,扩展类加载器和系统类加载器都属于java类,但只有启动类加载器不是,它是属于c++编写的,是jvm的一部分,看不到源码的,这就是为什么经验丰富的程序员把类加载器称之为两种:启动类加载器和其他类加载器。
启动类加载器,加载的是jdk自带的的核心库,也就是jre和jre/lib下的的包,我们可以写一个demo打出来看一下:

public static void main(String[] args) {
		URLClassPath bootstrapClassPath = Launcher.getBootstrapClassPath();
		URL[] urLs = bootstrapClassPath.getURLs();
		for (URL url:urLs) {
			System.out.println(url);
		}
	}

在这里插入图片描述
备注:%20是空格。
讲到这里,很多人又会疑惑了,为什么只加载一部分.class文件(jar包里面就是.class文件)。这是因为jvm启动的时候,并不是一次性把所有的类加载到内存当中,所以才会有这么多种类的加载器,通常它们都只是各自加载其中的一部分.class文件,而启动类加载起,加载的就是一些最基本的java类库,其中也包括了系统类加载器和扩展类加载器(这两哥们其实就是两个java类,属于ClassLoader体系)。它们被加载进来以后,基本上启动类加载器的使命就完成了,可以全身而退了,剩下的就交给别的种类的类加载器工作了。

3.2 扩展类加载器(ExtentionClassLoader)

这个加载器加载的是jre/lib/ext下面的jar包,这里提一下,这些路径是在你配置的环境变量中找到的,这就是为什么我们装完jdk,还要配置环境变量的原因,因为jvm需要找到类库的路径并启动加载,这下懂了不。
demo:

	public static void main(String[] args) {
		URL[] urLs = ((URLClassLoader) (ClassLoader.getSystemClassLoader().getParent())).getURLs();
		for (URL url:urLs) {
			System.out.println(url);
		}
	}

在这里插入图片描述
上面可能有同学会对我的demo有疑问,为啥扩展类加载器我的代码却是“ClassLoader.getSystemClassLoader().getParent()”,字面上的意思似乎是系统类加载器,该不会是我写错了吧?
其实并不是,请注意后面的getParent(),其实类加载器它有一个父子体系,而系统类加载器的父类就是扩展类加载器,所以“ClassLoader.getSystemClassLoader().getParent()”拿到的就是扩展类加载器。希望大家要理解这一点。这个父子关系,就是著名的双亲委派机制,接下来也会讲到。

3.3 系统类加载器

demo:

	public static void main(String[] args) {
		URL[] urLs = ((URLClassLoader) (ClassLoader.getSystemClassLoader())).getURLs();
		for (URL url:urLs) {
			System.out.println(url);
		}
	}

在这里插入图片描述
这个就是我们比较熟悉得了,系统类加载器加载的就是我们平时经常用到的包,我们仓库里面的包,还有我们自己写的代码,怎么样,现在是不是感觉亲切一点了,甚至还有fastJson!!

3.4 自定义类加载器

jdk实现的ClassLoader都是用于加载指定目录的.class,但有些较为极端的需求,可能需要加载一些在别的目录生成的class,比如网络传输的class文件等等。那么这个时候jdk自带的类加载器就无法实现了,这个就需要你自己去实现一个自定义的类加载器了。
实现自定义类加载器很简单,就是**写一个类继承ClassLoader,然后重写它的findClass方法,**此方法主要是让你自己去自定义你的class文件加载地址的。

3.5 JVM启动的入口和流程

java程序运行都需要jvm的支持,所以jvm必须要运行,jvm的入口就是jdk安装目录下的jvm.dll文件。
在这里插入图片描述
这个文件就是用来启动JVM虚拟机的,而sun.misc.Launcher就是该文件的第一行代码,这行代码就是启动类加载器的代码。让我们来看一看简版:

 public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
......

在这里插入图片描述
有人会问了,那启动类加载器(BootstrapClassLoader)在哪里呢?
其实早先就说了,启动类加载器不是java类,所以自然而然在这里看不到,但你只需要知道,在扩展类加载器和系统类加载器被加载之前,启动类加载器就已经被加载了就行。

3.6、双亲委派机制

3.6.1 概念理解

学习这个知识点,其实最大的困惑就是这个双亲委派的理解。什么叫做双亲委派,双亲是什么?其实这是一个比较大的误解,所谓的双亲其实就只是单亲,这个亲的意思的就是“父加载器”。应用类加载器是的父加载器是扩展类加载器,扩展类加载器的父加载器是启动类加载器(实际上因为启动类加载器是c++实现的,所以在类中就是null来表示)。
还要注意一点,父加载器不是父类,这是两个概念,父加载器相当于是子类加载器持有的一个全局变量,当子类加载器无法加载的时候,类将会被交由父加载器去加载。

在这里插入图片描述

3.6.2 有什么好处

双亲委派,看上去挺复杂的,到底为什么要这样?
我跟很多同学一样,在刚开始接触的时候对这个东西感到非常蛋疼,毕竟作为经常写业务代码的人来讲,花在底层研究的时间本来就不多,然而这些个底层的玩意每一个都是不按常理出牌,并不利于理解。其实为什么会不理解,归根结底还是因为对jvm的体系不熟悉。
存在即合理,我们必须相信,官方这样设计,一定是在能够满足需求下的最简单的方案了。所以,希望准备学习这一块的同学们一定要沉下心来,把知识点理解到位,吃透。
那么双亲委派机制到底有什么好处呢?
最关键的好处就在于保护一些核心的class文件不会被篡改,保证系统安全
比如我们经常使用到的java.lang.String这个类就是被我们的启动类加载器加载的,在java程序运行的时候,这个class就已经在jvm加载完毕了,如果此时我们写一个自定义的java.lang.String类,让类加载器加载,jvm中就会有两个java.lang.String类,这两个类的类全限定名都是一样的,那么就无法区分开了,那我们之前写的代码就无法运行了(因为不知道要用哪个java.lang.String类),所以java在区分类的时候不仅仅是根据类的全限定名来区分,还要根据结合类加载器来区分,虽然我们有另个java.lang.String类,但是这两个java.lang.String的类加载器不一样,一个是属于启动类加载器加载,另一个我们自己写的java.lang.String类,是属于我们的自定义类加载器加载的,所以就可以保证程序在执行的时候不会有问题,无论你有几个java.lang.String类,反正jvm在执行的时候就只使用启动类加载器加载的那个java.lang.String类!

3.7 类链接

上面刚说完了类加载和类加载器。现在开始讲类链接。
这个链接,总共分为三块,验证、准备、解析。看上去它又是个比较蛋疼的东西,然而事实上,它的确就是很蛋疼,蛋疼的如果分析的很深入,那你可能就出不来了,所以在这里,我不打算罗里吧嗦讲太多,毕竟学识有限,讲的太深了也不知道对不对。

3.7.1 验证

验证是什么?
其实就相当有你写业务代码的时候的校验逻辑。jvm毕竟是一个很严谨的系统,它不可能把所有.class文件都无条件地加载到内存中去。.class文件也是有规范的,尤其是不同的jdk版本,规范都不一样,如果是不符合规范的.class文件,不仅会影响程序执行,还会对jvm本身产生危害。
验证的内容包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
具体内容如下:

1.文件格式验证: (1)是否以魔数0xCAFEBABE开头。 (2)主、次版本号是否在当前虚拟机处理范围之内。 (3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。 (4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。 …
2.元数据验证: (1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。 (2)这个类是否继承了不允许被继承的类(被final修饰的类)。
(3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
(4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。

3.字节码验证: 主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
… (Halting
Problem:通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确的检查出程序是否能在有限时间之内结束运行。)
4.符号引用验证: 符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容: (1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。 (2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

引用文本

3.7.2 准备

此阶段就是给类变量(也就是静态变量)赋默认值(例如:static int =0;static String =null),不包括被final 修饰的静态变量,因为final修饰的变量在编译阶段就会被分配了,准备阶段会显示已初始化。

注意:

这个阶段不会为任何实例变量分配初始值。仅仅只是类变量!!!类变量在这个阶段被分配内存地址和各个数据类型的零值(比如 int 类型为0,long类型为0l等等)。 各个数据类型对应的零值:

在这里插入图片描述

3.7.3 解析

解析阶段的作用就是将常量池中的符号引用转化为直接引用。主要是针对类或接口、字段、类方法、接口方法这四类。
这个概念相对难理解一点,特别抽象,如果只是看官方描述的话,可能是有些云里雾里。下面我通俗的讲一下。
也就是说,所谓将符号引用转化为直接引用,就是将一些人能够看懂的引用,转化为ivm所能够看懂的引用(可以理解为指针),这个跟虚拟机内部规范有关,举个例子:

User user = new User();

上面这行代码,new user()我们可以看得懂,但是jvm并不知道这是个什么东西,所以这就是所谓的符号引用,在这个阶段,就需要把符号引用转化为直接引用,也就是地址指针,当代码执行到new user的时候,就会定位到user所在的指针,后续的每一个user引用对于虚拟机来说都是一个地址。

3.8 初始化

类的初始化时类加载过程中的最后一步,这一步之前,代码逻辑是不会有任何执行的,不管是什么静态代码,构造方法,还是类代码块,都是要在这一步或者这一步之后才会执行。
JVM 初始化一个类需要经历如下步骤:

1.如果这个类没有被加载、连接,则程序先加载、连接该类;
2.如果该类的直接父类没有被初始化,则先初始化直接父类;
3.如果该类中有初始化语句则先执行初始化语句
4.静态方法只执行一次

关于初始化,其实没必要说太多,关键是要理解一套规则,这种规则是jvm对class处理的执行流程相关,需要记忆,如果没有这方面知识,很多题目做起来就完全没头没脑,最好的方式是在理解的基础上,刷题巩固,协助记忆。
有兴趣的同学可以看看这篇博客:
类的初始化详解

本篇完,欢迎关注下一期 :死磕jvm体系第(2)篇——《JMM内存模型》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值