Java基础-JVM
JVM原理分析
前言:
JVM一直是java知识里面进阶阶段的重要部分,如果希望在java领域研究的更深入,则JVM则是如论如何也避开不了的话题,本系列试图通过简洁易读的方式,讲解JVM必要的知识点。
一. JVM是什么
JVM它是Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的,组成部分包括堆、方法区、栈、本地方法栈、程序计算器等部分组成的,其中方法回收堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归JVM的。Java能够被称为“一次编译,到处运行”的原因就是Java屏蔽了很多的操作系统平台相关信息,使得Java只需要生成在JVM虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。
二.JVM、JDK、JRE三者的关系
1.JVM
Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语⾔“⼀次编译,随处可以运⾏”的关键所在.
什么是字节码?采⽤字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做 字节码 (即扩展名为 .class 的⽂件),它不⾯向任
何特定的处理器,只⾯向虚拟机。Java 语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语
⾔执⾏效率低的问题,同时⼜保留了解释型语⾔可移植的特点。所以 Java 程序运⾏时⽐⾼效,
⽽且,由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系
统的计算机上运⾏。
Java 程序从源代码到运⾏⼀般有下⾯ 3 步:
2.JDK
JDK 是 Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。
3.JRE
JRE 是 Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序。
三.JVM内存区域划分
粗略分来,JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎。
1.类装载器
每一个Java虚拟机都由一个类加载子系统(class loader subsystem)负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个 Java 虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。 JVM 的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是 JVM 实现的一部分,用户自定义装载器则是Java程序的一部分,必须是 ClassLoader 类的子类
2.执行引擎
主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行其中解释属于第一代JVM,即时编译JIT属于第二代JVM,自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式 。
自适应优化:开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
3.运行时数据区
主要包括:方法区,堆,Java栈,PC寄存器,本地方法栈
- 方法区和堆由所有线程共享
堆:存放所有程序在运行时创建的对象
方法区:当JVM的类装载器加载.class文件,并进行解析,把解析的类型信息放入方法区。
-
Java栈和PC寄存器由线程独享
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址
-
本地方法栈:存储本地方法调用的状态
四.JVM运行时数据区
Java 虚拟机在执⾏ Java 程序的过程中会把它管理的内存划分成若⼲个不同的数据区域。JDK. 1.8 和
之前的版本略有不同,下⾯会介绍到。
- JDK 1.8 之前
- JDK 1.8
线程隔离的:
程序计数器
虚拟机栈
本地方法栈
线程共享:
堆
方法区(元数据区)
直接内存(非运行时数据区的一部分)
JVM1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
1.程序计数器
程序计数器是一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器。
Java 虚拟机的多线程是通过线程轮流切换,分配处理器时间的方式来实现的,所以在任何一个确定的时刻,一个处理器(即多处理器的一个内核)都只会执行一条线程中的指令。因此,为了线程切换后,能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间不影响,独立存储,我们称这类内存区域为“线程私有”。
如果线程执行的是Java方法,则记录的是正在执行的虚拟机字节码指令的地址。如果是Native 本地方法,计数器值为空(Undefined)
从上面的介绍中我们知道程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建
⽽创建,随着线程的结束⽽死亡。是唯一一个在《java虚拟机规范》中没有规定任何 OOM 情况的区域。
2.Java 虚拟机栈
线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法(也就是字节码)在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于保存 局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,对应了栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向一条字节码指令的地址)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若Java虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度
超过当前Java虚拟机栈的最⼤深度的时候,就抛出StackOverFlowError异常。 - OutOfMemoryError: 若 Java 虚拟机栈的内存⼤⼩允许动态扩展,且当线程请求栈时内存⽤完
了,⽆法再动态扩展了,此时抛出OutOfMemoryError异常。
Java 方法有两种返回方式:(1)return 语句;(2)抛出异常,不管哪种返回方式都会导致栈帧被弹出。
3.本地方法栈
功能与Java虚拟机栈十分相同。区别在于,虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服
务,而本地方法栈为虚拟机使用到的native方法服务。