![e4ae8fbb9d39f1e65e8d8a490c20ad9e.png](https://i-blog.csdnimg.cn/blog_migrate/184a9747385c7e3dab163546db78b928.jpeg)
欢迎关注头条号:Java小野猫
Java虚拟机是如何加载Java类的? 这个问题也就是面试常问到的Java类加载机制。在年初面试百战之后,菜鸟喜鹊也是能把这流程倒背如流啊!但是,也只是字面上的背诵,根本就是像上学时背书考试一样。
tonight ! 我们把它映射到实战里,看看如何用代码说明这个流程。
ready! go! ----------------在这之前还是搞点理论吧,不然又要先去百度加载机制流程了。
一、类加载机制(理论部分)
类加载机制有三大过程:加载、链接、初始化。其中链接又细分为验证、准备及解析。
Java语言的类型分为两大类:基本类型和引用类型。Java的基本类型是由Java虚拟机预先定义好的。而引用类型又分为:数组类、类、接口、泛型参数。在JVM中,只存在数组类、类、接口三类,而数组类是直接由Java虚拟机直接生成的,其他两类则有字节流而来。
字节流又是怎么来呢?最常见的还是从字节码文件而来(还可以从网络等而来)。所以,我就以字节码文件分析一下加载机制。
1.加载
加载是指查找字节流,并据此创建类的过程。对于数组类来说,它们是可以由Java虚拟机直接生成。而其他两类,却需要借助类加载器,Java虚拟机才能完成查找字节流的工作。
类加载器与类的关系就像是UI与效果图一样,需要将业务需求翻译成效果图。美丽的UI就是类加载器,效果图就是类。
类加载器有一个模型:双亲委派。
双亲委派模型是每次收到类加载请求时,先将请求委派给父类加载器完成,如果父类加载器无法完成加载,那么子类尝试自己加载
类加载器分为三种,启动类加载器、扩展类加载器、应用类加载器。
其中,
启动类加载器(Application ClassLoader)是负责加载最为基础、最为重要的类,即加载lib目录下核心库
扩展类加载器的父类加载器是启动类加载器,扩展类加载器负责加载相对次要、但又通用的类,即加载libext目录下扩展包
应用类加载器的父类加载器是扩展类加载器,应用类加载器负责加载应用程序路径下的类,即用户路径(classpath)上指定的类库
这三者类加载器的关系可以这样描述:
每当类加载器接收到加载请求时,它会先将请求转发给父类加载器,在父类加载器中没有找到需要的类的情况下,该加载器才会尝试去加载。
比如,应用加载器接收到加载请求, 会先去找父类加载器(扩展类加载器)里有没有需要的类,但该扩展类加载器顶头还有父类加载器(启动类加载器), 所以还要去启动类加载器中查找是否存在需要的类,如果不存在,那扩展类加载器就会自己加载需要的类,然后把引用传递给应用加载器。
类加载器还有一个功能,就是提供命名空间的作用。
在Java虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经过不同的类加载器加载,也会得到两个不同的类。(这里可以读者自己实现一下,用两个扩展类加载器加载同一个类,然后打印一下类信息就可以验证了)
2.链接
链接:将创建成的类合并至Java虚拟机中,使之能够执行的过程。(这里是下面代码要验证一个步骤)
链接分为验证、准备、解析。
验证的目的是为了让被加载的类能够满足Java虚拟机的约束。(关于约束条件,今天暂时不讲。)
准备的目的是为了被加载类的静态字段分配内存。(仅仅是分配内存,并没有在内存中写东西)除此之外,有些Java虚拟机还会用来实现虚方法的动态绑定的方法表。
下一步就是我们链接的重要步骤--解析
对于一个方法的调用,编译器会解析一个符号,这个符号能够无歧义地定位到具体的目标上。这个符号包含目标方法所在类的名字、目标方法的名字、接收的参数类型以及返回值类型的符号引用。
如果符号引用指向一个未被加载的类、字段、方法,那么这个解析将触发加载功能。
3.初始化
初始化:为标记为常量值的字段赋值,以及执行方法的过程。
只有初始化完成之后,类才正式成为可执行的状态。
那初始化这个步骤是什么时候执行呢?
JVM规范枚举了下述多种触发初始化的情况:
![7e63329f6df4affaedabf35fba514dd1.png](https://i-blog.csdnimg.cn/blog_migrate/b8e4906d05e768cdce7b2812631633a4.jpeg)
理论部分讲解完了。(参考链接:https://time.geekbang.org/column/article/11523)
二、类加载机制(Linux查看字节码)
在这,主要是因为网上都是一大串的理论描述,看得也是半信半疑。还不如拿出代码来,让自己也心服口服了。
talk is cheap,show the code!
1.classloader
![4798058d26802780c869e56040f6d7b4.png](https://i-blog.csdnimg.cn/blog_migrate/27ecebf07516c674efcac40d933d1a31.jpeg)
输出如下:
![3503d5b4f22743a42d1535161ecb5803.png](https://i-blog.csdnimg.cn/blog_migrate/37ce2ffdaf3fe5c14c094e73c79e36ee.jpeg)
结果很惊人,为什么父类加载器的父类加载器(我自己取名为爷爷类加载器。。),照理论来说,应该打印的是bootstrapClassLoader。 其实是它隐身了。贴一下代码里的一段注释:
* Returns the parent class loader for delegation. Some implementations may* use null to represent the bootstrap class loader. This method* will return null in such implementations if this class loader's* parent is the bootstrap class loader.
翻译黄色的句子就是:如果父类加载器是启动类加载器,则将返回null
但还是没解决怎么找bootstrapClassLoader啊。
它实际上不是 java.lang.ClassLoader的子类,而是由JVM自身实现的,我们可以通过这个方法得到实现的类路径。
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
2.链接
3.初始化
1.编译的单例类:Singleton.java
![2884bf18d7b2a3720261e311175e8ac4.gif](https://i-blog.csdnimg.cn/blog_migrate/213940e47ffe8cd8308113a0f642fede.gif)
public class Singleton{ private Singleton(){} private static class LazyHolder{ static final Singleton INSTANCE = new Singleton(); static{ System.out.println("LazyHolder."); } } public static Object getInstance(boolean flag){ if(flag) return new LazyHolder[2]; return LazyHolder.INSTANCE; } public static void main(String... args){ System.out.println("---"); getInstance(true); System.out.println("---"); getInstance(false); }}
![2884bf18d7b2a3720261e311175e8ac4.gif](https://i-blog.csdnimg.cn/blog_migrate/213940e47ffe8cd8308113a0f642fede.gif)
2.编译并运行
[root@localhost tmp3]# javac Singleton.java [root@localhost tmp3]# java Singleton------LazyHolder.
3.查看字节码文件(用的新指令:java -verbose:class Singleton)
![f705cda25dd7e734c27c5c43479ff098.png](https://i-blog.csdnimg.cn/blog_migrate/a9f3ad7f38bee92ddf90103954937d99.jpeg)
结合2,3的输出,我想,我该讲点啥。
在两段“-----”中,如果没有打印加载信息,我们就以为是连续输出了。但是并没有。根据加载信息来看,中间还会加载Singleton$LazyHolder这个内部类,而这一步,对应的就是getInstance(true);。说明了啥?说明了前面理论部分的链接的最后一句话:
如果符号引用指向一个未被加载的类、字段、方法,那么这个解析将触发加载功能。
这里注意的是,仅仅调用了加载功能而已,这里只完成了创建类而已,并没有让它继续进行链接和初始化功能。所以在这里,也就不会出现打印“LazyHolder.”的字样。
当使用到了getInstance(true)的时候,由于需要使用到“LazyHolder.INSTANCE;”这个静态常量,而它又与new 构造器方法相连通,此时就满足了理论中初始化的解释:
2.当遇到用以新建目标类实例的new指令时,初始化new指令的目标类3.当遇到访问静态字段的指令时,初始化该静态字段所在的类
在这,大家应该也有疑问:getInstance(false)的时候也有new啊。但是,false时候的new,只是new数组,数组里装的只是加载时候生成的类引用。
大家还可以看下面class字节码就知道,注释那里就标记着很清楚是类引用而已。
Singleton.class
![2884bf18d7b2a3720261e311175e8ac4.gif](https://i-blog.csdnimg.cn/blog_migrate/213940e47ffe8cd8308113a0f642fede.gif)
public static java.lang.Object getInstance(boolean); descriptor: (Z)Ljava/lang/Object; flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ifeq 9 4: iconst_2 5: anewarray #3 // class Singleton$LazyHolder 8: areturn 9: getstatic #4 // Field Singleton$LazyHolder.INSTANCE:LSingleton; 12: areturn LineNumberTable: line 11: 0 line 12: 9 StackMapTable: number_of_entries = 1 frame_type = 9 /* same */ public static void main(java.lang.String...); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS Code: stack=2, locals=1, args_size=1 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String --- 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: iconst_1 9: invokestatic #8 // Method getInstance:(Z)Ljava/lang/Object; 12: pop 13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #6 // String --- 18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 21: iconst_0 22: invokestatic #8 // Method getInstance:(Z)Ljava/lang/Object; 25: pop 26: return LineNumberTable: line 15: 0 line 16: 8 line 17: 13 line 18: 21 line 19: 26 Singleton(Singleton$1); descriptor: (LSingleton$1;)V flags: ACC_SYNTHETIC Code: stack=1, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method "":()V 4: return LineNumberTable: line 1: 0}
![2884bf18d7b2a3720261e311175e8ac4.gif](https://i-blog.csdnimg.cn/blog_migrate/213940e47ffe8cd8308113a0f642fede.gif)
可以看到,在getInstance中,那么#3 和#4 对应的是啥
![332c96b5b63cba72d8c939da1e59b424.png](https://i-blog.csdnimg.cn/blog_migrate/50ed97add7f9c1f1187fdbfbeb4f5b49.jpeg)
欢迎做Java的朋友们私信我【资料】免费获取免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)
其中覆盖了互联网的方方面面,期间碰到各种产品各种场景下的各种问题,很值得大家借鉴和学习,扩展自己的技术广度和知识面。