JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作
特点:
- Java 虚拟机基于二进制字节码执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成
- JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的跨平台性
Java 代码执行流程:Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)
下面介绍JVM的内存结构:
1.内存概述
内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区
JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行
Java1.8 以前的内存结构图:
(图片来源:https://github.com/Seazean/JavaNote)
Java1.8 之后的内存结果图:
(图片来源:https://github.com/Seazean/JavaNote)
2.JVM内存
2.1 *虚拟机栈*
2.1.1 Java 栈
Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存
(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)
虚拟机栈的运行过程:
- 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧)
- Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的
- 虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:
- 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
- 动态链接:也叫指向运行时常量池的方法引用
- 方法返回地址:方法正常退出或者异常退出的定义
- 操作数栈或表达式栈和其他一些附加信息
虚拟机栈特点:
- 栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据
- 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
- 方法内的局部变量是否线程安全:
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
设置栈内存大小:-Xss size
-Xss 1024k
2.1.2 局部变量
局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
- 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
- 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
- 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
- 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收
局部变量表最基本的存储单元是 slot(变量槽):
- 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
- 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
- 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
- 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的
2.1.3 操作数栈
操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
- 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区
- Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中
栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率
基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术
2.1.4 动态链接
动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定
- 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用
- 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中,常量池的作用:提供一些符号和常量,便于指令的识别
(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)
2.1.5 返回地址
Return Address:存放调用该方法的 PC 寄存器的值
方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置
- 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
- 异常:返回地址是要通过异常表来确定
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者
异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出
两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值
2.1.6 附加信息
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息
2.2 本地方法栈
本地方法栈是为虚拟机执行本地方法时提供服务的
JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植
- 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
- 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
- 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
- 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 直接从本地内存的堆中分配任意数量的内存
- 可以直接使用本地处理器中的寄存器
原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数
- dlopen 函数:Linux 系统加载和链接共享库
- dlclose 函数:卸载共享库
**
**
2.3 程序计数器
Program Counter Register 程序计数器(使用寄存器来存储地址)
作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)
简单来讲就是记住下一条jvm指令的执行地址
原理:
- JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
- 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号
特点:
- 是线程私有的(每个线程都有自己的程序计数器)
- 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC
Java 反编译指令:javap -v Test.class
#20:代表去 Constant pool 查看该地址的指令
0: getstatic #20 // PrintStream out = System.out;3: astore_1 // --4: aload_1 // out.println(1);5: iconst_1 // --6: invokevirtual #26 // --9: aload_1 // out.println(2);10: iconst_2 // --11: invokevirtual #26 // --
2.4 堆
Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题
存放哪些资源:
- 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
- 字符串常量池:
- 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
- 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
- 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
- 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
设置堆内存指令:-Xmx Size
内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
堆内存诊断工具:(控制台命令)
- jps:查看当前系统中有哪些 Java 进程
- jmap:查看堆内存占用情况
jhsdb jmap --heap --pid 进程id
- jconsole:图形界面的,多功能的监测工具,可以连续监测
在 Java7 中堆内会存在年轻代、老年代和方法区(永久代):
- Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
- Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
- Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理
分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能
public static void main(String[] args) { // 返回Java虚拟机中的堆内存总量 long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; // 返回Java虚拟机使用的最大堆内存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M}
2.5 方法区
方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式
方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池
运行时常量池是方法区的一部分
- 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
- 类在解析阶段将这些符号引用替换成直接引用
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
注:jdk1.8之后方法区(元空间)使用的内存是物理内存
(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)
本篇文章到这里就结束了,最后送大家一句话 白驹过隙,沧海桑田