定义
英文:Java Virtual Machine
中文:Java虚拟机,vmware虚拟机运行的是操作系统,Java虚拟机运行的是Java程序
JVM跨平台原理

JVM就是一个运行在操作系统上的程序,就像微信、QQ一样,不同操作系统上的JVM安装包不一样,而我们说的JDK和JRE就是JVM的安装包,我们双击QQ图标就能运行QQ,而我们运行java命令就能运行JVM,不管操作系统是什么,JVM运行起来后提供的功能是一样的,都是用来执行代码的,就像不同操作系统上的QQ、微信一样,都是用来聊天的。

不同操作系统上运行的JVM是不一样的,这才是JVM跨平台的本质!我们写一份Java代码,编译为字节码后,之所以能在不同操作系统上运行,就是因为不同操作系统上的JVM都能运行字节码,相当于不同操作系统上的JVM屏蔽了不同操作系统的底层区别。
那字节码的作用是什么呢?

JVM会逐行解释执行字节码,那为什么不逐行解释执行Java代码呢?不一样吗,一份Java代码对应的字节码是一样的,一对一的关系,为什么要把Java代码编译为字节码,因为性能,为了提高效率,如果直接把Java代码翻译为机器指令,也不是不行,也就是解释执行,这样就会导致Java代码再运行时效率比较低,一般的解释型语言效率都比较低,而如果我们提前先对Java代码进行编译,编译为字节码,那字节码再翻译为机器指令时,效率比较块,也就导致真正执行字节码时,效率会比较高,这就是字节码的作用,所以Java其实是编译+解释二合一的语言。
JVM与字节码

JVM关心的是字节码,而不是Java代码,所以一门语言只要能编译为字节码,那么也能在JVM上运行。
JVM整体结构

先将java文件编译为class文件,再利用类加载器将class文件加载到方法区中,然后由解析器逐行执行字节码,每执行一个Java方法,就将方法存入Java栈,每执行一个本地方法,也就是native方法,就将方法存入本地方法栈中,方法执行完后就从栈中移除,程序计数器用来记录当前正在执行的字节码指令地址,方法执行过程中产生的Java对象会存入堆中,垃圾回收器会回收已经没有被使用的Java对象,JIT编译器会在程序运行过程中发现热点代码,并编译为机器指令,从而提高执行效率。
类加载子系统

- 类加载子系统会将某个class文件加载到方法区的内存空间中,可以理解为把class文件中字节码指令,读取到内存中。
- 验证阶段会验证待加载的class文件是否正确,比如验证文件格式
- 准备阶段会为static变量分配内存并赋零值
- 解析阶段会将符号引用解析为直接引用,在一个字节码文件中,会用到其他类,而在字节码中只会存用到的类的类名,而解析阶段就是会根据类名找到该类加载后在方法区中的地址,也就是直接引用,并替换调符号引用,这样真正运行字节码时,就能直接找到某个类了。
- 初始化阶段会给static变量赋值,并执行static块
类加载器的分类

在JVM规范中,把类加载器分了两种:
- 一种是BootStrapClassLoader,这是由C和C++实现的,负责加载jre/lib下的jar包中的类,比如rt.jar中的String类
- 一种是继承了ClassLoader抽象类的类加载器,是由Java语言实现的,比如:
- ExtClassLoader,用Java实现的,加载目录为jre/lib/ext目录下的类,可以看下Launcher类中的代码
- AppClassLoader,用Java实现的,加载目录为classpath所指定的目录,可以看下Launcher类中的代码
- 以及其他自定义的,比如Tomcat中的WebAppClassLoader
比如在Launcher类中有两个静态内部类:
static class AppClassLoader extends URLClassLoader {// ...}static class ExtClassLoader extends URLClassLoader {// ...}

通过继承URLClassLoader,最终间接继承了ClassLoader。
双亲委派

直接看代码:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 首先,检查类是否被加载了Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// parent有值则委托给parent去加载,否则就委托给BootstrapClassLoader去加载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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
这段代码就体现了双亲委派,通常我们用AppClassLoader去加载一个类时,AppClassLoader有一个parent属性指向了ExtClassLoader,当我们用AppClassLoader去加载一个类时,会先委托给ExtClassLoader去加载,而ExtClassLoader没有parent属性,所以会委派给BootstrapClassLoader去加载,只有BootstrapClassLoader没有加载到,才会由ExtClassLoader去加载,也只有ExtClassLoader没有加载到,才会由AppClassLoader来加载,这就是双亲委派。
双亲委派的优点:
- 避免类的重复加载,如果一个类被BootStrapClassLoader加载过了,那么AppClassLoader就不会再重复加载到这个类了。
- 防止核心API被篡改,自定义一个java.lang.String类,但是我们是不到这个类的,因为根据双亲委派始终加载的都是rt.jar中的java.lang.String类
Tomcat为什么要自定义类加载器?
为了进行类的隔离,如果Tomcat直接使用AppClassLoader类加载类,那就会出现如下情况:
- 应用A中有一个com.zhouyu.Hello.class
- 应用B中也有一个com.zhouyu.Hello.class
- 虽然都叫做Hello,但是具体的方法、属性可能不一样
- 如果AppClassLoader先加载了应用A中的Hello.class
- 那么应用B中的Hello.class就不可能再被加载了,因为名字是一样
- 如果就需要针对应用A和应用B设置各自单独的类加载器,也就是WebappClassLoader
- 这样两个应用中的Hello.class都能被各自的类加载器所加载,不会冲突
- 这就是Tomcat为什么用自定义类加载器的核心原因,为了实现类加载的隔离
- JVM中判断一个类是不是已经被加载的逻辑是:类名+对应的类加载器实例
