美文网首页Android开发Android技术知识Android开发
关于JVM我们必须要知道的知识点(二)

关于JVM我们必须要知道的知识点(二)

作者: Android_Jian | 来源:发表于2018-07-14 16:20 被阅读47次

    在上篇文章中,我们介绍了运行时数据区域、垃圾回收算法、Java引用分类等一些有关于JVM的知识,今天我们一起来学习一下JVM类加载机制相关的部分。


    定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期可分为7个阶段,分别为:加载、验证、准备、解析、初始化、使用、卸载。如下图所示:

    类的生命周期

    对于加载的时机,JVM没有做强制约束,但是对于初始化阶段,虚拟机规范则是严格规定了有且只有以下5种情况必须立即对类进行“初始化”操作:

    (1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。常见的场景有:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。

    (2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

    (3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

    (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    (5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    上述5种场景中的行为,会触发类的初始化,称之为主动引用。除此之外,还有一些引用方式不会导致类的初始化,称为被动引用。常见的被动引用操作有:

    (1)通过子类引用父类的静态字段,不会导致子类初始化。

    (2)通过数组定义来引用类,不会触发此类的初始化。

    (3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。


    1. 加载

    在加载阶段,虚拟机主要完成以下3件事情:

    (1)通过一个类的全限定名来获取定义此类的二进制字节流。

    (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    2.验证

    验证阶段的目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    验证阶段大体可分为四个方面,分别为:文件格式验证、元数据验证、字节码验证、符号引用验证。

    3.准备

    准备阶段的作用:为类变量(被static修饰的变量)分配内存并且设置类变量的初始值(数据类型零值)。

    4.解析

    解析阶段的作用:虚拟机将常量池中的符号引用替换为直接引用。包括有:类或接口的解析、字段解析、类方法解析、接口方法解析等。

    5.初始化

    初始化阶段:真正开始执行类中定义的Java程序代码,初始化类变量和其他资源。或者可以从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

    注:<clinit>()方法:由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生。


    类加载器(ClassLoader)

    关于类加载器的知识,想必大家已经很熟悉了,在这里我还是要再提一下。类加载器作用于类生命周期的加载阶段,主要作用可以表述为:将.class文件(字节码文件)加载到JVM中,并转换成相应的Class对象,供JVM识别和使用。

    在讲解类加载器之前,我们先明确一点:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。通俗来讲就是:比较两个类是否“相等”,只有在这两个类是由同一加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

    Java语言为我们提供了三种类加载器,分别为:Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)和Application ClassLoader(应用程序类加载器)。

    Bootstrap ClassLoader(启动类加载器):使用C++语言实现,是虚拟机自身的一部分。负责加载<JAVA_HOME>\lib目录下或者被-Xbootclasspath参数指定路径下的rt.jar、resources.jar、charsets.jar、.class等。

    Extension ClassLoader(扩展类加载器):由Java语言实现,独立于虚拟机外部。负责加载<JAVA_HOME>\lib\ext目录下或者被java.ext.dirs系统变量所指定路径下的所有类库。

    Application ClassLoader(应用程序类加载器、系统类加载器):由Java语言实现,独立于虚拟机外部。负责加载当前应用的ClassPath目录下的所有类。

    简单来讲,上述三种类加载器最大的不同之处就是它们所负责加载的目录不同。如果有需要的话,我们还可以自定义类加载器,比如从网络获取.class文件并加载到JVM中。

    谈到类加载器,自然离不开“双亲委派机制”这个老生常谈的话题,下面我们一起来看一下。

    类加载器双亲委派机制

    双亲委派机制要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。在这里我们要明确一点,上述类加载器之间的关系并非继承关系。

    双亲委派机制的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己尝试去加载。

    使用双亲委派机制的好处:保证了Java核心类库的安全。它使得Java 类随着它的类加载器一起具备了一种带优先级的层次关系。例如 java.lang.Object 类,无论哪个类加载器去加载该类,最终都由启动类加载器加载,因此Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派机制,由各个类加载器去自行加载的话,用户自定义一个java.lang.Object 类放在Classpath 目录下,那么系统中会出现多个不同的Object类,导致程序一片混乱。


    上面噼里啪啦说了这么多,下面我们一起来验证一下,这样更有助于我们理解和记忆。

    在这里,我们创建一个Person类,然后在main方法中打印出Person类对应的ClassLoader。如图所示:

    打印classloader

    运行结果如下所示:

    运行结果

    在这里我们可以看出,平常我们在程序中定义的类是由AppClassLoader加载的。根据上文中的双亲委托机制,我们了解到AppClassloader的父加载器为ExtClassLoader,那AppClassloader和ExtClassLoader在代码中具体有什么关联呢,我们通过ClassLoader的getParent()方法来看一下:

    程序及验证结果

    可以看到,我们在AppClassloader中调用了getParent方法,得到了ExtClassLoader对象。我相信你肯定会对它的源码感兴趣。

    Launcher.class

    由上述源码我们可以知道,AppClassloader和ExtClassLoader都是定义在Launcher中的静态内部类,并且两者都继承自URLClassLoader。而URLClassLoader最终又是继承自ClassLoader类。这也验证了上文中我们所说的双亲委托机制并非继承关系,而是一种组合关系。

    Launcher是什么鬼东西呢?它其实是一个java虚拟机的入口应用,我们要想搞清楚AppClassloader的父加载器为什么是ExtClassLoader,还需要从Launcher的构造函数查找出答案。

    Launcher构造函数

    在这里我只截取了一部分代码,大家需要注意的地方就是我打断点的那两行代码。先大致说一下,第一行代码的作用就是创建了ExtClassLoader对象,第二行代码的作用就是创建AppClassloader对象,并在构造方法中将之前生成的ExtClassLoader对象作为参数传进去。在这里我们可以大胆猜想一下,在AppClassLoader中肯定定义了一个parent变量,用来接收getAppClassLoader方法中传入的参数。到底是不是这样呢?我们拭目以待。

    我们跟进去getAppClassLoader方法去看一下,结果如下:

    getAppClassLoader方法

    可以看到,getAppClassLoader方法中最终又是调用到AppClassLoader中带有两个参数的构造函数,分别将加载路径和我们传过来的classloader(也就是ExtClassLoader对象)传入,我们接着跟进去图中 2 标注的super方法,最终会调用到ClassLoader类中带有两个参数的构造方法,其中var2参数也就是我们传过来的ExtClassLoader对象。

    ClassLoader类中带有两个参数的构造方法

    注意看最后一行我用红笔标注的地方,到这里真相大白了吧。在我们创建AppClassloader对象的时候,会将之前创建的ExtClassLoader对象作为参数传入,在AppClassloader类中定义了一个parent变量,用来接收我们传入的ExtClassLoader对象。

    那么我们接下来再看一下,ExtClassLoader和BootsTrapClassLoader在代码中有什么关联呢?

    程序及运行结果

    由运行结果我们可以看到,程序抛出了空指针异常。我们判断肯定是13行调用了toString方法导致的,这也就说明ExtClassLoader的getParent方法为null。

    what???are you kidding me???

    要想确认我们的推断,我默默的翻起了源码。

    Launcher构造函数

    刚才我们只是分析了断点第二行,也就是AppClassLoader的创建过程,接下来我们照例分析下断点第一行ExtClassLoader的创建过程。在这里我们要注意下,调用getExtClassLoader方法并没有传参数。我们跟进去看下:

    getExtClassLoader方法

    我们可以看出getExtClassLoader方法最终又是调用到ExtClassLoader一个参数的构造方法,将加载路径传入,我们接下来看下ExtClassLoader一个参数的构造方法。

    ExtClassLoader一个参数的构造方法

    睁大眼睛注意了,方法中调用super类时,传入的第二个参数,也就是ClassLoader对象为null。接下来的调用就和AppClassLoader类似了,最终会调用到ClassLoader类中带有两个参数的构造方法,其中var2参数也就是我们传过来的null。

    ClassLoader类中带有两个参数的构造方法

    这下我们确信了,ExtClassLoader中的parent参数确定为null。

    接下来我们来看下双亲委托机制的代码实现(代码有所删减):

    双亲委托机制的代码实现

    在我们当前ClassLoader进行类加载的时候会调用到loadClass方法。方法中首先判断所要加载的类是否已经被加载过,如果已经被加载过了,则直接返回我们加载过的类对象。如果没有加载过,则判断当前ClassLoader的parent参数是否为null,关于parent参数的赋值,我们在上文中已经通过源码分析过了,简单讲,AppClassloader的parent参数为ExtClassLoader对象,而ExtClassLoader的parent参数为null。例如我们当前的ClassLoader为AppClassloader,则parent参数不为null,会调用ExtClassLoader的loadClass方法进行类加载,即AppClassloader的父加载器为ExtClassLoader。例如我们当前的ClassLoader为ExtClassLoader,则parent参数为null,会调用BootstrapClassLoader进行类加载,即ExtClassLoader的父加载器为BootstrapClassloader。在父加载器无法完成类加载的时候,最终会调用到当前ClassLoader的findClass方法进行类加载。


    关于JVM中类加载机制相关的知识我们先讲到这里,希望在我学习的同时也有帮助到大家。如果有哪些分析不对的地方,还望老哥们指出,我后续改正过来。后续我会接着分析下Android中类加载机制相关部分,欢迎大家关注。

    相关文章

      网友评论

        本文标题:关于JVM我们必须要知道的知识点(二)

        本文链接:https://www.haomeiwen.com/subject/dqnqpftx.html