小豪爱上JVM--类加载机制

小豪最近又收到Java后端开发岗位的面试通知了,数次的失败并没有让小豪丧失斗志,反而在不断的跌倒、站起来的过程中越来越强,像个打不死的小强。为了保险起见,小豪准备让宇哥给自己来一次模拟面试。

故事人物背景介绍
小豪: 23岁,武汉某双非本科不知名专业大学四年级学生,成绩一般,面临毕业,对后端开发、Java很感兴趣,正求职找工作。
宇哥: 跟小豪通过租房认识,两人是室友,26岁,毕业后长期从事软件开发工作,是一个半吊子工程师,兴趣爱好是吹牛,不打草稿那种。

听说你学的Java?

小豪:宇哥,我明天要去面试了,给你个机会,当下面试官,考考我,随便问。
宇哥:问啥你都不会啊,有啥好问的,你自己心里没坐标吗?
你会啥
小豪:你这样会失去我的,你快问就完事了,别哔哔赖赖嗷!

宇哥:那我随便问喽,你说你学的Java, 那你了解Java和C++的区别吗,它们的运行方式有什么不同?

小豪:这个我肯定知道呀,Java需要运行时环境,也就是JRE,里面包含了虚拟机和Java核心类库…你能再问的简单点不老哥?你别敷衍我啊。

宇哥:呵呵,年轻!你知道面试的问题一般都是层层递进的夺命连环套吗,越简单的开始往往意味着越悲惨的结局。既然你说到了虚拟机,那你对虚拟机了解吗,Java为什么要通过虚拟机运行?

小豪:首先,虚拟机可以将Java程序编译后的class字节码文件转换为不同的操作系统和CPU指令集能够读懂的二进制机器码,做到**“一次编译,到处运行”;其次,虚拟机的另外一个好处是它带来了一个托管环境,能够帮助我们自动完成内存管理与垃圾回收**,减少内存泄漏和内存溢出问题出现的机率。

宇哥:小豪啊,你这些基础概念了确实记得很清楚,但是往往就因为你这样回答就钻进了面试官的套子。比如,你的回答里面提到了,虚拟机减少了内存泄漏和内存溢出问题出现的概率。面试官马上就可能会问你内存泄漏和内存溢出的区别内存溢出一般发生在虚拟机的哪一块?什么原因会导致发生内存溢出

小豪:宇哥,你别说了,我收拾收拾,不面试了,呜呜呜,怎么老问这些问题嘛,感觉好难呀~

宇哥:这些呀,说是基础,其实跟你日常开发工作都是息息相关的,学习JVM虚拟机,你不仅仅可以知道怎么调优,增强你的程序性能,在你的程序出现bug和问题时,你也能够及时定位,JVM相关的知识还是十分重要的,你必须好好学,说到底,你如果连JVM都不懂,你好意思说你是干Java的吗,凑弟弟。虽然你这次模拟面试算是失败了,不过不要紧,每天进步一点点嘛!

小豪:宇哥,你别暗示我了,我请你喝一点点,你能教教我虚拟机JVM相关的知识不,万一我明天真被问到了这个怎么办。

宇哥:行啊,不过这一晚上可讲不完JVM,就算只挑重点讲,也得讲个几天,这样,我今天带你入个门,后面几天你一边自学,我一边教你,争取把面试中常遇到的JVM问题给解决个七七八八。

小豪:ojb98k,我,JVM,必学会,面试,必没问题!

JVM架构

宇哥:行了行了,我们从JVM架构开始说起,先有个整体认识,再个个击破。我给你画张图,你看好了。

整个结构体系分为上中下三个部分,JVM为了执行Java代码,首先需要将class文件加载到内存中,然后将加载后得到的Java类存放于方法区(Method Area)内;在实际运行时,虚拟机就会执行方法区内的代码,同时,JVM在内存中划分出来存储运行时的数据;用程序计数器来存放各个线程执行位置,控制程序执行流程。就这个图,面试官就有很多可以考你的点:比如:你对类加载机制了解吗?类加载器有哪些?JVM运行时数据区都有哪些块?分别是干什么的?每一个点深挖下去,和实际开发相联系,又会衍生出一连串的夺命连环问。

小豪:宇哥啊,你快教教我吧,我现在就是干瘪的海绵体,需要你的灌溉!

类加载机制

加载过程

宇哥:你有点饥渴啊,那我们先聊聊类加载机制。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行连接、初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

为了搞清楚JVM中类加载的过程,我们从前到后挨个聊。加载是类加载过程的一个阶段,加载主要是为了查找待加载类的字节流,创建该类的java.lang.Class对象。小豪,Java语言的类型一般分为哪两大类?

小豪:基本类型和引用类型呀,引用类型细分为类、接口、数组类、泛型参数四种。

宇哥:是的,Java的基本类型由Java虚拟机预先定义好,泛型参数在编译过程中会被擦除,而数组类是由Java虚拟机直接创建的,类和接口这种非数组类的加载阶段,获取其二进制字节流的动作既可以使用引导类加载器完成,也可以使用用户自定义的类加载器去完成,开发人员可以通过重写类加载器的loadClass()方式达到这个目的。

小豪:宇哥,也就是说,所谓的加载过程就是虚拟机通过类加载器获取类的二进制字节流,将外部的class文件按照虚拟机所需的格式存储在方法区中,然后在内存中实例化一个java.lang.Class的对象,这个对象就是后面程序访问方法区中的类型数据的接口,这样理解对不?

宇哥:是的,总结的不错。以盖房子为例,你先要去找个建筑设计师给你设计房型,几层几室几厅的,这里的房型就是加载的类,设计师就是类加载器。

小豪:这个类加载器我老是听说,原来就是用在这里的呀。

类加载器

宇哥:从架构图中可以发现,类加载器获取类的二进制字节流是在JVM外部实现的,所谓的类加载器,其实就是实现类的加载动作的代码模块, 常见的有启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(Application ClassLoader);

小豪:宇哥,现在我是面试官,我来问你,类加载器有哪些,有什么区别,说说看!

宇哥:这可还行。

  • 启动类加载器负责加载最为基础、最重要的类,比如存放在jre的lib目录下jar包中的类,例如rt.jar包。启动类加载器是由C++实现的,没有对应的Java对象,因此如果你在编写自定义类加载器的时候想要把加载请求委派给它,只能通过null来指代。
  • 扩展类加载器负责加载相对次要、但又通用的类,比如jre中的lib\ext目录中的类库。
  • 应用程序加载器负责加载应用程序路径下的类,如果程序没有自定义过自己的类加载器,这个就是默认加载器。
双亲委派机制

凑弟弟,面试官一般可不会像你这么问,它们之间的关系才是考察的重点,也就是常说的双亲委派模型
小豪:好像在哪听过这个词,啥意思啊,好复杂的样子。

宇哥:其实并不复杂,还是以建房子为例,设计师ACL接了一个设计房子的单子,但他自己不会马上开始干,而是先跑去问问师父EC,看看自己的师父干不干,师父EC也不会马上接手,而是会让祖师爷BC过过目,总之,只有师父不愿意干的情况下,才能自己干。

小豪:我懂了,就是一个类加载器如果接收到了类加载的请求,它首先不会自己尝试加载这个类,而是将这个请求委派给父类加载器去完成,每一个层次的加载器都是如此。可这样做的目的是什么呀,宇哥。

宇哥:给你解释这样做的目的之前,我先问问你,你怎样判断两个类是不是同一个类?

小豪:你别卖关子,我要说就靠类名判断,你肯定说不对。

宇哥:嘻嘻,类的唯一性由类加载器实例以及类的全名一同确定,即便是同一串字节流,经由不同的类加载器加载,会得到两个不同的类

小豪:抢答,我知道了,也就是说,双亲委派机制保证了Java程序中的核心API不被篡改,例如类java.lang.Object,它存放在rt.jar中,无论开发人员使用哪一个类加载器加载这个类,最后都要由老师傅启动类加载器来加载这个类,避免程序中出现由不同的类加载器加载的多个不同的Object类,造成一片混乱。

宇哥:是的,你还有点悟性。
小豪:那当然,这可难不倒我!

宇哥:如果面试官问这么简单就好了,针对这些问题,如果我是面试官,你答上了什么是双亲委派机制,马上就是夺命连环问:实现双亲委派机制的代码逻辑你可以写写看吗?如果程序中不想使用双亲委派机制要怎么办呀,你有啥办法破坏这个机制吗?

小豪:…我能说什么呢,原以为我会了,你又让我知道了原来我啥也不会这个惨痛的事实…

宇哥:哈哈哈哈,我就是要让你明白,你会的越多,不懂的就越多。但是只要我们每天进步一点点,你总会懂的越来越多,不是吗?

小豪:我买还不行吗,波霸奶茶,大杯的,行了吧?
宇哥:可以,七分甜,加冰吧。

小豪:…秀。宇哥,类加载机制的第一步 加载 是不是就这些了,那后面的连接过程和初始化过程呢?

连接过程

宇哥:那接着给你说连接过程。连接过程分为验证、准备、解析三个子阶段。

  • 验证:确保被加载类能够满足Java虚拟机的约束条件。就好比设计师将设计好的房型交给相关机构审核,只有审核机构通过了,才能继续下面的工作。
  • 准备:为被加载类的静态字段也就是类变量分配内存,并设置初始值。这些类变量使用的内存都是在运行时数据区里面的方法区中进行分配的,并且这个时候进行内存分配的只包括类变量,不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。并且这里的初始值都是对应的数据类型的零值,而不是你给静态变量的具体赋值。

小豪:我知道啥意思,比如一个类变量定义为:

public static int value = 123;

那变量value在准备阶段过后的初始值是0而不是123

宇哥:对的哦,不过有特殊情况,如果一个类变量用final修饰了,比如:

public static final int value = 123;

在准备阶段虚拟机就直接将其值设置为123了哦。
总之,准备阶段就相当于在审核之后,我们的房子盖成了毛坯房。

  • 解析:虚拟机将常量池内的符号引用替换为直接引用。你肯定会问什么是符号引用,什么是解析引用。在我给你详细的分析Java虚拟机是如何执行方法调用之前呢,你可能不会很明白这两个概念到底是什么。我在这里先给你下个定义,等到以后深入了解了后,我们再回过头来看。

符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。相当于你终于知道那个设计师最后把房子给你建在哪了。

初始化

加载、连接过程之后,就到了初始化过程,这是类加载过程的最后一步,目的是为变量赋初始值,执行<clinit>方法,使得类变成可执行的状态。相当于房子终于装修好了,你就可以入住了。

小豪:哇,想不带一个类加载的过程就这么多故事,我醉了。
宇哥:这才哪到哪呀兄弟。

加载、验证、准备、初始化这几个阶段的顺序是确定的,类的加载必须按照这种顺序开始,但是解析阶段则不一定,为了支持Java语言的运行时动态绑定,解析阶段在某些情况下可以在初始化阶段之后再开始。注意,这里的用词是开始,而不是进行并完成,也就是说这些阶段通常都是互相交叉地混合进行的,通常会在一个阶段执行的过程中调用、激活另一个阶段。这些阶段,都有各自开始的时间,考察最多的就是初始化阶段的触发时间了,虚拟机规范严格规定了有且只有5种情况必须立即执行类的初始化

  1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发其初始化。
    你应该对这个点很熟悉。

小豪:是的,好多关于初始化的题目都是来源这里。
宇哥:哈哈,你应该是被这种题目教育过。上述5种行为称为对一个类的主动引用,除此之外所有引用类的方式都不会触发初始化,被称为被动引用。给你举几个例子让你回忆回忆:

被动使用类字段演示一:通过子类引用父类的静态字段,不会导致子类初始化

public class SuperClass{
	static {
		System.out.println("SuperClass init!");
	}
	public static int value = 123;
}
public class SubClass extends SuperClass{
	static{
		System.out.println("SubClass init!");
	}
}

public class NotInitialization{
	public static void main(String[] args){
		System.out.println(SubClass.value);
	}
}

上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会被触发子类的初始化。对于HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数观察到此操作会导致子类的加载。
如果通过数组定义来引用SuperClass类,不会触发此类的引用,例如

SuperClass[] sca = new SuperClass[10];

该行代码同样不会触发SuperClass类的初始化,因为不符合上述五种触发条件的任意一种。
如果在被调用类中定义了常量,由于常量在编译阶段会存入调用类的常量池中,因此初始化调用类时本质上并没有直接引用到定义常量的被调用类,所以不会触发被调用类的初始化。示例代码如下:

//被调用类
public class ConstClass{
	static{
		System.out.println("ConstClass init!");
	}
	//定义常量
	public static final String HELLOWORLD = "hello world";
}
//调用类
public class NotInitialization{
	public static void main(String[] args){
		System.out.println(ConstClass.HELLOWORLD);
	}
}

上述代码不会输出“ConstClass init”,因为在编译阶段常量值已经存储到了NotInitialization类的常量池中去了,NotInitialization的Class文件之中并没有ConstClass类的符号引用入口。接口也有初始化过程,与类的初始化稍有不同的是在于第三点:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化。

小豪的期待

宇哥:哇,这一点点奶茶属实好喝,不枉费我跟你讲的口干舌燥。

小豪:宇哥啊,你别又只讲这么点呀,运行时数据区数据区还没讲呢,就讲了个类加载,你别停啊,我遭得住的!
宇哥:你不累我累了,明天吧,看明天啥时候下班。你先复习总结下嘛,明天再继续,放心,我肯定给你把JVM整得明明白白。

小豪:哈哈,好,那我复习下,准备明天面试,嘻嘻!

宇哥:其实今天跟你讲的东西已经涉及很多关键点了,就看面试官怎么发散开来问你了。比如:

1. JVM运行时内存划分?PC+虚拟机栈+本地方法栈+堆+方法区+JDK1.7与1.8区别 ?
2. 详细介绍下类加载过程。
3. 双亲委派机制,使用这个机制的好处?破坏双亲委派机制的场景?如何破坏?
4. 了解tomcat的类加载机制吗?

这些问题你都可以尝试着去回答哦,不懂得可以看书,学习,最后问我,加油哟!

小豪:好滴,我会变强的,我要秃头!

参考

深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著. 第2版。北京:机械工业出版社.

更多

觉得文章写的不错,关注、点赞、评论是对我最大的支持!
欢迎关注微信公众号LearnJava,一起学习,一起交流哦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值