1. 从main函数启动说起
不出意外,我们的第一个程序是hello world!,执行main函数就执行打印。但这个看似简单的程序,就能分析出java运行的原理。当我们通过IDE执行main函数的时候,它作了包装,隐藏了java文件执行的过程。无论是eclipse或者是idea,我们均能找到配置jvm参数和program参数的地方,而在启动main函数的时候,这些会被作为main函数的启动参数加载到运行进程中。
上述的启动过程其实等价于在class文件的根目录执行了这样一条命令:java com.xxx.xxx.Main param1 param2 param3 … -Xms …,jvm参数设置命令规范如下:
(不做翻译)
-Xmixed mixed mode execution (default)
-Xint interpreted mode execution only
-Xbootclasspath:<directories and zip/jar files separated by ;>
set search path for bootstrap classes and resources
-Xbootclasspath/a:<directories and zip/jar files separated by ;>
append to end of bootstrap class path
-Xbootclasspath/p:<directories and zip/jar files separated by ;>
prepend in front of bootstrap class path
-Xnoclassgc disable class garbage collection
-Xincgc enable incremental garbage collection
-Xloggc:<file> log GC status to a file with time stamps
-Xbatch disable background compilation
-Xms<size> set initial Java heap size
-Xmx<size> set maximum Java heap size
-Xss<size> set java thread stack size
-Xprof output cpu profiling data
-Xfuture enable strictest checks, anticipating future default
-Xrs reduce use of OS signals by Java/VM (see documentation)
-Xcheck:jni perform additional checks for JNI functions
-Xshare:off do not attempt to use shared class data
-Xshare:auto use shared class data if possible (default)
-Xshare:on require using shared class data, otherwise fail.
The -X options are non-standard and subject to change without notice.
可以使用简单的程序对此进行验证:
public class Test {
public static void main(String[] args) {
System.out.println("Test is starting!........");
if(null!= args && args.length>0){
System.out.println("print params..........");
for(String a: args){
System.out.println(a);
}
}
}
}
将上述代码编译之后,到其class二进制文件的根目录下(类文件的全限定名,类似于com.xxx.xxx.Test),执行java 命令,得到如下结果:
为啥执行一个命令就能够产生这种结果,其实有一个较为复杂的过程:
1.1 解析命令
前置条件是,机器平台需要安装jdk,具备jvm运行时环境;
当命令行回车后,以空格分隔解析命令字符串,左边第一个字符串为在环境变量path中对应的可执行文件的去后缀名称(在windows环境下可以是exe,cmd,bat等文件),这里就是java,即java.exe文件,此时windows就会去path中找到目录下的该文件,path大概是这样:
由图中可以看出有两处可能找到java.exe文件,javapath以及jdk1.8.0_201\bin目录,出现这种情况是安装了新版的jdk(exe文件安装jdk,会新增这样一个目录),windows取环境变量的顺序是从左到右依次取,取到了就停止检索,故而真正执行java的是javapath下的java.exe。
找到java.exe(这就相当于Java的api gateway)后,命令就剩下com.xxx.xxx.Main param1 param2 param3 … -Xms …,java.exe会去校验紧随java 之后的字符串,当检测到标准命令(比如:-version,-help等)做出特定响应。但当检测到其他字符串时,会尝试按照class文件执行,绝对路径或当前路径全限定名检索,找到二进制class文件,到了这一步,就会去调jvm.dll。这个时候命令就只剩下param1 param2 param3 … -Xms …,诸如-Xms的参数是jvm.dll内置的一些属性,这里不做说明,参考上文。
1.2 加载执行过程
当动态链接库文件jvm.dll被调用的时候,会启动java虚拟机,对于可感知的流程来说,紧接着jvm会BootstrapClassLoader,即根类加载器,加载java库核心依赖组件,如rt.jar,resource.jar等,提供了基础运行能力。与此同时会启动ExtClassLoader,用以加载扩展依赖库(jdk目录下/lib/ext的依赖)以及AppClassLoader加载应用程序class,AppClassLoader会将xxx.xxx.xxx加载到内存中并启动其入口函数,jvm规定为public static void main(String[] args),那么这个时候,param1 param2 param3就会转化为args(数组)作为入参,至此,程序进入main函数执行。因该类和函数都是用的jdk自带的类,所以不会存在加载其他应用类的情况,故而直接就执行成功了,如果是引用到其他应用类,则需要将被引用类也加载到当前jvm实例中。
2. java类加载过程
现在可以确定的是,Java程序在启动的时候,通过java.exe(入口)运行jvm.dll从而启动Java虚拟机,
jdk8中jvm会在这个时候就会去初始化sun.misc.Luancher启动类(jdk1.8以后有调整),初始化过程如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//初始化应用程序加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//将加载器设置为当前线程的类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
2.1 扩展类加载器
可见Luancher在构造时会依次创建扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader),其中,扩展类加载器创建过程中,会获取Java运行时变量java.ext.dirs配置的扩展类库路径并加载进来,我们可以通过java -Dxxx=xxxxxxx的命令对该值进行设置(实际上是虚拟机运行配置),我们在程序运行过程中通过System.getProperty(“java.ext.dirs”) 查看在当前jvm实例下的扩展类库,比如:E:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\Windows\Sun\Java\lib\ext,把所有的扩展类库都加载到jvm中,jdk8中,初始化ExtClassLoader通过如下代码实现:
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
//在初始化扩展类加载器的时候会在外层包装一层 AccessConroller.doPrivileged,用于临时提升当前程序的本地资源
//和系统资源的访问权限,这与jvm的安全模型及策略相关
return (Launcher.ExtClassLoader)AccessController.doPrivileged(
new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
//读取java.ext.dirs配置的扩展类路径,以;分割
//这里读取出来的基本都是jar包的绝对路径并生成File数组
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
//针对配置的扩展库目录按照该目录下的 meta-index,这个文件描述了对应路径下jar包的class文件清单
//会将jar包文件与meta-index记录的对应关系存放在volatile化的静态HashMap中,提供类加载的一个
//快速拒绝机制,因为类是不被允许重新加载的
MetaIndex.registerDirectory(var1[var3]);
}
//实例化扩展类加载器
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
首先,为了使得执行程序能够合法且安全地去访问本地资源以及系统资源,在执行扩展类加载器的时候会使用AccessController进行权限控制,这就要从jvm的安全模型及策略说起,针对jvm执行未知来源的代码(比如远程代码)时,可能存在安全隐患,因此jvm需要考虑具备较强的防攻击和抗干扰的安全防范能力。jvm的安全模型按照时间顺序如下:
- 早期的jvm采用沙箱将这种代码严格隔离执行;
- 对含有指定标识的合法代码允许其访问本地和系统资源;
- 用户对其要执行的代码进行签名,将代码放到不同权限的运行空间里,实现代码的差异化执行;
- jvm将代码加载到不同的域中,系统域或者应用域,不同的应用域以不同的权限通过系统域去访问资源;
而jdk在初始化ClassLoader的时候,为了获取对尽可能多的系统资源的访问权限,那么就需要临时破除这种应用域权限的限制,AccessController.doPrivileged就是jvm提供赋予程序资源访问特权的本地方法,以达到破域访问。这是个复杂的过程和机制,这里简介。
其次,通过MetaIndex来做文件和类的映射以达到对相同类的被加载的快速拒绝效果,使用本地线程间可见的HashMap存储包及类的直属路径映射关系进行管控,其大致逻辑就是找到对应的路径下的meta-idnex(元索引)文件,然后逐行读取解析并整理加载到HashMap中(其主要是被sun.misc.URLClassPath使用),文件格式:
% VERSION 2
% WARNING: this file is auto-generated; do not edit
% UNSUPPORTED: this file and its format may change and/or
% may be removed in a future release
! access-bridge-64.jar
com/sun/java/accessibility/
! cldrdata.jar
sun/text
sun/util
# dnsns.jar
META-INF/services/sun.net.spi.nameservice.NameServiceDescriptor
sun/net
! jaccess.jar
com/sun/java/accessibility/
# localedata.jar
sun/text
sun/util
# nashorn.jar
jdk/nashorn
META-INF/services/javax.script.ScriptEngineFactory
jdk/internal
# sunec.jar
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# sunjce_provider.jar
com/sun/crypto/
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# sunmscapi.jar
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# sunpkcs11.jar
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF
# zipfs.jar
META-INF/services/java.nio.file.spi.FileSystemProvider
com/sun/nio/
# jfxrt.jar
META-INF/INDEX.LIST
com/sun/deploy/uitoolkit/impl/fx/
com/sun/glass/events/
com/sun/glass/ui/
com/sun/glass/utils/
com/sun/javafx/
com/sun/media/jfxmedia/
com/sun/media/jfxmediaimpl/
com/sun/openpisces/
com/sun/pisces/
com/sun/prism/
com/sun/scenario/
com/sun/webkit/
javafx/animation/
javafx/application/
javafx/beans/
javafx/collections/
javafx/concurrent/
javafx/css/
javafx/embed/swing/
javafx/event/
javafx/fxml/
javafx/geometry/
javafx/print/
javafx/scene/
javafx/stage/
javafx/util/
netscape/javascript/
meta-index(元索引)文件中,主要包含以下五种类型的数据:
- 以% 开头的行表示说明性的内容;
- 以!开头的行表示只包含class文件jar文件名称;
- 以@开头的行表示只包含资源文件的jar文件名称;
- 以#开头的行表示资源文件和class文件的jar文件名称;
- 非上文特殊符号开头的表示上一个2,3,4对应的jar文件的子目录索引;
需要注意的是,每个jar文件都必定会有META-INF/MANIFEST.MF文件,这个是jar的标准文件。
最后,会实例化ExtClassLoader,进行两个实例化操作:
public ExtClassLoader(File[] var1) throws IOException {
//会实例化URLClassLoader属性,这里产生一些不可忽略的初始化行为,会另做研究
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
//需要注意的是初始化URLCLassLoader的时候会初始化JavaNetAccess,包含了URLClassPath
//通过URLClassPath去初始化LookupCacheLoader(实际上是一个ClassLoader)
//通过private static native URL[] getLookupCacheURLs(ClassLoader var0);本地方法检索
//传入的ClassLoader是否有缓存相应的URL,若存在则将该ClassLoader作为LookupCacheLoader
//否则将该URLClassPath标记为不使用LookupCache
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
2.2 应用类加载器
应用类加载器初始化的过程与扩展类加载器初始化的过程大概相同,区别在于AppClassLoader所选取的类库路径不同,初始化代码:
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
//在启动的时候,通过-classpath xxxx设置,是具体应用关联的类路径集合,以;分割
final String var1 = System.getProperty("java.class.path");
//将classpath中的文件路径转为文件对象集合
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
//获取文件访问特权
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
//通过classpath的文件对象获取URL集合
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
//实例化AppClassLoader
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
值得注意的是,AppClassLoader实例化的时候已经将先前生成的ExtClassLoader作为其parent classLoader,而ExtClassLoader则为空,即其父类加载器为BootStrapClassLoader。
到了这一步还是觉得有些欠缺,虽然知道了来加载器的初始化流程和逻辑,但却不知道类加载器的使用时机和类加载的逻辑,接下来结合类初始化具体代码来谈谈我自己对类加载及类加载器(ClassLoader)调度的理解,以下为本人写的自定义ClassLoader实现:
package com.spring.qinyu.self;
import lombok.extern.slf4j.Slf4j;
import java.io.FileInputStream;
@Slf4j
public class CustomizedClassLoader extends ClassLoader{
//Note: customized classpath is necessary, just like AppClassLoader's 'java.class.path',
//ExtClassLoader's 'java.ext.dirs', and BootStrapClassLoader's 'sun.boot.class.loader',
//so a customized classloader should contain a classpath to scan the specified class.
private static String customizedClassPath;
//set customized classpath value
public void setCustomizedClassPath(String classpath){
customizedClassPath = classpath;
}
//the constructor method
public CustomizedClassLoader(String classpath){
customizedClassPath = classpath;
}
//get byte[] of class file,which is match with parameter 'name'
private byte[] readByteFromClassPath(String name) throws Exception{
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(customizedClassPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
//a customized classloader must provide a findClass function
public Class<?> findClass(String name){
try {
byte[] b = readByteFromClassPath(name);
//call parant classloader's defineClass method, AppClassLoader
return defineClass(name,b,0,b.length);
}catch (Exception e){
log.error("find class error: ",e);
return null;
}
}
public static void main(String[] args) throws Exception{
//newthe customized classloader and init classpath
CustomizedClassLoader classLoader = new CustomizedClassLoader("F:/class");
//load a class not appeared in java.class.path
Class class0 = classLoader.loadClass("com.zds.appcore.biz.common.tencent.enums.TencentApiTypeEnum");
ClassLoader loader0 = class0.getClassLoader();
printClassLoaderPath(loader0);
System.out.println("================================");
//load a class a appeared in java.class.path which will be load by AppClassLoader
Class class1 = classLoader.loadClass("com.spring.qinyu.TestMain");
printClassLoaderPath(class1.getClassLoader());
}
//iterate the classloader's path, and print it
private static void printClassLoaderPath(ClassLoader classLoader){
while (classLoader != null){
System.out.println(classLoader);
classLoader = classLoader.getParent();
}
}
}
执行之后的输出结果为:
com.spring.qinyu.self.CustomizedClassLoader@5ef04b5
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2ef1e4fa
================================
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2ef1e4fa
由此可得以下几个总结:
- 每个类加载器都应该有自己的检索路径,启动类加载器的是sun.boot.class.path,扩展类加载器是java.ext.dirs,应用类加载器是java.class.path,当上述路径无法满足类加载需求时,应该自定义加载器,并为该类加载器指定具体的类加载基础路径;
- 类加载器加载类的本质是,将对应的.class文件转换成byte[ ]交由jvm进行加载,由readByteFromClassPath方法逻辑就可以看出,具体的byte[ ]加载是交由本地方法完成的;
- 类加载器的代系关系为(父==>子):启动类加载器==>扩展类加载器>应用类加载器>自定义加载器,因为双亲委派机制的存在,所以任何自定义的类加载器的父类加载器都是应用类加载器;
- 由上述两种被加载类可以看出,自定义的类加载器加载类时,会去检查父加载器是否已加载过该类,如果已加载过就不再加载,如果没加载过再去祖父类加载器检查加载,如此递归,如果最终都没有加载过,那么就会使用自定义的类加载器进行加载。
spring boot打的jar包能够直接运行,其自定义的类加载器起着不可或缺的作用,因为其所有的依赖包都在一个包内,需要一个区别于系统级类加载器的自定义类加载器按照一些规则加载依赖类并启动。