概述
本篇文章是根据学习《深入理解Java虚拟机》书籍及其聆听尚硅谷宋红康老师讲解 ,最终自己按照自己的理解总结而出
图片引用: https://imlql.cn/post/a7ad3cab.html && 尚硅谷教育
对于c++选手来说, 内存管理是一项基本功,因为c++没有自带的管理技术, 所以c++开发人员需要自己对实现的所有代码进行内存管理。 虽然说Java实现了一套自己的内存管理机制, 这让Java程序员可以全心投入到需求开发中去, 不需要对内存做太多了的了解。 但是问题也正是出现在这里, 因为不知道虚拟机是怎么使用内存的,所以出了问题也是无从下手,不知道具体哪里出了问题。 所以这些都是我们Java程序员需要了解和掌握的内存管理技术的原因。
之前我们学习了类加载器子系统的知识 ,知道了一个Class文件是如何一步步被加载到虚拟机内存中的开始 ,到卸载出内存位为止, 他的整个生命周期以及每个周期所做的事等等….
既然一个类已经被加载到内存了 ,那么下一步就是查看内存如何管理这些了
运行时数据区(Runtime Data Area)
当我们通过前面的:类的加载 –> 验证 –> 准备 –> 解析 –> 初始化,这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区
虚拟机在执行Java文件的时候会把他所管理的内存划分为若干个不同的数据区域, 这些区域有各自的用途 , 会随着虚拟机进程的启动而创建 或者是 随着用户线程的启动和结束而建立和销毁。
我们通过磁盘或者网络IO得到的数据,都需要先加载到内存中,然后CPU从内存中获取数据进行读取,也就是说内存充当了CPU和磁盘之间的桥梁
最新的JDK 8中对之前的运行时数据区相关内容有着不同的划分, 具体参考阿里的JDK 8 运行时数据区图
前面我们提到这些区域有各自的用途 , 会随着虚拟机进程的启动而创建 或者是 随着用户线程的启动和结束而建立和销毁。 随着虚拟机进程这点我可以理解,因为内存区域都是由Java虚拟机管理的 但是为什么会随着用户的线程呢?
线程的相关内容
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行 。 在JVM内部, 每个线程都是与本地的线程直接映射
在我们Java的程序启动一个线程时, 操作系统同时也会创建一个本地线程,本地线程一旦初始化成功就会开始执行run()方法
一些JVM系统线程
虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括”stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持
编译线程:这种线程在运行时会将字节码编译成到本地代码
信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
以上的5个线程不包括main方法所在的线程以及通过main方法创建的线程, 这些都是Java程序启动之后 ,后台启动的一些线程。
程序计数器(Program Counter Register)
概述: 程序计数器占用的是一块很小的空间, 它相当于当前线程所执行的字节码的行号指示器。 可以理解为和计算机组成原理中的程序计数器一个概念(但是具体的执行功能还是不同的,知识有大致的相同), 都是通过这个计数器的值来选取下一条需要执行字节码指令。 同时程序计数器的生命周期正在执行的线程的生命周期相同
作用:
JVM中 程序计数器用于追踪线程执行的位置。它保存了当前线程正在执行的字节码指令的地址或索引。当线程被调度执行时,程序计数器指示了下一条要执行的指令的位置。在线程切换时,程序计数器的值会被保存和恢复,以确保线程能够从正确的位置继续执行。
程序计数器也是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)。
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
程序计数器区域 是唯一一个在《Java虚拟机规范》中没有规定任何OneOfMemoryError情况的区域
Java虚拟机栈(Java Virtual Machine Stack)
与上面的程序计数器一样, 都是线程私有的 ,随着线程的创建和销毁 而 生 & 死 。
虚拟机栈描述的是Java方法执行的线程内存模型
每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
内存中的栈与堆区分
首先栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里
因为按照c++中的内存布局结构, 人们好像都会将其划分为栈内存 和 堆内存, 但是实际的内存布局结构却比这更加复杂。
虚拟机栈的作用:
主管Java程序的运行,它保存方法的局部变量(8 种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
局部变量,它是相比于成员变量来说的(或属性)
基本数据类型变量 VS 引用类型变量(类、数组、接口)
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
虚拟机栈的特点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM直接对Java栈的操作只有两个:
每个方法执行,伴随着进栈(入栈、压栈)
执行结束后的出栈工作
对于栈来说不存在垃圾回收问题
栈不需要GC,但是可能存在OOM
栈运行原理
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出(后进先出)原则
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
注意:
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
异常情况(两类)
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError 异常。
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈所发挥的作用是非常相似的,区别就是虚拟机栈为虚拟机执行Java方法服务, 而本地方法栈则是为虚拟机使用本地方法时提供服务。
《Java虚拟机规范》对本地方法栈中使用的任何内容都没有规范。 抛出的异常和Java虚拟机中的异常是一样的。
在栈深度溢出或者栈扩展失败时分别抛出StackoverflowError 异常 和 OutofMemoryError 异常。
Java堆(Java Heap)
**Java堆是虚拟机所管理的内存中最大的一块, 被所有线程所共享, 生命周期是随着虚拟机的, 此内存的唯一目的就是存放对象实例的。 **
上述就是堆区的重点。Java中, 几乎所有的对象示例都会在这里分配内存。 (但是在《Java虚拟机规范》中 它表明 所有的对象示例以及数组都应该在堆上分配), 具体听谁的咱也不知道…
相关细节:
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,堆是JVM管理的最大一块内存空间,并且堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
从实际使用角度看:“几乎”所有的对象实例都在堆分配内存,但并非全部。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
也就是触发了GC的时候,才会进行回收
如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。