一、JVM是什么,能解决什么问题?
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算机的规范,它是一个虚构出来的计算机,是通过在实际计算机上仿真模拟各种计算机功能来实现的。
JVM帮助编程语言屏蔽与具体平台(OS)相关的信息,使得由各中编程语言编写的编译程序只需在JVM上生成运行的字节码,就可以在多种平台上运行(WORA, Write Once Run Anywhere)。
二、JVM体系结构
JVM主要分为类加载器子系统、运行时数据区、执行引擎、本地方法接口以及垃圾收集模块。
2.1 类加载器(ClassLoader)
2.1.1 类加载器作用
负责加载并初始化class文件,得到真正的class类,class文件在文件开头有特定的文件标识(比如说编译的版本信息),将class文件的字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,classLoader只负责class文件的加载,至于它是否可以运行,则有Execution Engine(执行引擎)决定。
2.1.2 字节码
jvm不能执行java文件,我们编写的java文件需要经过javac编译器编译成字节码(.class),然后再由JVM执行。java bytecode 由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。但是实际上java只使用了200左右的操作码,还有一些操作码则保留给调试操作。ClassLoader将.class文件加载并初始化后,得到当前类的Class模板,并将其存放在方法区
使用字节码有什么优势? java语言通过字节码的方式,在一定程度上解决了传统解释语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以java程序运行时比较高效,而且由于字节码并不专对一种特定的机器,因此,java程序无须重新编译便可在多种不同的计算机上运行。
解释型语言:在运行的时候将程序翻译成机器语言。解释型语言的程序不需要在运行前编译(java语言是需要提前编译成字节码的),在程序运行的时候才进行翻译,专门的解释器负责在每个语句执行的时候解释程序代码。这样解释型语言每执行一次就要翻译一次,效率比较低。(例如:python、php)
2.1.3 ClassLoader种类
- JVM自带的加载器(系统类加载器,加载当前应用的classpath的所有类)
- 启动类加载器(也叫根加载器Bootstrap):C++编写,存储位置:$JAVAHOME/jre/lib/rt.jar
- 扩展类加载器(Extension):Java编写,javax开头的,都是扩展包,存储位置:$JAVAHOME/jre/lib/ext/*.jar
- 应用程序类加载器(AppClassLoader),平时程序中自定义的类,new出来的。
- 用户自定义加载器
- java.lang.ClassLoader的子类
- Java的类加载机制:启动类加载器 --> 拓展类加载器 --> 应用程序类加载器
2.1.4 双亲委派的机制
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自已先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成此加载任务,子加载器才尝试自已去加载;
为啥使用双亲委派机制:
- 防止内存中出现多份同样的字节码。如果没有使用双亲委派机制,由各个类加载器自行进行类加载,会使得java类型中最基础的类出现混乱,比如用户自定义java.lang.Object类,并放在Classpath中,那系统就会出现多个Object,程序就会变得混乱。
2.1.5 沙箱安全机制
通过双亲委派机制,类的加载永远都是从 启动类加载器开始,依次下放,保证你所写的代码,不会污染Java自带的源代码,所以出现了双亲委派机制,保证了沙箱安全
2.2 运行时数据区(Runtime Data Area)
2.2.1 堆(Heap)
详见内存模型篇
2.2.2 方法区(Method Area)
- 供各线程共享的运行时内存区域,存储了每一个类的结构信息
- 比如: 运行时常量池(Runtime Constant Pool)、字段 和 方法数据、构造函数和普通方法的字节码内容。
- 方法区是一个规范/概念定义,并不是一种具体的实现,在各版本的JVM中实现的方式不一致
- JDK1.7中的永久代(PerGen space)在JVM堆中
- JDK1.8中的元空间(Metaspace)在计算机内存中
- “元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义; 不能说“JVM元空间是方法区”,但可以说JDK8以后的HotSpot中“元空间用来实现了方法区”
- “元空间”的空间范围默认是: 最小:20.75MB 最大:16EB(GB -> TB -> PB -> EB)
2.2.3 栈(栈帧)
栈管运行,堆管存储
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建。他的生命周期是跟随线程的生命周期,线程结束那么栈内存也就随之释放, 对于栈来说不存在垃圾回收问题 ,只要线程已结束该栈就over了,是线程私有的。8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。
StackOverflowError: ? 若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。 ? JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用,代码见下:
2.2.4 本地方法栈(Native Method Stack)
本地方法栈服务的对象是JVM执行的native方法, 而虚拟机栈服务的是JVM执行的java方法。 它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。
2.3 执行引擎(Execution Engeine)
执行引擎负责解释命令,提交操作系统执行
2.4 本地方法接口(Native Interface)
native :在Java中是一个关键字,有声明,无实现。 以线程为例,不要以为线程是属于Java的一个东西,其实它是属于操作系统底层的,Java中通过Thread类的start() 类启动一个线程,
进入Thread的start()的源码,你会看到虽然调用的是start(),但其实调用的start0()这个方法, 最终是由 private native void start0();这段代码去跟底层做了交互实现,有声明,无实现,Java到此交由系统去处理了。
三、JVM如何运行JAVA类
3.1 java为什么要在JVM中运行
Java作为一门高级语言,其抽象程度很高,语法非常复杂。因此无法直接运行在机器硬件上,所以java程序在运行之前,需要对其进行一番转换。转换思路:设计一个面向java语言特性的虚拟机,并通过编译器将Java转换成该虚拟机所能识别的指令序列(java字节码)。这个虚拟机可以由硬件直接实现,但是更为常见的是由各个平台(Windows x64,Linux_aarch64等)进行软实现,这么做的意义在于,一旦一个Java程序被转换成Java字节码,那么它便可以在不同平台上的虚拟机里运行,也就是“一次编写,到处运行”。
虚拟机还有个好处:提供托管环境(Managed Runtime)。这个托管环境能够代替我们处理一些代码中冗长而且容易出错的部分,比如内存管理与垃圾回收。此外还提供了诸如:数据越界、动态类型、安全权限等动态检查。使我们能够少写一些业务逻辑无关的代码。
3.2 Java虚拟机如何运行Java字节码
以HotSpot虚拟机为例:
- 加载由Java文件编译而成的class文件到虚拟机。加载好存放到方法区。实际运行时,JVM会执行方法区中的代码。
- 执行时需要将字节码翻译成机器码,有两种翻译方式:解释执行与即时编译。
- 解释执行:逐条将字节码翻译成机器码并执行。优势:无需等待编译。
- 即时编译(Just-In-Time Compilation, JIT):一个方法中所有的字节码都翻译成机器码后再执行。优势:执行效率高。
- HotSpot内置了多个即时编译器:C1、C2以及Graal(Java10才引入的,实验性的)。
- C1:Client编译器,面向启动性能要求更高的客户端GUI程序。
- C2:Server编译器,面向对峰值性能有要求的服务器端程序,优化手段相对复杂,因此编译时间长。但生成的代码执行效率高。
3.3 Java虚拟机运行java类
3.4 Java虚拟机运行java方法
- 首先有个执行栈,存储目前所有活跃的方法,以及它们的局部变量和参数。
- 当一个新方法被调用时,会在执行栈的栈顶压入一个新的栈帧,该方法分配的本地变量和参数会存储在这个栈帧中。
- 跳到目标方法代码执行。
- 方法执行完返回的时候,本地变量和参数都会被销毁,该方法所在栈帧会进行出栈操作(栈顶被移除)。
- 返回原来的地址执行。
四、即时编译(Just In Time Compilation)
附件:参考资料
- JVM体系结构概述
- JVM 双亲委派机制
- JVM中常量池存放在哪里
- Java8中的JVM元空间是不是方法区?