12

带你深入理解Java类加载机制

陈宇明 2017年04月28日 发布于Android mp.weixin.qq.com

Java的核心是什么?当然是JVM了,所以说了解并熟悉JVM对于我们理解Java语言非常重要,不管你是做Java还是Android,熟悉JVM是我们每个Java、Android开发者必不可少的技能。

带你深入理解Java类加载机制

2017年04月28日 发布,来源:mp.weixin.qq.com

前言

在上一次文章「一文让你明白Java字节码」中,
我们了解了java字节码的解析过程,那么在接下来的内容中,我们来了解一下类的加载机制。

作者博客

http://www.jianshu.com/u/b997cd1b6a9d

文章目录

  1. 为什么要学习类的加载机制?

  2. 引子

  3. 关于加载

    1. 什么是类的加载?

    2. 什么时候对类进行加载呢?

  4. 类加载器

    1. 什么是类加载器?

    2. Java自带有3种类加载器

    3. 双亲委派机制

    4. 为何要双亲委派机制?

  5. 类的连接

    1. 验证阶段。

    2. 准备阶段

    3. 解析阶段

  6. 初始化时机

  7. 类的初始化步骤

  8. 结束JVM进程的几种方式

  9. 结束并回顾

1

为什么要学习类的加载机制?

Java的核心是什么?当然是JVM了,所以说了解并熟悉JVM对于我们理解Java语言非常重要,不管你是做Java还是Android,熟悉JVM是我们每个Java、Android开发者必不可少的技能。如果你现在觉得Android的开发到了天花板的地步,那不妨往下走走,一起探索JAVA层面的内容。如果我们不了解自己写的代码是如何被执行的,那么我们只是一个会写代码的程序员,我们知其然不知其所以然。看到很多人说现在工作难找,真是这样吗?如果我们足够优秀,工作还难找吗?如果我们底子足够深,需要找工作吗?找不到工作多想想自己的原因,总是抱怨环境是没有用的,因为你没办法去改变坏境。如果我们一直停留在框架层面,停留在新的功能层面,那么我们的优势在哪里呢?所以说,我们不仅要学会写代码,还要知道为什么这样写代码,这才是我们的核心竞争力之一。这样我们的差异化才能够体现出来,不信?我们走着瞧......我们第一个差异化就是对JVM的掌握,而今天的内容类加载机制是JVM比较核心的部分,如果你想和别人不一样,那就一起仔细研究研究这次的内容吧。

2

引子

为了看看自己是否掌握了类加载机制,我们看看一道题:

上面是一个Singleton类,有3个静态变量,下面是一个测试类,打印出静态属性的值,就是这么简单。

在往下看之前,大家先看看这道题的输出是啥?如果你清楚知道为什么,那么说明你掌握了类的加载机制,往下看或许有不一样的收获;如果你不懂,那就更要往下看了。我们先不讲这道题,待我们了解了类的加载机制之后,回过头看看这道题,或许有恍然大悟的感觉,或许讲完之后你会怀疑自己是否真正了解Java,或许你写了这么多年的Java都不了解它的执行机制,是不是很丢人呢?不过没关系,马上你就不丢人了。

3

关于加载

a

什么是类的加载?

类的加载指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法去内,然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系.这样才能通过反射找到相应的类信息.

我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。通过它可以找到类的信息,我们来看下源码:

从上面贴出的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。

b

什么时候对类进行加载呢?

Java虚拟机有预加载功能。类加载器并不需要等到某个类被"首次主动使用"时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)。其实就是一句话,Java虚拟机有预加载功能。

4

类加载器

讲到类加载,我们不得不了解类加载器.

a

什么是类加载器?

类加载器负责对类的加载。

b

Java自带有3种类加载器

  1. 根类加载器,使用c++编写(BootStrap),负责加载rt.jar  

  2. 扩展类加载器,java实现(ExtClassLoader)

  3. 应用加载器,java实现(AppClassLoader) classpath

根类加载器,是用c++实现的,我们没有办法在java层面看到;我们接下来看看ExtClassLoader的代码,它是在Launcher类中

static class ExtClassLoader extends URLClassLoader

同时我们看看AppClassLoader,它也是在Launcher中

static class AppClassLoader extends URLClassLoader

他们同时继承一个类URLClassLoader。
关于这种层次关系,看起来像继承,其实不是的。我们看到上面的代码就知道ExtClassLoader和AppClassLoader同时继承同一个类。同时我们来看下ClassLoader的loadClass方法也可以知道,下面贴出源代码:

源码没有全部贴出,只是贴出关键代码。从上面代码我们知道首先会检查class是否已经加载了,如果已经加载那就直接拿出,否则再进行加载。其中有一个parent属性,就是表示父加载器。这点正好说明了加载器之间的关系并不是继承关系。

c

双亲委派机制

关于类加载器,我们不得不说一下双亲委派机制。听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。
上面的源码也说明了这点。

d

为何要双亲委派机制

 

对于我们技术来讲,我们不但要知其然,还要知其所以然。为何要采用双亲委派机制呢?了解为何之前,我们先来说明一个知识点:
判断两个类相同的前提是这两个类都是同一个加载器进行加载的,如果使用不同的类加载器进行加载同一个类,也会有不同的结果。我们看一个例子:

大家可以看看输出的是什么?我们自己定义了一个类加载器,让它去加载我们自己写的一个类,然后判断由我们写的类加载器加载的类是否是MyClassLoader的一个实例。
答案是否定的。为什么?因为jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加载器加载的,而我们却指定了自己的加载器,当然加载出来的类就不相同了。不信,我们将他的父类加载器都打印出来。在上面代码中加入下面代码:

对比一下下面的代码:

第一个是我们自己加载器加载的类,第二个是直接new的一个对象,是由App类加载器进行加载的,我们把它们的父类加载器打印出来了,可以看出他们的加载器是不一样的。很奇怪为何会执行classloader==null这句话。其实classloader==null表示的就是根类加载器。我们看看Class.getClassLoader()方法源码:

从注释中我们知道了,如果返回了null,表示的是bootstrap类加载器。

5

类的连接

讲完了类的加载之后,我们需要了解一下类的连接。类的连接有三步,分别是验证,准备,解析。下面让我们一一了解

a

验证阶段

验证阶段主要做了以下工作
-将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。
-类文件结构检查:格式符合jvm规范-语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖
-字节码验证:确保字节码可以安全的被java虚拟机执行.
二进制兼容性检查:确保互相引用的类的一致性.如A类的a方法会调用B类的b方法.那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性.因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。

b

准备阶段

java虚拟机为类的静态变量分配内存并赋予默认的初始值.如int分配4个字节并赋值为0,long分配8字节并赋值为0;

c

解析阶段

解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。

6

初始化时机

类的加载时机5.2中我们提到了“首次主动使用”这个词语,那什么是“主动使用”呢?

主动初始化的6种方式
(1)创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
(2)调用类的静态属性或者为静态属性赋值
(3)调用类的静态方法
(4)通过class文件反射创建对象
(5)初始化一个类的子类:使用子类的时候先初始化父类
(6)java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类
只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。

同时我们需要注意下面几个Tips:
1)在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.
这里多说一点,为什么只初始化一次呢?因为我们上面讲到过类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到
类的相关信息。唯一一个Class对象说明了类只需要初始化一次即可,如果再次初始化就会出现多个Class对象,这样和唯一相违背了。
2)在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化;
3)在编译时无法确定下来的静态变量(运行时常量),会对类进行初始化;
4)如果这个类没有被加载和连接的话,那就需要进行加载和连接
5)如果这个类有父类并且这个父类没有被初始化,则先初始化父类.
6)如果类中存在初始化语句,依次执行初始化语句.

上面和下面的例子大家对比下,然后自己看看输出的是什么?

第一个输出的是
2
第二个输出的是
FinalTest2 static block
61(随机数)
为何会出现这样的结果呢?
参考上面的Tips2和Tips3,第一个能够在编译时期确定的,叫做编译常量;第二个是运行时才能确定下来的,叫做运行时常量。编译常量不会引起类的初始化,而运行常量就会。

那么将第一个例子的final去掉之后呢?输出又是什么呢?
这就是对类的首次主动使用,引用类的静态变量,输出的当然是:
FinalTest static block
2
那么在第一个例子的输出语句下面添加
FinalTest.x =3;
又会输出什么呢?
大家不妨试试!提示(Tips1)

7

类的初始化步骤

讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)

没有父类的情况:

  1. 类的静态属性

  2. 类的静态代码块

  3. 类的非静态属性

  4. 类的非静态代码块

  5. 构造方法

有父类的情况:

  1. 父类的静态属性

  2. 父类的静态代码块

  3. 子类的静态属性

  4. 子类的静态代码块

  5. 父类的非静态属性

  6. 父类的非静态代码块

  7. 父类构造方法

  8. 子类非静态属性

  9. 子类非静态代码块

  10. 子类构造方法

在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。
类的初始化内容这样看起来还是挺多的,包括“主动使用”大家可以自己去写一些demo去验证一下。

8

结束JVM进程的几种方式

(1) 执行System.exit()
(2) 程序正常结束
(3) 程序抛出异常,一直向上抛出没处理
(4) 操作系统异常,导致JVM退出

JVM有上面4种结束的方式,我们一一了解下:

(1)我们先来看看第一种方式,找到源代码我们发现:

上面的代码解释了System.exit()方法的作用就是:是中断当前运行的java虚拟机。这是自杀方式。

(2)第二种程序正常结束的方式,我们在运行main方法的时候,运行状态按钮由绿色变红色再变绿色的过程就是程序启动-运行-结束的过程。 那么,我们来看看Android的程序,同样,安卓也有自己的启动方式,也是一个main方法。那么我们的android程序能够一直运行的前提就是我们的main方法一直被执行着,一旦main方法执行完毕,程序就是kill。我们找找源代码才能有更好的说服力;我们找到ActivityThread的main方法

上面的代码都不用看,直接看最后两行代码。执行完Looper.loop()之后,直接抛出了异常。但是我们并没有见到这个异常,说明我们的Looper一直在执行这样保证我们的app不被kill掉。Android就是用这种方式来保证我们的app一直运行下去的。

(3)第三种方式不用过多解释,一直没有处理被抛出的异常,这样导致了程序崩溃。


(4)第四种方式是系统异常导致了jvm退出。其实jvm就是一个软件,如果我们的操作系统都出现了错误,那么运行在他上面的软件(jvm)必然会被kill。

9

结束并回顾

到这里,我们基本都清楚了类的加载机制。那么我们在第一篇文章中开头提到一个例子,我们这里来讲讲输出的是什么,并且为何如此输出,大家坐稳。

下面是我们的测试类TestSingleton

输出是:
counter1=1
counter2=0
why?我们一步一步分析:

1 执行TestSingleton第一句的时候,因为我们没有对Singleton类进行加载和连接,所以我们首先需要对它进行加载和连接操作。在连接阶-准备阶段,我们要讲给静态变量赋予默认初始值。
singleton =null
counter1 =0
counter2 =0


2 加载和连接完毕之后,我们再进行初始化工作。初始化工作是从上往下依次执行的,注意这个时候还没有调用Singleton.getSingleton();
首先 singleton = new Singleton();这样会执行构造方法内部逻辑,进行++;此时counter1=1,counter2 =1 ;
接下来再看第二个静态属性,我们并没有对它进行初始化,所以它就没办法进行初始化工作了;
第三个属性counter2我们初始化为0,因此此时的counter2 =1 ;

3 初始化完毕之后我们就要调用静态方法Singleton.getSingleton(); 我们知道返回的singleton已经初始化了。
那么输出的内容也就理所当然的是1和0了。这样一步一步去理解程序执行过程是不是让你清晰的认识了java虚拟机执行程序的逻辑呢。

那么我们接下来改变一下代码顺序,将 

public static int counter1; 

public static int counter2 = 0; 

private static Singleton singleton = new Singleton();
又会输出什么呢?为什么这样输出呢?
这个问题留给大家去思考,主要还是理解为什么这样输出才是最重要的。

1.2k 浏览 46 收藏 报告 阅读模式
chinleo · 2017年04月28日

第三个属性counter2我们初始化为0,因此此时的counter2 =1 ;
这句怎么更正?

回复

1

@chinleo 此时counter=1,接下来我们初始化为0,最后就是counter=0

emoo · 2017年05月02日
0

@H一天一天天 手误,上文中的counter为countrt2

emoo · 2017年05月02日
载入中...