平时运行java程序都是java -c xxx.java, java xxx.class, java -jar xxx.jar等命令,然后我们的类或者jar包项目就能跑起来了,那么java里面到底做了什么操作呢?
现在就来解读一下,会涉及类加载的知识:
从 java -jar Test.jar命令开始,当我们运行命令时,java命令会到我们的jdk安装目录下找到jvm.dll动态链接库文件,创建一个jvm实例,表现形式就是在windows系统里的任务管理器里多了个java的进程,如下图:
那么从jvm.dll执行到生成一个进程之间,是个什么样的过程?
dll文件是由c++代码实现的,在代码里,会为自己申请一份内存,然后c++会创建一个BootstrapClassLoader实例,学过JVM的类加载过程的就知道,java有三大类加载器,这个是我们的根类加载器。BootstrapClassLoader会将rt.jar包里所需的类(下方图片所示)加载进内存(方法区)中,然后会调用java代码里的Launcher类,即该类是java提供的入口类。
sun.boot.class.path是Bootstrap类加载器加载的类库,按需加载
下方的图可以通过命令获取:jinfo -sysprops <你的进程pid>
Launcher类初始化过程
接下来就是解读下Launcher.getLauncher()代码干了啥事,这个是BootstrapClassLoader调用的方法。
Launcher类实例化的过程会涉及到类的加载过程,双亲委派机制,打破双亲委派机制的内容,上源码:
从这段代码可以看出就是直接new Launcher()对象,细心点还能发现‘sun.boot.class.path’路径就是上面截图的路径,并非胡乱猜的。
从Launcher类的构造方法里可以看出一些信息:
- 这里有两个类加载器 ExtClassLoader和AppClassLoader,看语法可以知道是内部类。
- 先加载完ExtClassLoader,再加载AppClassLoader。
- 能看到最下面有一行代码,有个线程上下文类加载器ContextClassLoader。
- ExtClassLoader对象作为参数传给AppClassLoader类,知道双亲委派机制的应该能猜测出是作为父类加载器传进去的。
先从第一行代码getExtClassLoader()开始看起
从这段源码可以知道:
5. ExtClassLoader类加载器加载的是java.ext.dirs目录下的类库,即上边cmd截图所示的目录。
6. ExtClassLoader继承于URLClassLoader类。
7. 将java.ext.dirs下的类读取到MetaIndex下,并且作为参数传递给ExtClassLoader构造器。
跟踪下构造器,发现ExtClassLoader接收一个null作为父类ClassLoader参数,如下源码,上面猜测ExtClassLoader作为AppClassLoader类加载器的父类加载器(并不是继承)也就证实了。
再看下Launcher构造器的第二行代码getAppClassLoader()方法:
解读到的信息:
8. AppClassLoader同样继承于URLClassLoader,即文件路径在java里都是URL形式。
9. AppClassLoader类加载器要加载的路径是java.class.path,即我们配置的环境变量。
10.返回一个AppClassLoader对象 ,var0参数(即ExtClassLoader实例对象)作为父类参数传递。
至此,已经实例化了ExtClassLoader和AppClassLoader类加载器,即我们熟知的java三大类加载器都已实例化完毕,并且知道它们会加载哪些类库。
类加载器的双亲委派机制
先来看看第四句代码loadClass()
细心的可以知道this.loader.loadClass()中的this.loader指的是AppClassLoader对象。
解读下loadClass()方法步骤:
- 参数name指的是二进制类名
- getClassLoadingLock(name)对当前类进行加锁,锁逻辑是通过ConcurrentHashMap#putIfAbsent方法判断锁的存在与否,感兴趣可自行查看源码。
- 代码3是回调本函数,当前类加载器没有加载过则让父类加载器去查找。
- 代码4表示当前类加载器如果没有父加载器,则调用Bootstrap类加载器查找类。
- 代码5,遍历所有类加载器所加载的类还是没找到,则调用findClass从各个jar文件里查找该类。
至此,类加载器的双亲委派机制就出来了,以类A为例:
AppClassLoader类加载器先去查找类A是否被加载过,没有则委托父类ExtClassLoader类加载器去加载类A,ExtClassLoader类加载器也没有找到类A,则委托BootstrapClassLoader去查找,如果也没有找到,则看看类A是否在自己加载的范围jar包内(sun.boot.class.path里),没有则让ExtClassLoader类加载器尝试去加载类A,如果ExtClassLoader在java.ext.dirs目录下找不到类A可加载,则让AppClassLoader加载,如果在java.class.path中找不到,则抛出异常。
即委派机制是先保证让最上级先加载,没有则逐级往下加载。
类加载过程
在类加载器的双亲委派机制中,有一步是当前类加载器如果在自己的范围jar包中找到了class文件,则会将class文件加载进内存(JVM中的方法区)中,并且解析class文件内容,源码如下:
该方法是loadClass方法里的代码5,主要逻辑就是红圈里的代码,步骤如下:
- 参数类名是类似sun.misc.Launcher这样用"."来连接的,需要转换为目录结构的“/”形式,并加载文件扩展名。
- 从URLClassPath中查找该路径类,上面源码已经解释了所有类都是URL形式的。
- 找到了该类的Resource对象,交给defineClass方法去解析,即上边逻辑图中标红字体步骤。
类加载的过程主要代码都在defineClass方法里,但是是native方法,所以没法展示代码,但步骤大致如下:
- 加载:将类文件加载到方法区中
- 验证:
- 读class文件内容,
- 检查里面的内容是否符合java规定的语义,
- 是否java定义的开头,版本号信息,
- 解析类信息,方法,属性,验证是否符合java语法,
- 生成Class类对象,作为方法区中提供访问该类的入口(可通过反射机制获取相关信息等)
- 准备:此时类信息已经解析完毕,
- 给类的静态变量分配在堆中内存空间(1.8已经不在方法区中分配了),赋默认值,比如0
- 解析:将字面量(字面上的字符串表示,称为符号引用)常量池里的信息解析成直接引用(即分配内存,将对象引用指向该内存地址),这种是静态链接,如果是运行时才解析成直接引用,则是动态链接。
- 初始化:开始对变量进行赋值(开发人员给的值),执行静态代码块
至此,java运行程序的所有准备都ok了,现在只需要调用main方法就行了,这个是c++源码定义死了,c++会找到main方法并调用。
打破双亲委派机制
在最上面,我略过了第三行代码没说:setContextClassLoader(this.loader);
这行代码是将AppClassLoader作为上下文类加载器,那这个上下文类加载器是干什么用的呢?
像JDBC这类第三方引入的代码,是不是应该被Bootstrap类加载器加载调用,但是该jar又不在sun.boot.class.path范围内,那你让Bootstrap类加载器怎么调用啊,代码何苦为难代码呢。为了解决这情况,造了个上下文类加载器出来,这样Bootstrap类加载器就可以让上下文类加载器去帮忙额外的加载类。这也是java的spi机制,通过约定,在METE-INF/services目录下放置个实现类的文件,通过上下文类加载器去加载调用。
与双亲委派机制加载类顺序倒过来了,这也就打破了双亲委派机制,这也是需求所需,没办法中的办法。
类似于tomcat热部署也是一种情况。
完结了,打破双亲委派机制只是侧重的说了下,对SPI机制感兴趣的可以看ServiceLoader类源码。
其实类加载过程这块,每一个步骤如果细说,还有很多东西可以说,比例内存架构怎么划分,对象怎么申请内存等等,内容太多,放到下个篇章说。每天进步一点点…