Java虚拟机启动、加载类过程分析
下面我将定义一个非常简单的java程序并运行它,来逐步分析java虚拟机启动的过程。
当输入上述的命令时,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销毁。
Step 1.根据JVM内存配置要求,为JVM申请特定大小的内存空间
为了不降低本文的理解难度,这里就不详细介绍JVM内存配置要求的话题,今概括地介绍一下内存的功能划分。
JVM启动时,按功能划分,其内存应该由以下几部分组成:
如上图所示,JVM内存按照功能上的划分,可以粗略地划分为 方法区(Method Area) 和 堆(Heap),而所有的类的定义信息都会被加载到 方法区中
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
来指定的目录来加载类。而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)
一般而言,{JRE_HOME}/lib
下存放着JVM正常工作所需要的系统类,如下表所示:
文件名 | 描述 |
---|---|
rt.jar | 运行环境包,rt即runtime,J2SE 的类定义都在这个包内 |
charsets.jar | 字符集支持包 |
jce.jar | 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现 |
jsse.jar | 安全套接字拓展包Java™ Secure Socket Extension |
classlist | 该文件内表示是引导类加载器应该加载的类的清单 |
net.properties | JVM 网络配置信息 |
引导类加载器(Bootstrap ClassLoader) 加载系统类后,JVM内存会呈现如下格局:
-
引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
-
类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是由C++语言实现的,所以是无法访问的,故而该引用为
NULL
。当我们在代码中尝试获取系统类如java.lang.Object的类加载器时,你会始终得到NULL:
<span style="color:#000000"><span style="background-color:#fafafa"><code class="language-java">System<span style="color:#999999">.</span>out<span style="color:#999999">.</span><span style="color:#dd4a68">println</span><span style="color:#999999">(</span>String<span style="color:#999999">.</span><span style="color:#0077aa">class</span><span style="color:#999999">.</span><span style="color:#dd4a68">getClassLoader</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#708090">//null</span> System<span style="color:#999999">.</span>out<span style="color:#999999">.</span><span style="color:#dd4a68">println</span><span style="color:#999999">(</span>Object<span style="color:#999999">.</span><span style="color:#0077aa">class</span><span style="color:#999999">.</span><span style="color:#dd4a68">getClassLoader</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#708090">//null</span> System<span style="color:#999999">.</span>out<span style="color:#999999">.</span><span style="color:#dd4a68">println</span><span style="color:#999999">(</span>Math<span style="color:#999999">.</span><span style="color:#0077aa">class</span><span style="color:#999999">.</span><span style="color:#dd4a68">getClassLoader</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#708090">//null</span> System<span style="color:#999999">.</span>out<span style="color:#999999">.</span><span style="color:#dd4a68">println</span><span style="color:#999999">(</span>System<span style="color:#999999">.</span><span style="color:#0077aa">class</span><span style="color:#999999">.</span><span style="color:#dd4a68">getClassLoader</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#708090">//null</span> </code></span></span>
有关为何被引导类加载器加载的类获取其类加载器时返回null,可以参考加载器之sun.misc.Launcher类第二节内容,搜索getClassLoader关键字。
-
对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
Step 3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader
上述步骤完成,JVM基本运行环境就准备就绪了。接着,我们要让JVM工作起来了:运行我们定义的程序 org.luanlouis,jvm.load.Main
。
此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher
的静态方法getLauncher()
, 获取sun.misc.Launcher
实例:
<span style="color:#000000"><span style="background-color:#fafafa"><code class="language-java">sun<span style="color:#999999">.</span>misc<span style="color:#999999">.</span>Launcher launcher <span style="color:#a67f59">=</span> sun<span style="color:#999999">.</span>misc<span style="color:#999999">.</span>Launcher<span style="color:#999999">.</span><span style="color:#dd4a68">getLauncher</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">;</span> <span style="color:#708090">//获取Java启动器</span>
ClassLoader classLoader <span style="color:#a67f59">=</span> launcher<span style="color:#999999">.</span><span style="color:#dd4a68">getClassLoader</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">;</span> <span style="color:#708090">//获取类加载器ClassLoader用来加载class到内存来</span>
</code></span></span>
sun.misc.Launcher
使用了单例模式
设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher
实例。
在Launcher
的内部,其定义了两个类加载器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoader
和sun.misc.Launcher.AppClassLoader
,这两个类加载器分别被称为拓展类加载器(Extension ClassLoader)
和 应用类加载器(Application ClassLoader)
如下图所示:
图例注释:除了引导类加载器(Bootstrap Class Loader )的所有类加载器,都有一个能力,就是判断某一个类是否被引导类加载器加载过(通过ClassLoader类的findBootstrapClassOrNull()
方法),如果加载过,可以直接返回对应的Class instance,如果没有,则返回null. 图上的指向引导类加载器的虚线表示类加载器的这个有限的访问 引导类加载器的功能。
此时的 launcher.getClassLoader()
方法将会返回 AppClassLoader
实例,AppClassLoader
将ExtClassLoader
作为自己的父加载器。
当AppClassLoader
加载类时,会首先尝试让父加载器ExtClassLoader
进行加载,如果父加载器ExtClassLoader
加载成功,则AppClassLoader
直接返回父加载器ExtClassLoader
加载的结果;如果父加载器ExtClassLoader
加载失败,将会抛出“ClassNotFoundException”,那么ClassLoader将会尝试自己加载,加载失败将会抛出“ClassNotFoundException”。
有关Launcher类的更多信息可以参考加载器之sun.misc.Launcher类
具体AppClassLoader
的工作流程如下所示:
双亲委派模型(parent-delegation model):
上面讨论的应用类加载器AppClassLoader的加载类的模式就是我们常说的双亲委派模型(parent-delegation model),对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:
- 委托父类加载器帮忙加载;
- 父类如果仍然有父类,则继续委托给上一层,直至Ext加载器(extends关系的最顶层,父类为null)
- Ext加载器没有父类加载器,则查询引导类加载器有没有加载过该类;
- 如果引导类加载器加载过,返回对应的Class 对象,并层层向下传递
- 如果引导类加载器没有加载过该类,则Ext加载器尝试加载该类;
- 若Ext加载成功,返回 对应的Class 对象,并层层向下传递;若失败,抛出异常
- 子加载器捕获“ClassNotFoundException”,并自己尝试加载类,递归下去。
请注意:
双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:
- 父类加载器(parent classloader):它可以替子加载器尝试加载类
- 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
一般情况下,双亲加载模型如下所示,增加了自定义加载器:
Step 4. 使用类加载器ClassLoader加载Main类
通过 launcher.getClassLoader()方法返回AppClassLoader实例,接着就是AppClassLoader加载 org.luanlouis.jvm.load.Main
类的时候了:
<span style="color:#000000"><span style="background-color:#fafafa"><code class="language-bash">ClassLoader classloader <span style="color:#a67f59">=</span> launcher.getClassLoader<span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">;</span>//取得AppClassLoader类
classLoader.loadClass<span style="color:#999999">(</span><span style="color:#50a14f">"org.luanlouis.jvm.load.Main"</span><span style="color:#999999">)</span><span style="color:#999999">;</span>//加载自定义类
</code></span></span>
- 1
- 2
上述定义的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则会先查询这些类是否已经被BootstrapClassLoader加载过,结果表明这些类已经被BootstrapClassLoader加载过,则无需重复加载,直接返回对应的Class实例;
2. 加载sun.security.pkcs11.P11Util
<span style="color:#000000"><span style="background-color:#fafafa"><code>此在`{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>实例;
</code></span></span>
- 1
3. 加载org.luanlouis.jvm.load.Main
AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader则会查询这些类是否已经被BootstrapClassLoader加载过,而结果表明BootstrapClassLoader 没有加载过它;ExtClassLoader于是自己尝试加载,结果不在自己的加载范围内,于是加载失败,抛出ClassNotFoudException;AppClassLoader发现父类加载器ExtClassLoader无法加载,这时候AppClassLoader只能自己动手负责将其加载到内存中,然后返回对应的Class<org.luanlouis.jvm.load.Main>实例引用;
以上三步骤都成功,才表示classLoader.loadClass(“org.luanlouis.jvm.load.Main”)完成,上述操作完成后,JVM内存方法区的格局会如下所示:
如上图所示:
- JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;
- 某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class,用来表示访问该类信息的入口
Step 5. 使用Main类的main方法作为程序入口运行程序
Step 6. 方法执行完毕,JVM销毁,释放内存
类的生命周期可以简单地描述为:JVM虚拟机把.class文件中类信息加载进内存并进行解析生成对应的class对象,Class对象被JVM使用,最后随着JVM要关闭而卸载的整个过程->可以抓重点记忆: .class字节码加载进jvm,生成Class对象,使用类对象最后卸载
在 JVM 的启动过程中,申请内存、创建类加载器以及启动垃圾回收器这些步骤的具体时机会有所不同。下面是一般情况下的典型顺序:
1. JVM 启动,操作系统为 JVM 进程分配初始内存空间。
2. JVM 初始化,其中包括执行与 JVM 相关的配置和初始化操作,例如设置默认的堆大小、堆外内存空间的分配等。
3. 创建引导类加载器(Bootstrap Class Loader),这是 JVM 内置的类加载器,负责加载 Java 核心类库(如 `java.lang`、`java.util` 等)。
4. 创建扩展类加载器(Extension Class Loader),它是引导类加载器的子类,负责加载 Java 扩展库。
5. 创建应用程序类加载器(Application Class Loader),也称为系统类加载器(System Class Loader),它是扩展类加载器的子类,负责加载应用程序的类。
6. JVM 为堆、栈、方法区等各个内存区域分配内存空间。此时会根据 JVM 的启动参数和默认配置来确定各个内存区域的大小。
7. 初始化运行时数据区域,包括创建 Java 主线程、设置主类的上下文类加载器、准备执行环境等。
8. 启动垃圾回收器(Garbage Collector),垃圾回收器的具体启动时机与 JVM 的实现和垃圾回收策略有关。通常,在 JVM 的
在 JVM 的启动过程中,还有其他一些步骤,如下所示:
9. 加载主类:JVM 根据启动命令中指定的主类名找到对应的字节码文件,并通过应用程序类加载器加载主类的字节码。
10. 解析符号引用:在链接阶段的解析步骤中,JVM 将符号引用转换为直接引用。这个过程将符号引用与实际的类、方法、字段等进行关联,建立起引用的对象之间的联系。
11. 准备阶段:在类加载的准备阶段,JVM 为类的静态变量分配内存空间,并设置默认的初始值,例如数字类型对应的零值、对象引用类型对应的 null 值等。
12. 初始化阶段:在类加载的初始化阶段,JVM 执行类的初始化代码,包括对静态变量的赋值、执行静态代码块等。这些初始化步骤将对类的静态成员进行初始化。
13. 执行主线程:在主类的初始化完成后,JVM 将开始运行主线程,顺序执行 `main` 方法中的代码。
14. 运行垃圾回收器:垃圾回收器的具体运行时机和策略可能会因 JVM 的实现和配置而有所不同。一般情况下,JVM 会根据垃圾回收算法和触发条件来判断是否执行垃圾回收操作。
好的,以下是 JVM 启动过程中的另外几个步骤:
15. JIT 编译器初始化:JIT(Just-In-Time)编译器是 JVM 进行实时编译的核心组件。在初始化过程中,JVM 会选择并加载适当的 JIT 编译器版本,并进行相关的初始化操作。
16. 设置运行时参数:JVM 可以通过启动参数或者其他配置方式来设置一系列与运行时相关的参数,例如垃圾回收器类型、堆空间大小、线程池大小等。
17. 运行时监控和管理:在 JVM 运行过程中,可以通过运行时 JMX(Java Management Extensions)接口来对 JVM 进行监控和管理,从而实现对应用程序的动态调整和优化。
需要注意的是,不同的 JVM 实现可能会包含其他的额外步骤,例如加载特定的配置文件、根据运行环境进行优化等。此外,部分步骤可能会在 JVM 的启动过程中被延迟执行,直到特定条件满足或者特