JVM那些事儿之内存区域
相信绝大多数java开发者或多或少的都应该知道jvm,但是有多少人又深入去了解过,笔者深感自身能力的不足,去看了些资料,觉得还是有必要整理下自己的学习记录,时常回头看看,多看多实践提升自己的能力,故开始进行jvm相关的知识梳理和记录,一起来学习吧
前言
从我们刚开始学习java时,我们就被告知其“一次编写,到处运行”的特点,随着学习和工作的深入,你也应该了解了构成这种特点的原因,说到这里,有点像中间件的思想,有人说过,没什么东西是不能通过添加一个中间件来完成的,如果一个不行,那就两个。
由于jvm的存在,使得java语言可以不去关注系统区别而进行同一套代码的开发,借助class字节码文件在jvm上运行,只需要jvm关注底层系统即可,有没有点像中间件的思想,将底层处理相关部分抽离出来构成jvm,而开发者无需关注底层实现,根据自己的需要进行自己的开发即可
但是同时由于jvm的存在屏蔽了许多的底层细节,方便了开发人员,但是其缺点也是很明显的,在我们需要对程序进行优化,比如内存,CPU,并发量,IO等进行必要的处理时,就有点懵逼,你是不是经历过下面的场景:
- 程序内存占用满了,导致程序经常宕机需要重启,不知道从哪里查问题,怎么优化?
- 程序偶尔莫名其妙的卡顿,代码检查了一遍又一遍,没问题,不知道接下来怎么搞了?
- 服务器上资源紧张,需要优化配置,怎么做?
- ...
其他更加离奇的现象就不列举了,很多问题都要求我们需要对jvm有所了解,知道查找问题的大概方向,这也是高级开发者和初级开发者的不同之处吧,其实就像现在很多人使用了很多框架一样,很多人都会用,但是一出问题就懵逼,所以我一直觉得基础是最重要的,无论什么框架都需要建立在基础之上,万丈高楼平地起,底层基础的东西才是需要重点掌握的核心
JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域
概念上我们也能看出其是虚构出来的计算机,jvm运行过程简单来说主要包含:
- 创建jvm环境
- 加载class文件
- 管理分配内存
- 垃圾回收
运行时数据区域
jvm的自动内存管理机制似乎使得java开发者不需要关心内存泄漏和溢出问题,但是如果出现了问题,开发者不了解jvm是怎么使用内存的,就无法进行问题的排查,这就是简便性带来的副作用,像c开发者需要对每个对象进行内存管理,非常清楚其整个生命周期,要相对容易进行问题的排查,故我们需要先进行了解运行时数据区域
jvm会在程序运行过程中将内存划分为不同的数据区域,每个区域有各自的用途,同时会区分线程共享的数据区和线程隔离的数据区
![关系图](https://www.gclearning.cn/articlesimages/JVM/neicunquyu/neicunquyu-1.png)
程序计数器
程序计数器(Program Counter Register),也有称作为PC寄存器。在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示执行哪条指令的。由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是程序当前执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
偏向操作系统底层,其中部分说明下,程序计数器保存了线程需要执行的指令地址,如果你了解过多线程,应该明白系统底层是如何运行的,单个cpu并发执行任务,会进行线程切换获取执行时间,那么必须保证切换回来的时候恢复到线程之前的程序执行位置上,而这个程序计数器就是做这个的,故其也是线程隔离的数据,因为每个线程不一样,多理解理解,简单总结为:
- 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
- 字节码解释器通过计数器来选取下一条需要执行的字节码指令
- 为了线程切换能恢复正确执行位置需要独立的程序计数器,即每个线程都需要各自的计数器,所以程序计数器是线程私有的内存
- 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的
虚拟机栈
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。事实上,Java栈是Java方法执行的内存模型。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
局部变量表就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的,简单总结:
- 虚拟机栈是线程私有的
- Java栈是Java方法执行的内存模型
- 方法执行会创建栈帧,栈帧中保存了很多信息,其中需要关注的是局部变量表
本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
如果你有稍微去了解jdk源码,就会找到许多native方法调用,这部分就是使用本地方法栈完成操作的,比如访问操作系统底层信息,只使用java语言是没办法做到的,就需要借助native来访问,当然这部分数据区域同样是线程私有的
方法区
方法区在JVM中是一个非常重要的区域,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等数据。
很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。在JVM规范中,没有强制要求方法区必须实现垃圾回收。
在方法区中有一个非常重要的部分就是运行时常量池,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的字面量和符号引用,这部分内容(也可以称为.Class文件中的静态常量池)将在类加载后进入方法区的运行时常量池中存放。除了保存Class文件中描述的符号引用外,还会把编译出来的直接引用也存储在运行时常量池中。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
JDK 1.8 中,已经没有方法区(永久代),而是将方法区直接放在一个与堆不相连的本地内存区域(Native Memory),这个区域被叫做元空间。
- 方法区是线程共享的区域
- 方法区保存了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等数据
- 运行时常量池保存了编译期生成的字面量和符号引用
字面量是一种直接表示,,比如int a = 1 这里的1就是字面量,而符号引用则是因为在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替,解析时会进行替换成直接引用,可以看看class文件的反编译源码,有很多符号引用
堆
java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。
- Java堆是被所有线程共享的内存区域
- 存储对象本身的以及数组
- 也被称为GC堆,涉及到的GC算法以后再详述
总结
本文主要是简单介绍jvm运行时数据区域,我们应该明白的是每个区域的存储的是什么,每个区域的作用,是否是线程共享的,是否会发生溢出等,总结如下:
![关系图](https://www.gclearning.cn/articlesimages/JVM/neicunquyu/neicunquyu-2.png)
以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢
参考资料:
- 深入理解Java虚拟机(第2版)(作者: 周志明 出版社: 机械工业出版社 副标题: JVM高级特性与最佳实践)