本文参考自“《Java虚拟机原理图解》5. JVM类加载器机制与类加载过程 ”,对原内容作了些修改并重新组织
JVM启动、加载类过程
下面我将定义一个非常简单的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);
}
}
根据JVM内存配置要求,为JVM申请特定大小的内存空间;
JVM启动时按照其配置要求,申请一块内存,并根据JVM规范和实现将内存划分为几个区域。class二进制文件信息被放入“方法区”,对象实例被放入“java堆”等。关于JVM内存模型内容参考 JVM(一)JVM内存模型
创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;
JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如
java.lang.String
,java.lang.Object
等等。
引导类加载器(Bootstrap Classloader)会读取{JRE_HOME}/lib
下的jar包和配置,然后将这些系统类加载到方法区内。引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
创建JVM 启动器实例
Launcher
,并取得类加载器ClassLoader
;此时,JVM虚拟机调用已经加载在方法区的类
sun.misc.Launcher
的静态方法getLauncher()
, 获取sun.misc.Launcher
实例使用上述获取的
ClassLoader
实例加载我们定义的类;通过
launcher.getClassLoader()
方法返回AppClassLoader
实例,接着就是AppClassLoader
加载 我们自定义类.加载自己写的类之前先要加载我们写的类中用到的其他类。在
org.luanlouis.jvm.load.Main
类被编译成的class文件中有一个叫常量池(Constant Pool)的结构体,通过这个常量池中的CONSTANT_CLASS_INFO
常量判断该class用到了哪些类,并通过类加载器去加载这些类。加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;
- 结束,java程序运行结束,JVM销毁。
类加载器
- 类加载器的作用:通过一个类的全限定名来获取描述此类的二进制字节流,并将此类相关信息加载到JVM的方法区,并创建一个
java.lang.Class
对象作为此类的访问接口,class
对象的引用也保存在方法区内。 - 每一个类加载器都有独立的类名称空间。比较两个类是否相等的前提是两个类是由同一个类加载器加载的,否则两个类比不相等。
从JVM角度来讲,只有两种类加载器:启动类加载器、其他的类加载器。因为前者是JVM虚拟机的一部分,后者是独立于JVM实现的。
更细致一点划分,类加载器分为下面三种:
1. 启动类加载器(Bootstrap ClassLoader)
启动类加载器是使用C++
语言实现的(HotSpot),负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String
, java.lang.Object
等等。
启动类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib
下的jar包(如 rt.jar)和配置,然后将这些系统类加载到方法区内。
由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过。
2. 扩展类加载器 (Extension ClassLoader)
此加载器由 sun.misc.Launcher$ExtClassLoader
实现,它负责加载 {JAVA_HOME}\lib\ext
目录下的类库, 开发者可以直接获取此加载器。
拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。
3. 应用程序加载器 (Application ClassLoader)
此加载器负责加载用户类路径上指定的类库,若没有指定自定义加载器,则此加载器一般是程序中默认的加载器。
应用类加载器将拓展类加载器当成自己的父类加载器。
4. 用户自定义类加载器(Customized Class Loader)
用户可以自己定义类加载器来加载类。所有的类加载器都要继承 java.lang.ClassLoader
类并重写 findClass(String name)
方法。用户自定义类加载器默认父加载器是 应用程序加载器
各个类加载器关系如下:
双亲委派模型
双亲委派模型工作过程:一个类加载器收到类加载的请求,它首先会把这个请求委派给父类加载器去完成,层层上升,只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。
要注意的是父加载器和子加载器的关系不是继承关系而是组合关系。子加载器中有一个私有属性 parent
指向父加载器。
具体到上述三个加载器时:当应用程序加载器尝试加载类的时候,首先尝试让其父加载器–拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果 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;
}
}
应用程序加载器一般流程如下: