java运行机制
java虚拟机构建在操作系统之上,java文件可以用JDK自带的java编译器(javac)编译成字节码,字节码经过JVM翻译成本地机器语言(操作系统和底层硬件平台都可以理解),只要能运行JVM虚拟机就可以跑java程序(还有其他语言比如Groovy,Scala,Kotlin等,因为他们最终都翻译成了字节码)
jvm整体架构
JVM只是一种规范,具体的实现因供应商而异,我们来看一下最流行的实现架构(图中的方法区在java8之后已经替换成了元空间[metespace])
1)类加载器系统
JVM是驻留在RAM(主存)里的,JVM在运行的时候可以通过类加载系统将class文件信息加载到RAM中,这就是java的动态类加载功能,在第一次引用这个类的时候会经历加载,链接,和初始化。
1.1)加载
把class文件加载进内存里是类加载器的主要任务,程序首先从主类开始加载,所有后续尝试装入的类都是根据已经运行的类中的引用来完成的,比如下面这些情形:
- 当字节码引用一个静态类的时候(
System.out
) - 当字节码创建一个类的时候(
A a = new A()
)
类加载器主要有四个原则:
1.1.1)可见性原则
子类加载器可以看见父类加载器载入的类信息,但是父类加载器不可以看见子类加载器加载的类信息
1.1.2)唯一性原则
被父类加载器加载过的类不应该再被子类加载器加载一次
1.1.3)向上委托原则(很多时候听到的是双亲委派原则,但是个人感觉会导致更疑惑)
一个类请求加载的时候,先请求Application加载器,Application加载器先不直接加载这个类,而是向上请求Extension加载器,同样的,Extension加载器再向上请求Bootstrap加载器,只要其中有一个加载器类路径中发现了请求加载的类,那么就会返回,否则就会原路返回到Application加载器(Application -> Extension -> Bootstrap -> Extension -> Application)
1.1.4)不卸载原则
类加载器不能卸载类
1.2)链接
链接有三个阶段:
1.2.1)验证
检查是否符合java语法规范
1.2.2)准备
为静态域和被JVM使用的数据类型(比如方法表)分配内存,静态域被分配内存并赋予默认值,并不会执行代码
1.2.3)解析
将符号引用替换成直接引用
1.3)初始化
将静态变量从默认值改成代码中赋予的初始值,静态代码块里的代码会执行
2)运行时数据区
运行时数据区域是JVM程序在操作系统上运行时分配的内存区域,类加载器为每个类生成相应的二进制数据并在方法区中保存以下信息(ps:java8后方法区变成了元空间,但是否和之前方法区存放的数据一样目前还没考证):
- 载入的类和其直接父类的全限定名
- class文件是否是Class/Interface/Enum类型
- 修饰符,静态变量,和方法信息
对于每个class文件,只会在堆内存中创建一个class对象,这些类对象可以用于读取类级别的信息(类名,父类,方法,变量信息,静态变量)
2.1 方法区(线程共享)
存放类级别的信息:
- 类加载器信息
- 运行时常量池——数字常量,引用字段,引用方法,属性,当要用到一个方法或者字段的时候,JVM会通过这个运行时常量池去查找其真正的地址
- 字段数据——每个字段的:字段名,类型,修饰符,属性
- 方法数据——每个方法的:方法名,返回类型,参数类型,修饰符,属性
- 方法代码——每个方法的:字节码,操作数栈大小,局部变量的大小,局部变量表,异常表,异常表中每个异常的处理器
2.2 堆区(线程共享)
每个对象的实例都在堆内存产生,垃圾清理(GC)主要针对的也是这一块
2.3 栈区(线程私有)
这个区域不是共享的,每个线程会配备一个运行时栈来储存方法调用所产生的帧,每一次方法调用产生一片对应的栈帧
每一片栈帧持有这些引用:局部变量数组,操作栈,类的运行时常量池,局部变量数组和操作数栈的大小在编译时就已确认,因此,方法栈帧的大小在运行时是确定的。
方法调用正常返回或者抛出异常时帧出栈
2.4 程序计数器(线程私有)
- 如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址
- 如果正在执行的是Native 方法,则这个技术器值为空(Undefined)
2.5 本地方法栈(线程私有)
java线程和操作系统的线程之间存在映射,当java线程准备好之后,会创建一个本地方法栈用来储存调用JNI产生的信息
3)执行引擎
字节码最终都是在执行引擎这里运行
3.1 解释器
解释器负责翻译字节码(让操作系统可以识别)和执行指令,解释器翻译的速度非常快,但是执行的速度相对来说慢一点,有一个缺点就是:如果一个方法多次调用,那么这个翻译和执行的过程会多次重复。
3.2 JIT编译器
如果只有解释器,那么当一个方法多次调用的时候,每次都要执行一次一模的解释操作,JIT就是为了解决这个问题的,首先,它将整体字节码编译成机器码,当同一个方法多次调用的时候,直接提供上次编译得到的机器码,这比逐行解释指令快的多,解释过后的机器码存在缓存中
然而,JIT编译花的时间比解释器还多,因为前者是整体编译的,这样来看,对于那些只执行一次的代码片段,解释器是更好的选择,JIT编译器内部会检查方法的调用次数,达到某个设定的值的时候,才会去编译它,Oracle Hotspot虚拟机就是用了这种自适应编译
的办法
3.3 垃圾回收器
只要对象被引用了,JVM就判定它是存活的,一般来说,垃圾回收是JVM系统底层帮你做了的事,不过你也可以手动调用System.gc()
(但是并不保证你调用了就执行,可以使用Thread.sleep(1000)
给GC腾出时间来让它执行)
4)java本地接口(JNI)
JNI的出现就是为了使得java能够调用C/C++写的一些库
5)本地方法库
执行引擎所需要的一些C/C++本地库,并且提供了本地接口去访问这些库
java线程
有一部分的线程是携带逻辑且由用户主动创建的(应用线程),还有一部分的线程是JVM自身创建来处理系统后台任务的(系统线程)
主要的应用线程就是main
线程,其他的应用线程
都是通过main
线程产生的,应用线程执行一些任务比如:从main()
方法开始执行代码,在堆内存上创建一些对象
而系统线程主要由如下几种
- 编译器线程:在运行期间,这些线程负责把字节码编译成机器码
- GC线程:所有GC相关的活动都由这些线程来接管
- 定时任务线程
- 信号分派器线程
- VM线程:作为一个先决条件,一些操作需要JVM达到一个安全点,在这个点上不会再发生对堆区域的修改。这类场景的例子有
stop-the-world
垃圾收集、线程栈转储、线程挂起和撤销偏向锁,这些操作可以在一个称为VM线程的特殊线程上执行。
一些需要了解的要点
- java是一门同时具有编译器和解释器的语言
- JIT编译器弥补了解释器重复操作的缺点,因为JIT编译器可以保存字节码编译过后得到的机器码
- 最新的Java版本解决了其原始体系结构中的性能瓶颈
- JVM只是一种规范。在实现过程中,供应商可以自由地定制、创新和改进其性能
PS:储存器的小知识
- RAM:断电就丢失数据
- ROM:只能读取数据,是写死在里面的,所以断电也不会丢失数据
延展阅读
Class Loaders in Java
Compiled vs. Interpreted Languages
Understanding JVM Architecture
What does java “VM thread” do?
Symbolic references in Java