一、Java虚拟机启动、加载类过程分析

下面我将定义一个非常简单的java程序并运行它,来逐步分析java虚拟机启动的过程。

package org.luanlouis.jvm.load;
import sun.security.pkcs11.P11Util; /**
* Created by louis on 2016/1/16.
*/
public class Main{ public static void main(String[] args) {
System.out.println("Hello,World!"); ClassLoader loader = P11Util.class.getClassLoader(); System.out.println(loader);
}
}

在windows命令行下输入:

java    org.luanlouis.jvm.load.Main

当输入上述的命令时: 
windows开始运行{JRE_HOME}/bin/java.exe程序,java.exe 程序将完成以下步骤:

1.  根据JVM内存配置要求,为JVM申请特定大小的内存空间;

2.  创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;

3.   创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader;

4.  使用上述获取的ClassLoader实例加载我们定义的 org.luanlouis.jvm.load.Main类;

5.  加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;

6.  结束,java程序运行结束,JVM销毁。

1.1Step 1          根据JVM内存配置要求,为JVM申请特定大小的内存空间

为了不降低本文的理解难度,这里就不详细介绍JVM内存配置要求的话题,今概括地介绍一下内存的功能划分。

JVM启动时,按功能划分,其内存应该由以下几部分组成: 

如上图所示,JVM内存按照功能上的划分,可以粗略地划分为方法区(Method Area) 和堆(Heap),而所有的类的定义信息都会被加载到方法区中。

1.2 Step 2.    创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;

JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。

引导类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib 下的jar包和配置,然后将这些系统类加载到方法区内。

本例中,引导类加载器是用 {JRE_HOME}/lib加载类的,不过,你也可以使用参数 -Xbootclasspath 或 系统变量sun.boot.class.path来指定的目录来加载类。

一般而言,{JRE_HOME}/lib下存放着JVM正常工作所需要的系统类,如下表所示:

文件名 描述
rt.jar 运行环境包,rt即runtime,J2SE 的类定义都在这个包内
charsets.jar 字符集支持包
jce.jar 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现
jsse.jar 安全套接字拓展包Java(TM) Secure Socket Extension
classlist 该文件内表示是引导类加载器应该加载的类的清单
net.properties JVM 网络配置信息

引导类加载器(Bootstrap ClassLoader) 加载系统类后,JVM内存会呈现如下格局:

  • 引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池类型信息字段信息方法信息类加载器的引用对应class实例的引用等信息。
  • 类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL
  • 对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

小测试:
当我们在代码中尝试获取系统类如java.lang.Object的类加载器时,你会始终得到NULL:

1.3 Step 3.     创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader

上述步骤完成,JVM基本运行环境就准备就绪了。接着,我们要让JVM工作起来了:运行我们定义的程序 org.luanlouis,jvm.load.Main。

此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher 的静态方法getLauncher(),  获取sun.misc.Launcher 实例:

sun.misc.Launcher 使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
在Launcher的内部,其定义了两个类加载器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoadersun.misc.Launcher.AppClassLoader,这两个类加载器分别被称为拓展类加载器(Extension ClassLoader) 和 应用类加载器(Application ClassLoader).如下图所示:

图例注释:除了引导类加载器(Bootstrap Class Loader )的所有类加载器,都有一个能力,就是判断某一个类是否被引导类加载器加载过,如果加载过,可以直接返回对应的Class<T> instance,如果没有,则返回null.  图上的指向引导类加载器的虚线表示类加载器的这个有限的访问 引导类加载器的功能。

此时的  launcher.getClassLoader() 方法将会返回 AppClassLoader 实例,AppClassLoader将ExtClassLoader作为自己的父加载器。

当AppClassLoader加载类时,会首先尝试让父加载器ExtClassLoader进行加载,如果父加载器ExtClassLoader加载成功,则AppClassLoader直接返回父加载器ExtClassLoader加载的结果;如果父加载器ExtClassLoader加载失败,AppClassLoader则会判断该类是否是引导的系统类(即是否是通过Bootstrap类加载器加载,这会调用Native方法进行查找);若要加载的类不是系统引导类,那么ClassLoader将会尝试自己加载,加载失败将会抛出“ClassNotFoundException”。

具体AppClassLoader的工作流程如下所示:

双亲委派模型(parent-delegation model):
上面讨论的应用类加载器AppClassLoader的加载类的模式就是我们常说的双亲委派模型(parent-delegation model).
对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:
1. 委托父类加载器帮忙加载;
2. 父类加载器加载不了,则查询引导类加载器有没有加载过该类;
3. 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
4. 若加载成功,返回 对应的Class<T> 对象;若失败,抛出异常“ClassNotFoundException”。
请注意:
双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:
1. 父类加载器(parent classloader):它可以替子加载器尝试加载类
2. 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。

一般情况下,双亲加载模型如下所示:

1.4  Step 4.     使用类加载器ClassLoader加载Main类

通过 launcher.getClassLoader()方法返回AppClassLoader实例,接着就是AppClassLoader加载 org.luanlouis.jvm.load.Main类的时候了。

上述定义的org.luanlouis.jvm.load.Main类被编译成org.luanlouis.jvm.load.Main class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常亮信息。常量池中有CONSTANT_CLASS_INFO类型的常量,表示该class中声明了要用到那些类:

当AppClassLoader要加载 org.luanlouis.jvm.load.Main类时,会去查看该类的定义,发现它内部声明使用了其它的类: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;org.luanlouis.jvm.load.Main类要想正常工作,首先要能够保证这些其内部声明的类加载成功。所以AppClassLoader要先将这些类加载到内存中。(注:为了理解方便,这里没有考虑懒加载的情况,事实上的JVM加载类过程比这复杂的多)

加载顺序:

  • 1. 加载java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class

AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过,结果表明这些类已经被BootstrapClassLoader加载过,则无需重复加载,直接返回对应的Class<T>实例;

  • 2. 加载sun.security.pkcs11.P11Util

此在{JRE_HOME}/lib/ext/sunpkcs11.jar包内,属于ExtClassLoader负责加载的范畴。AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现其正好属于加载范围,故ExtClassLoader负责将其加载到内存中。ExtClassLoader在加载sun.security.pkcs11.P11Util时也分析这个类内都使用了哪些类,并将这些类先加载内存后,才开始加载sun.security.pkcs11.P11Util,加载成功后直接返回对应的Class<sun.security.pkcs11.P11Util>实例;

  • 3. 加载org.luanlouis.jvm.load.Main

AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过。而结果表明BootstrapClassLoader 没有加载过它,这时候AppClassLoader只能自己动手负责将其加载到内存中,然后返回对应的Class<org.luanlouis.jvm.load.Main>实例引用;

以上三步骤都成功,才表示classLoader.loadClass("org.luanlouis.jvm.load.Main")完成,上述操作完成后,JVM内存方法区的格局会如下所示:

如上图所示:

  • JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;
  • 某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class<T>,用来表示访问该类信息的入口

1.5   Step 5.      使用Main类的main方法作为程序入口运行程序

1.6  Step 6.      方法执行完毕,JVM销毁,释放内存

二、类加载器有哪些?其组织结构是怎样的?

类加载器(Class Loader):顾名思义,指的是可以加载类的工具。JVM自身定义了三个类加载器:引导类加载器(Bootstrap Class Loader)、拓展类加载器(Extension Class Loader )、应用加载器(Application Class Loader)。当然,我们有时候也会自己定义一些类加载器来满足自身的需要。

引导类加载器(Bootstrap Class Loader): 该类加载器使JVM使用C/C++底层代码实现的加载器,用以加载JVM运行时所需要的系统类,这些系统类在{JRE_HOME}/lib目录下。由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过。我们经常使用的系统类如:java.lang.String,java.lang.Object,java.lang*....... 这些都被放在 {JRE_HOME}/lib/rt.jar包内, 当JVM系统启动的时候,引导类加载器会将其加载到 JVM内存的方法区中。

拓展类加载器(Extension Class Loader): 该加载器是用于加载 java 的拓展类 ,拓展类一般会放在 {JRE_HOME}/lib/ext/ 目录下,用来提供除了系统类之外的额外功能。拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。

应用类加载器(Applocatoin Class Loader): 该类加载器是用于加载用户代码,是用户代码的入口。我经常执行指令 java   xxx.x.xxx.x.x.XClass , 实际上,JVM就是使用的AppClassLoader加载 xxx.x.xxx.x.x.XClass 类的。应用类加载器将拓展类加载器当成自己的父类加载器,当其尝试加载类的时候,首先尝试让其父加载器-拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果Class<T> instance,加载失败,则会询问是否引导类加载器已经加载了该类;只有没有加载的时候,应用类加载器才会尝试自己加载。由于xxx.x.xxx.x.x.XClass是整个用户代码的入口,在Java虚拟机规范中,称其为 初始类(Initial Class).

用户自定义类加载器(Customized Class Loader):用户可以自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader类。

三、双亲加载模型的逻辑和底层代码实现是怎样的?

上面已经不厌其烦地讲解什么是双亲加载模型,以及其机制是什么,这些东西都是可以通过底层代码查看到的。

我们也可以通过JDK源码看java.lang.ClassLoader的核心方法 loadClass()的实现:

//提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class<T> instance 实例
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
} protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例
Class<?> c = findLoadedClass(name);
//初次加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果有父类加载器,则先让父类加载器加载
c = parent.loadClass(name, false);
} else {
// 没有父加载器,则查看是否已经被引导类加载器加载,有则直接返回
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父加载器加载失败,并且没有被引导类加载器加载,则尝试该类加载器自己尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 自己尝试加载
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否解析类
if (resolve) {
resolveClass(c);
}
return c;
}
}

相对应地,我们可以整理出双亲模型的工作流程图:

相信读者看过这张图后会对双亲加载模型有了非常清晰的脉络。当然,这是JDK自身默认的加载类的行为,我们可以通过继承复写该方法,改变其行为。

四、类加载器与Class<T>  实例的关系

五、线程上下文加载器

Java 任何一段代码的执行,都有对应的线程上下文。如果我们在代码中,想看当前是哪一个线程在执行当前代码,我们经常是使用如下方法:

相应地,我们可以为当前的线程指定类加载器。在上述的例子中, 当执行   java    org.luanlouis.jvm.load.Main  的时候,JVM会创建一个Main线程,而创建应用类加载器AppClassLoader的时候,会将AppClassLoader  设置成Main线程的上下文类加载器:

线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的 双亲加载模型解放出来,进而实现特定的加载需求。

(二十七)JVM类加载器机制与类加载过程的更多相关文章

  1. 深入JVM类加载器机制,值得你收藏

    先来一道题,试试水平 public static void main(String[] args) { ClassLoader c1 = ClassloaderStudy.class.getClass ...

  2. JVM之类加载器、加载过程及双亲委派机制

    JVM 的生命周期 虚拟机的启动 Java 虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实 ...

  3. Tomcat类加载器机制

    Tomcat为什么需要定制自己的ClassLoader: 1.定制特定的规则:隔离webapp,安全考虑,reload热插拔 2.缓存类 3.事先加载 要说Tomcat的Classloader机制,我 ...

  4. 深入剖析Classloader(二)--根类加载器,扩展类加载器与系统类加载器

    原文地址:http://yhjhappy234.blog.163.com/blog/static/31632832201152555245584/?suggestedreading&wumii ...

  5. 通过JDBC驱动加载深刻理解线程上下文类加载器机制

    关于线程上下文类加载器已经在之前学得比较透了,作为一个收尾,这里用平常J2EE开发时JDBC连接Mysql数据库常见的一段代码通过分析它的底层进一步加深对线程上下文类加载器的理解,所以先来将连接应用代 ...

  6. jvm系列(二)jvm垃圾收集器与内存分配策略

    众所周知,在java语言中,内存分配和回收是由jvm自动管理的.因此内存的分配和回收也是jvm三大功能之一.垃圾收集器(GC)需要完成三件事情: 哪些内存需要回收? 什么时候进行回收? 如何回收? 本 ...

  7. JVM类加载机制详解(二)类加载器与双亲委派模型

    在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...

  8. 深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

    一.概述   定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的 ...

  9. 深入了解java虚拟机(JVM) 第十二章 类加载器

    一.什么是类加载器 类加载器是一个用来加载类文件的类,Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序.需要注意的是,只有被同一个类加载器加载的类才可能会相等 ...

随机推荐

  1. Django REST framework认证权限和限制和频率

    认证.权限和限制 身份验证是将传入请求与一组标识凭据(例如请求来自的用户或其签名的令牌)相关联的机制.然后 权限 和 限制 组件决定是否拒绝这个请求. 简单来说就是: 认证确定了你是谁 权限确定你能不 ...

  2. ArrayList 集合的几种遍历的方法

    ArrayList 集合  也可称作动态数组(长度可变),在新建的时候是没有默认长度的,在新增数据长度小于10的时候,ArrayList 的长度会自动设置为10 //了解更多可以按住Ctrl 再点击你 ...

  3. Linux Shell 如何获取参数

    $# 是传给脚本的参数个数 $0 是脚本本身的名字 $1 是传递给该shell脚本的第一个参数 $2 是传递给该shell脚本的第二个参数 $@ 是传给脚本的所有参数的列表 $* 是以一个单字符串显示 ...

  4. 数码管动态显示——74HC04

    1.电路设计: p0实现段选,p2实现位选,74hc04是反相器,有反向和放大的双重作用. 2.程序设计: #include<reg52.h> code unsigned char sev ...

  5. P4921 【情侣?给我烧了!】

    加强前这道题还是比较友好的 首先我们设\(g_x\)为x对情侣没有一对坐在一起的数量 然后答案就可以表示成:\(C_n^k*A_n^k*2^k*g_{n-k}\) 这里的复杂度是\(O(T*N)\), ...

  6. 微信小程序开源

    | UI组件 | | | | | | | | | weui-wxss ★1873 - 同微信原生视觉体验一致的基础样式库 | | | | | | zanui-weapp ★794 - 好用易扩展的小程 ...

  7. Safe Path(bfs+一维数组存图)

    题目链接:http://codeforces.com/gym/101755/problem/H 题目分析:先bfs一遍怪兽可以到达的点,再bfs人可以走的地方看可不可以到达终点: 很显然读到  2&l ...

  8. Visual Detail Augmented Mapping for Small Aerial Target Detection(航片动态小目标检测)

    1.介绍 航片里小目标占总像元数不足1%,普通目标检测算法如YOLO会有很多错误,主要原因有3点: 1.航片的无关背景占多数 2.目标大小由于飞行高度和拍摄角度不同 3.航片中的小移动目标和噪音会混淆 ...

  9. bytearray 字符串转为字节

    >>> str="hello world">>> x=bytearray(str)>>> xbytearray(b'hello ...

  10. 记一次vue+vuex+vue-router+axios+elementUI开发(二)

    开发环境跟脚手架初始完毕后,我们开始配置axios请求后台接口   axios使用说明:https://www.kancloud.cn/yunye/axios/234845 1.本人是在脚手架中的sr ...