定义

英文:Java Virtual Machine
中文:Java虚拟机,vmware虚拟机运行的是操作系统,Java虚拟机运行的是Java程序

JVM跨平台原理

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

image.png

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

那字节码的作用是什么呢?

image.png

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

JVM与字节码

image.png

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

JVM整体结构

image.png

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

类加载子系统

image.png

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

类加载器的分类

image.png

在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类中有两个静态内部类:

  1. static class AppClassLoader extends URLClassLoader {
  2. // ...
  3. }
  4. static class ExtClassLoader extends URLClassLoader {
  5. // ...
  6. }

image.png

通过继承URLClassLoader,最终间接继承了ClassLoader。

双亲委派

image.png

直接看代码:

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // 首先,检查类是否被加载了
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. // parent有值则委托给parent去加载,否则就委托给BootstrapClassLoader去加载
  11. if (parent != null) {
  12. c = parent.loadClass(name, false);
  13. } else {
  14. c = findBootstrapClassOrNull(name);
  15. }
  16. } catch (ClassNotFoundException e) {
  17. // ClassNotFoundException thrown if class not found
  18. // from the non-null parent class loader
  19. }
  20. if (c == null) {
  21. // If still not found, then invoke findClass in order
  22. // to find the class.
  23. long t1 = System.nanoTime();
  24. c = findClass(name);
  25. // this is the defining class loader; record the stats
  26. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  27. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  28. sun.misc.PerfCounter.getFindClasses().increment();
  29. }
  30. }
  31. if (resolve) {
  32. resolveClass(c);
  33. }
  34. return c;
  35. }
  36. }

这段代码就体现了双亲委派,通常我们用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类加载类,那就会出现如下情况:

  1. 应用A中有一个com.zhouyu.Hello.class
  2. 应用B中也有一个com.zhouyu.Hello.class
  3. 虽然都叫做Hello,但是具体的方法、属性可能不一样
  4. 如果AppClassLoader先加载了应用A中的Hello.class
  5. 那么应用B中的Hello.class就不可能再被加载了,因为名字是一样
  6. 如果就需要针对应用A和应用B设置各自单独的类加载器,也就是WebappClassLoader
  7. 这样两个应用中的Hello.class都能被各自的类加载器所加载,不会冲突
  8. 这就是Tomcat为什么用自定义类加载器的核心原因,为了实现类加载的隔离
  9. JVM中判断一个类是不是已经被加载的逻辑是:类名+对应的类加载器实例