Java的口号是什么?“一次编译,到处运行!”

简单聊聊JVM

我们在刚开始学习Java的时候,一定使用过javac来编译.java文件,然后使用java来运行.class文件。
1.1 编译过程:
.java文件是使用Java源码编译器(也就是上面说到的javac.exe)来完成的。
Java源码编译器流程
总结一下就是由以下三个过程组成:

1.2 JVM实现跨平台:
通过Java源码编译器,我们成功的将.java文件生成出.class文件。这些.class文件是不能直接运行的,它不像是C/C++。
这些生成的.class文件是交给JVM来解析运行的。
JDK是区分操作系统的,只要你安装了JDK,那么这个JDK就可以匹配你的操作系统。JDK中的JVM是运行在操作系统上的,.class文件是运行在JVM上的,所以完全不需要担心字节码是在哪个系统操作系统上编译完成的,只要符合标准,那么就完全可以运行。这样,Java就实现了“一次编译,到处运行”。
1.3 类的加载时机:
生成后的.class文件中的类都会加载进JVM中吗?
实际上,JVM严格的规范了哪些情况下会对类立刻进行初始化:

  • 创建类的实例(new 的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法;
  • 反射的方式;
  • 初始化某个类的子类,则其父类也会被初始化;
  • Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类(包含main方法的那个类)。

所以,JVM中类的加载是动态的,它不会一次性将所有的类都加载运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。当然这是为了节省内存开销。
1.4 如何将类加载到JVM中:
.class文件通过类的加载器装载到JVM中。
Java中的类加载器:
ClassLoader
每种加载器的工作职责:

  • Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类;
  • Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包;
  • App ClassLoader:负责记载classpath中指定的jar包及目录中class。

工作流程:

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载;
  • 如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
优点就是可以防止内存中出现多份同样的字节码。
备注
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
1.5 JIT即时编译器
JVM加载了.class文件以后,难道会逐条取出,逐条执行吗?
如果是设想的这样的话,速度就太慢了。
实际上,JVM有自己的想法:

  • 将这些Java字节码重新编译优化,生成机器码,让CPU直接执行。这样编出来的代码效率会更高;
  • 编译也是要花费时间的,我们一般对热点代码做编译,非热点代码直接解析就好了。

备注:热点代码包含(1).多次调用的方法;(2).多次执行的循环体。
那么我们怎么知道是不是热点代码,我们可以通过热点探测来检测。目前的热点探测使用计数器的方式,它为每个方法准备了两类计数器:

  • 方法调用计数器(Invocation Counter)
  • 回边计数器(Back EdgeCounter)
  • 在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。


拓展阅读:

1.6 JVM的内存结构
我们可以来看看JVM内存结构:

  • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存;
  • 虚拟机栈:虚拟机栈描述的是Java方法执行的内存结构:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息;
  • 本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务;
  • 方法区:存储已被虚拟机加载的类元数据信息(元空间);
  • 程序计数器:当前线程所执行的字节码的行号指示器。

1.7 工作流程:

参考文章:
https://www.cnblogs.com/qiumingcheng/p/5398610.html Java程序编译和运行的过程


未完待续,嘻嘻

最后更新: 2019年11月17日 23:36

原始链接: http://leiii33.github.io/2019/11/15/JVM-从入门到放弃/