JVM内存模型

内存简介

​ 程序运行过程中需要不断将逻辑地址和物理地址进行映射,找到相关指令以及数据去执行,作为操作系统进程,java运行时面临着与其他进程相同的内存性质,受限于操作系统架构提供的可寻址地址空间。操作系统架构的可寻址地址空间由处理器位数决定,32位处理器支持232(4GB)位的可寻址范围,64位处理器支持264位的可寻址范围。

地址间的划分

内核空间

​ 主要的操作系统程序和c运行的空间,包含连接计算机硬件,调度程序以及提供联网,虚拟内存等服务的逻辑以及基于c的进程。

用户空间

JVM架构图 - JDK8

java程序运行在虚拟机之上,运行时需要内存空间。虚拟机执行java的过程中会把它管理的内存划分成不同数据区方便管理。C编译器将管理内存区域分为数据段,代码段,数据段包括堆,栈,静态数据区。对于java的内存模型可以以线程和存储的角度来分析。

线程角度 (线程私有)

程序计数器(Program Counter Register)

  • 当前线程执行字节码行号指示器(逻辑)
  • 改变计数器的值来选取下一条需要执行的字节码的指令
  • 和线程是一对一的关系,即线程私有。(JVM中的多线程是通过线程轮流切换并分配处理器处理时间的方式来实现,在任何一个确定的时刻一个处理器只会执行线程中的一条指令,为了线程切换后能恢复到正确的执行位置,每条线程需要一个独立的计数器,各条线程之间程序计数器独立存储,互不影响。)
  • 对java方法计数,如果是Native方法则计数器值为Undefined。
  • 不会发生内存泄漏。

分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

java虚拟机栈(Stack)

  • java方法执行的内存模型
  • 包含多个栈帧

java虚拟机栈也是线程私有,每个方法执行都会创建一个栈帧(方法运行期间的基础数据结构),栈帧用于存储局部变量表,操作栈,动态链接,方法出口等每个方法执行中对应虚拟机栈帧从入栈到出栈的过程。java虚拟机栈用来存储栈帧,当方法调用结束时,帧才会被销毁。虚拟机栈包含了单个线程每个方法执行的栈帧,栈帧则存储了栈帧用于存储局部变量表,操作栈,动态链接等信息。

局部变量表和操作数栈
  • 局部变量表:包含方法执行中的所有变量

    • 局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量
  • 操作数栈:入栈、出栈、复制、交换、产生消费变量
    * 操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。

    • 大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作
  • 栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。

操作数栈和局部变量数组的交换变量指令操作通过字节码频繁执行。

局部变量为操作数栈提供必要的数据支撑。

递归为什么会引发StackOverflowError异常?

当虚拟机线程执行一个方法时会随之创建一个对应的栈帧,并将建立的栈帧压入虚拟机栈中,方法执行完毕的时候会将栈帧出栈,线程当前执行的方法栈帧必定位于java虚拟机栈的顶部。递归不断调用自身方法,每次调用会产生新的一个栈帧,并保留当前栈帧状态,栈帧上下文切换会切换到最新的方法栈帧当中,虚拟机栈栈帧深度固定,递归过深,栈帧超出虚拟机栈深度。解决思路:限制递归的次数或者采用循环实现。

虚拟机栈过多会引发OutOfMemoryError异常。

当虚拟机栈可以动态扩展时,如果无法申请足够多的内存,就会抛出该异常。

由于Windows平台java虚拟机将线程映射到系统线程内核上,上述代码可能会引起操作系统假死。

虚拟机栈也是由jvm自动管理,有固定容量。

本地方法栈

​ 与虚拟机栈相似,主要作用于标注了native的方法。

线程角度 (线程共享)

元空间(MetaSpace)与永久代(PermGen)的区别

​ 元空间和永久代都是方法区的实现,只是实现方法不同,方法区只是jvm的一种实现规范。在java7之后,原先位于方法区里的字符串常量池已被移到了java堆中,在java8之后使用元空间替代了永久代。

​ 元空间使用本地内存,而永久代使用jvm的内存。

​ MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出。
  • 类和方法的信息难以确定,给永久代的大小指定带来困难
  • 永久代会为GC带来不必要的复杂度
  • 方便HotSpot与其他JVM如Jrockit的集成

​ 只需记住metaspace使用内存空间是本机内存,metaspace没有了字符串常量池,在JDK7之后移动到了堆中,metaspace其他存储的内容(如类文件,在jvm运行时数据结构,class相关的method)待遇上与permGen一样,只是划分上更加合理了。比如类及相关的元数据生命周期与类加载器一致,每个类加载器都有单独的存储空间。

java堆(heap)

java堆是jvm管理中最大的一块内存区域,此内存用于存储java对象实例。

  • 对象实例的分配区域

    图为32位处理器的java进程的内存布局,从图中可以看到可寻址的内存空间为4GB,OS和C运行大概占用了1GB,JVM本身也要占用内存(像OS内核和C一样),java堆占用了近2GB,本机堆和metaspace则占用了其他部分。java堆可处于不连续的物理空间内存上,只需逻辑连续即可,在实现时既可设定成固定大小的,也可设定成可扩展的。主流jvm都是按可扩展来实现的。

  • GC管理的主要内存区域

JVM三大性能调优参数-Xms -Xmx -Xss的含义
  • Xss规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k足够了,此配置会影响并发线程数大小)

  • Xms 堆的初始值

  • Xmx 堆能达到的最大值

    一但对象超出了java堆的初始容量,java堆会自动扩容, 我们一般把Xms和Xmx设置成一样,因为堆内存扩容时会发生内存抖动

java内存模型中堆内存分配策略
  • 静态存储:编译时确定每个数据目标在运行时的存储空间要求(要求程序不能有可变数据结构,递归,嵌套)
  • 栈式存储:数据区需求在编译时未知,运行时模块入口确定
  • 堆式存储:编译时或运行时模块入口都无法确定, 动态分配
java内存模型中堆和栈的联系

​ 创建好的对象实例和数组都保存在堆中,想引用堆中的对象或数组可以在栈中定义一个特殊的变量 ,让该变量的取值等于堆中目标的首地址,栈中的这个变量就成了数组或者对象实例的引用变量,之后在程序中就可以使用栈中的引用变量来访问堆中的数组或者对象。引用变量就像是对数组或实例对象取得一个名称。引用变量在栈中分配,引用变量在程序运行到其作用域外之后会被释放。对象和数组在堆中分配,就算程序运行到new语句之外也不会被释放,直到没有引用变量指向的时候成为垃圾,等待垃圾回收器回收释放。

  • 引用对象、数组时,栈里定义变量保存对目标中的首地址。
java内存模型中堆内存分配策略
  • 管理方式:栈自动释放,堆需要GC(JVM可以直接对内存栈进行管理,该内存空间的释放是编译器可以操作的内容,jvm执行引擎不会对堆内存进行操作,还是让GC系统自动回收)

  • 空间大小: 栈比堆空间小

  • 碎片相关:栈产生的碎片远小于堆

  • 分配方式:栈支持静态分配和动态分配,堆只支持动态分配

  • 效率:栈的效率比堆高

元空间,线程独占部分,堆的联系——内存角度

常见考题

posted @ 2019-10-30 20:40  vipzk  阅读(74)  评论(0)    收藏  举报