- JVM内存分区
- 线程共享和线程隔离
- 程序计数器
- 虚拟机栈(Java方法栈)
- 栈帧
- 局部变量表(Local Variable)
- *** 作数栈(Operand Stack)
- 动态连接(Dynamic linking)
- 返回地址(Return Address)
- 本地方法栈
- 方法区
- 方法区概念
- 方法区-类型信息
- 常量池(Constant Pool)
- 运行时常量池
- 堆
内存管理是JVM中的重要命题,当JVM对内存进行管理后降低了开发者的门槛,提高了程序的可维护性。
注:内存区域和内存模型并不是一个概念,内存模型传送门。 JVM内存分区
*** 作系统和JVM都进行了内存区域的划分,将连续的内存,抽象为不同作用的内存区域。Java的内存划分存在于更加上层的用户空间内的封装。JVM将内存划分为五个部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
线程共享和线程隔离线程共享:被所有线程共享的内存区域。包括 堆和方法区。
线程隔离:仅被当前线程独占的内存区域。包括 程序计数器、虚拟机栈、本地方法栈。
当编程时,需要判断当前读写的数据是存在于哪类内存的,若是存在共享内存区域,则需要考虑是否存在线程安全问题。若是线程隔离区域则不需考虑。
硬件层面:在硬件层面程序计数器是一种寄存器,用来存储指令地址,提供给处理器执行。
软件层面:在JVM软件层面程序计数器作用一样,用来存储字节码的指令地址,提供给执行引擎去取址执行。
这两种程序计数器分别存在于硬件和软件中,实现方式不一样,但设计思想类似。
程序执行的过程对应着方法的调用,而方法的调用对应着栈帧的入栈出栈 *** 作。
栈帧栈帧中主要存在四种结构:局部变量表、 *** 作数栈、动态连接、返回地址。
局部变量表(Local Variable)栈帧通过方法源码来生成的,当调用该方法时,传入方法的参数类型、 局部变量的类型在源码中都是已经确定的。既然数量与类型都可以确定那么需要占用的存储空间也能确定。那么该如何进行存储呢,在 局部变量表中通过4字节的slot来存储。
OS层面: *** 作数是计算机指令的一部分。
栈帧 *** 作数栈:JVM用来存储 *** 作数的栈,这个 *** 作数大部分是方法内的变量。 *** 作数栈一可以存储 *** 作数(变量以及中间结果),二能够方便指令顺序读取 *** 作数。虚拟机执行引擎在执行字节码指令的时候,会通过当前指令类型在 *** 作数栈中取出栈顶 *** 作数进行计算,然后将计算结果入栈,继续执行后续指令。
多个栈帧执行如下图:
OOP的主要特性是多态,而Java中的多态就是通过栈帧中的动态连接实现的。
概念:每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic linking)。
Java类加载过程步骤“连接”中,JVM会将class对象(存储在方法区的运行时常量池中)中的部分符号引用(静态解析),替换为直接引用。 有些方法由于多态的存在,JVM无法在类加载阶段确定被调用的具体类型,只能在运行时,真正产生调用时,根据实际的类型进行连接,即动态连接。
返回地址(Return Address)当出现以下两种情况,当前方法就会返回:
- 正常情况:方法正常执行完成,然后返回。若方法B正常返回就代表栈帧B执行完成,此时调用栈帧A,由于栈帧B是被栈帧A调用的,栈帧B在退出虚拟机时需要把返回信息共享给栈帧A的 *** 作数栈,如上图。同时需要修改程序计数器的值,让程序能够继续执行下去。
- 异常情况:方法执行期间遇到异常情况,所以返回。若方法B异常返回,需要通过额外的异常处理器表来进行处理,其他和程序计数器相关的逻辑和正常情况类似。
public void funA(){ funcB(); } public void funB(){}本地方法栈
本地方法:指的是非Java语言实现的函数,往往是由c/c++编写,和 *** 作系统相关性较强的底层函数(比如CAS)。
本地方法栈:就是为了支持本地方法的调用逻辑。
方法区是虚拟机规范中的抽象概念。其中规定了虚拟机必须有方法区这样的机构,但具体的实现可以灵活发挥。
具体实现:比如HotSpot虚拟机,在JDK8以前HotSpot的开发者将面向堆的分代设计复用到了方法区上,使用永久代来作为HotSpot上的方法区的实现,但是后来发现这种设计并不好,所以从JDK8开始借鉴了JRockit的设计思路,使用了元空间来代替永久代作为新的实现方式。
方法区是抽象概念,永久代/元空间是具体实现。
类型信息:类加载的加载阶段,虚拟机会读取被编译后的class文件生成class对象,class对象存储了一些类型信息,这些信息就是存储在方法区内,这些信息有类的签名、属性、方法等。
常量池(Constant Pool)作用:大部分类之间都存在相互调用关系,通过指针(即类加载中的符号引用)实现,在类A中存放类B的指针(符号引用)起到链接作用。
在虚拟机加载字节码文件的时候,首先加载的就是一些静态的符号引用,然后再类加载连接阶段(程序运行时),将符号引用转化为直接引用。符号引用本身是从字节码中加载进来的,那么字节码就是通过常量池(Constant Pool)来存储关系这些符号引用及其他静态引用。
常量池像一张链接表,而一些源码中定义的常量字面量则存储在运行时常量池中。
运行时常量池运行时常量池存储两大类数据:
- 编译期间产生的,主要是字节码中定义的静态信息。比如由字节码生成的class对象 (上文Constant Pool就包含在内)。由字节码生成的字面量,就是我们在编写代码时所定义的常量自变量。
- 运行期间产生的,这部分比较灵活,虚拟机开发者可以将必要的信息都放进去。比如运行时会将一部分的符号引用转化为直接引用,那么这些直接引用就可以存储进来。比如字符串常量池。
堆,从JVM的规范上并没有进行严格意义上的分区,只是从不同角度去看,可以进行不同的逻辑划分。最主流最常见得是从垃圾回收的角度对堆内存进行划分。
学习视频链接:https://www.bilibili.com/video/BV1uQ4y1y7T2?spm_id_from=333.999.0.0
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)