平时运行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的进程,如下图:

java 启动新进程 java怎么启动一个进程_加载

那么从jvm.dll执行到生成一个进程之间,是个什么样的过程?

dll文件是由c++代码实现的,在代码里,会为自己申请一份内存,然后c++会创建一个BootstrapClassLoader实例,学过JVM的类加载过程的就知道,java有三大类加载器,这个是我们的根类加载器。BootstrapClassLoader会将rt.jar包里所需的类(下方图片所示)加载进内存(方法区)中,然后会调用java代码里的Launcher类,即该类是java提供的入口类。

sun.boot.class.path是Bootstrap类加载器加载的类库,按需加载

下方的图可以通过命令获取:jinfo -sysprops <你的进程pid>

java 启动新进程 java怎么启动一个进程_加载_02


java 启动新进程 java怎么启动一个进程_java 启动新进程_03

Launcher类初始化过程

接下来就是解读下Launcher.getLauncher()代码干了啥事,这个是BootstrapClassLoader调用的方法。

Launcher类实例化的过程会涉及到类的加载过程,双亲委派机制,打破双亲委派机制的内容,上源码:

java 启动新进程 java怎么启动一个进程_类加载器_04


从这段代码可以看出就是直接new Launcher()对象,细心点还能发现‘sun.boot.class.path’路径就是上面截图的路径,并非胡乱猜的。

java 启动新进程 java怎么启动一个进程_java_05

从Launcher类的构造方法里可以看出一些信息:

  1. 这里有两个类加载器 ExtClassLoader和AppClassLoader,看语法可以知道是内部类。
  2. 先加载完ExtClassLoader,再加载AppClassLoader。
  3. 能看到最下面有一行代码,有个线程上下文类加载器ContextClassLoader。
  4. ExtClassLoader对象作为参数传给AppClassLoader类,知道双亲委派机制的应该能猜测出是作为父类加载器传进去的。

先从第一行代码getExtClassLoader()开始看起

java 启动新进程 java怎么启动一个进程_加载_06


java 启动新进程 java怎么启动一个进程_java 启动新进程_07


从这段源码可以知道:

5. ExtClassLoader类加载器加载的是java.ext.dirs目录下的类库,即上边cmd截图所示的目录。

6. ExtClassLoader继承于URLClassLoader类。

7. 将java.ext.dirs下的类读取到MetaIndex下,并且作为参数传递给ExtClassLoader构造器。

跟踪下构造器,发现ExtClassLoader接收一个null作为父类ClassLoader参数,如下源码,上面猜测ExtClassLoader作为AppClassLoader类加载器的父类加载器(并不是继承)也就证实了。

java 启动新进程 java怎么启动一个进程_java 启动新进程_08


java 启动新进程 java怎么启动一个进程_类加载器_09


再看下Launcher构造器的第二行代码getAppClassLoader()方法:

java 启动新进程 java怎么启动一个进程_加载_10


解读到的信息:

8. AppClassLoader同样继承于URLClassLoader,即文件路径在java里都是URL形式。

9. AppClassLoader类加载器要加载的路径是java.class.path,即我们配置的环境变量。

10.返回一个AppClassLoader对象 ,var0参数(即ExtClassLoader实例对象)作为父类参数传递。

至此,已经实例化了ExtClassLoader和AppClassLoader类加载器,即我们熟知的java三大类加载器都已实例化完毕,并且知道它们会加载哪些类库。

类加载器的双亲委派机制

先来看看第四句代码loadClass()

java 启动新进程 java怎么启动一个进程_java_11


java 启动新进程 java怎么启动一个进程_加载_12


细心的可以知道this.loader.loadClass()中的this.loader指的是AppClassLoader对象。

解读下loadClass()方法步骤:

  1. 参数name指的是二进制类名
  2. getClassLoadingLock(name)对当前类进行加锁,锁逻辑是通过ConcurrentHashMap#putIfAbsent方法判断锁的存在与否,感兴趣可自行查看源码。
  3. 代码3是回调本函数,当前类加载器没有加载过则让父类加载器去查找。
  4. 代码4表示当前类加载器如果没有父加载器,则调用Bootstrap类加载器查找类。
  5. 代码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文件内容,源码如下:

java 启动新进程 java怎么启动一个进程_java 启动新进程_13


该方法是loadClass方法里的代码5,主要逻辑就是红圈里的代码,步骤如下:

  1. 参数类名是类似sun.misc.Launcher这样用"."来连接的,需要转换为目录结构的“/”形式,并加载文件扩展名。
  2. 从URLClassPath中查找该路径类,上面源码已经解释了所有类都是URL形式的。
  3. 找到了该类的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类源码。
其实类加载过程这块,每一个步骤如果细说,还有很多东西可以说,比例内存架构怎么划分,对象怎么申请内存等等,内容太多,放到下个篇章说。每天进步一点点…