Java 虚拟机中的运行时数据区分析
本文基于 JDK1.8 阐述分析
运行过程
我们都知道 Java 源文件通过编译器编译后,能产生相应的 .Class 文件,也就是字节码文件。而字节码文件通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。
跨平台的特性
Java 能跨平台的原因是因为:不同的平台有不同的 JVM 版本,一个 Java 源文件被编译成字节码文件,被不同平台的 JVM 翻译成特定平台下的机器码从而运行。
Java 虚拟机组成
Java 虚拟机由三个子系统构成,分别是类加载子系统、JVM 运行时数据区和执行引擎,本文的重点是在 JVM 运行时数据区。
类加载子系统将硬盘上的字节码文件加载进内存,JVM 运行内存有一套自己的结构划分如图所示,最终程序要运行,需要操作系统分配相应的时间调度,由执行引擎去执行,才能得到最终结果。
线程共享数据:允许被所有线程共享访问的一块内存区域。
线程私有数据:本线程私有的一块内存区域
虚拟机栈(JVM Stacks)
Java 虚拟机栈是线程私有的,它的生命周期与线程相同,线程启动而产生,线程结束而消亡。
Java 虚拟机栈是描述 Java 方法执行的内存模型,用于存储栈帧。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
除了 native 方法,几乎所有的 Java 方法都是通虚拟机栈来实现方法的调用和执行(需要程序计数器、堆、方法区的配合)。
栈帧(Stack Frame)
- 每个方法执行的同时会创建一个栈帧,它是虚拟机栈的基本元素。
- 一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
- 栈帧随着方法调用而创建,随着方法结束而销毁。
- 每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。
局部变量表(Local Variable Table)
- 一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
- 该方法所需要分配的局部变量表的最大容量在将 Java 编译为 Class 文件时已经确定。
- 一个局部变量表保存的是编译期可知的各种基本数据类型、对象引用和 returnAddress 类型(它指向了一条字节码指令的地址)。
- 局部变量表的容量以变量槽为最小单位,每个变量槽可以存储32位长度的内存空间。对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的变量糙空间。
- 局部变量表所需的内存空间在编译期间就能完成分配,在运行期间不会改变其大小。
- 虚拟机通过索引定位的方法查找相应的局部变量
操作数栈(Operand Stack)
- 虚拟机栈中的一个用于计算的临时数据存储区。
- 随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
动态链接(Dynamic Linking)
- 在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于运行时常量池。
- 每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用。
- 这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
方法返回
- 一个方法开始执行后,只有两种退出方式:正常完成出口和异常完成出口
- 正常完成出口指方法正常完成并退出,根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。
- 异常完成出口指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
- 无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,方法返回时可能需要在栈帧中保存一些信息。
- 一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址要通过异常处理器表来确定,栈帧中一般不保存这部分信息。
附加信息
- 虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信息,这部分信息完全取决于不同的虚拟机实现。
- 在实际开发中,一般会把动态连接,方法返回地址与其他附加信息一起归为一类,称为栈帧信息。
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是线程私有的
JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,在同一时刻一个处理器内核只会执行一条线程,处理器切换线程时并不会记录上一个线程执行到哪个位置,所以为了线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
JVM 规范中唯一没有规定 OutOfMemoryError 情况的区域
程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。
执行 Native 方法时计数器值为空
当执行 Java 方法时,程序计数器存放 Java 字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做 bytecode index(简称 bci)。另一种是该 Java 字节码指令在内存的地址,叫做 bytecode pointer(简称 bcp)。
Native 方法大多通过 C 实现,它的方法体不是由 Java 字节码构成,无法应用上述 Java 字节码地址的概念,也就不需要存储字节码文件的行号。
Native 方法的实际执行
Java 线程总是需要以某种形式映射到 OS 线程上,HotSpot VM 目前在大多数平台上都使用 1:1 模型(原生线程模型),也就是每个 Java 线程直接映射到一个 OS 线程上执行。此时 native 方法由原生平台直接执行。
本地方法栈(Native Method Stacks)
本地方法栈为虚拟机使用到的 Native 方法服务。Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用 C/C++ 方法。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java 程序调用本地方法
不同于虚拟机栈的入/出栈,当线程调用 native 方法时,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
本地方法接口回调 JVM 中的 Java 方法
如果某个虚拟机实现的本地方法接口是使用 C 连接模型的话,那个他的本地方法栈就是 C 栈,当一个 C 函数调用另一个 C 函数时,它的栈操作是确定的。如果本地方法接口需要回调JVM 中的 Java 方法,该线程会保存本地方法栈的状态并进入到另一个Java栈。
不同虚拟机的不同实现
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。常用的 HotSpot 虚拟机选择合并了虚拟机栈和本地方法栈。
堆(Heap)
堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
分代概念
- JVM 中堆空间由新生代和老年代两个区组成
- 新生代可以划分为三个区,Eden 区,两个 Survivor 区
- Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
- JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
常用参数配置
参数 说明 -Xms 堆内存初始大小 -Xmx 堆内存最大允许大小 -Xss 每个线程的 Stack 大小 -XX:NewSize(-Xns) 新生代初始大小 -XX:MaxNewSize(-Xmn) 新生代最大允许大小 -XX:NewRatio 设置新生代与老年代比值 -XX:SurvivorRatio 设置 Survivor 与 Eden 比值 -XX:PermSize 设置持久代初始内存大小(JDK8 以前) -XX:MaxPermSize 设置持久代最大内存(JDK8 以前) -XX:MetaspaceSize 设置元空间初始内存大小(JDK8 以后) -XX:MaxMetaspaceSize 设置元空间最大内存(JDK8 以后) 堆 GC
在堆中分配的内存,由 JVM 自动垃圾回收器来管理。关于 GC 详情,之后再补充。
方法区(Method Area)
方法区是一种规范,不同的虚拟机的实现也不一样。从 JDK 1.8 开始,元空间(Metaspace)取代了永久代(PermGen)成为 HotSpot VM 对方法区的实现。方法区存储加载进来的每一个类的结构信息,可以看做是将类(Class)的模板信息,保存在方法区里
元空间属于本地内存
JDK8 以前,永久代是堆的一部分,和新生代、老年代的地址是连续的。JDK8 以后,元空间属于本地内存,不再属于堆的一部分,它还有一个别名叫非堆(Non-Heap),所以元空间不存在 OOM 内存溢出的情况。
方法区是线程共享的
当多个线程用到同一个类,而这个类还未被加载,则应该只有一个线程去加载类,其他线程等待。
Java 虚拟机中的运行时数据区分析的更多相关文章
- Java内存管理:Java内存区域 JVM运行时数据区
转自:https://blog.csdn.net/tjiyu/article/details/53915869 下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些, ...
- 介绍下Java内存区域(运行时数据区)
介绍下Java内存区域(运行时数据区) Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域.JDK 1.8 和之前的版本略有不同. 下图是 JDK 1.8 对JV ...
- Java虚拟机一:运行时数据区域
java虚拟机在执行java程序的过程中,会把内存划分为若干个不同的数据区域.每个区域都有各自的用途,创建和销毁时间,按照<java虚拟机规范(Java SE 7 版)>的规定,虚拟机运行 ...
- 深入理解Java虚拟机一:运行时数据区域
根据<Java虚拟机规范(第2版)>的规定,Java虚拟机管理的内存包括下图几个运行时数据区域: 1.程序计数器 程序计数器(Program Counter Register ...
- 虚拟机系列 | JVM运行时数据区
本文源码:GitHub·点这里 || GitEE·点这里 一.内存与线程 1.内存结构 内存是计算机的重要部件之一,它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱 ...
- 你必须了解的java内存管理机制(一)-运行时数据区
前言 本打算花一篇文章来聊聊JVM内存管理机制,结果发现越扯越多,于是分了四遍文章(文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8),本文为其中第一篇.from 你必须了解的java内存 ...
- jvm入门及理解(三)——运行时数据区(程序计数器+本地方法栈)
一.内存与线程 内存: 内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了JAVA在运行过程中内存申请.分配.管理的策略,保证了JVM的 ...
- JVM 专题九:运行时数据区(四)本地方法栈
1. 本地方法栈 2. 什么是本地方法栈? Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用 本地方法栈,也是线程私有的. 允许被实现成固定或者是可动态拓展的内存大小 ...
- JVM运行时数据区--本地方法栈
本地方法栈 1.Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法(一般非Java实现的方法)的调用 2.本地方法栈,也是线程私有的. 3.允许被实现成固定或者是可动态拓展的内存 ...
随机推荐
- mongodb服务器启动
以配置文件启动服务器: mongod --config /usr/local/mongodata/config/mongodb.conf(配置文件路径) 客户端启动: mango 关闭mongodb的 ...
- 宝塔phpmyadmin可能问题及解决方法
1. 端口问题检查宝塔phpmyadmin的默认端口888是否放行,和在服务器的安全组规则有没有添加888端口 2.phpmyadmin的php版本问题 在phpmyadmin的设置里的版本选择php ...
- Python学习笔记:函数和变量详解
一.面向对象:将客观世界的事物抽象成计算机中的数据结构 类:用class定义,这是当前编程的重点范式,以后会单独介绍. 二.函数编程:逻辑结构化和过程化的一种编程方法 1.函数-->用def定义 ...
- 学习方法,学习方式By:ラピスラズリ(Dawn)20200407
原创,转载请注明,谢谢!
- HTML特殊转义字符——特殊符号
干货,见下图: 后期我会陆续更一些JavaScript的文章,大家可以一起学习交流.
- 金三银四科学找工作,用python大数据分析一线城市1000多份岗位招聘需求
文章每周持续更新,各位的「三连」是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) 每年的三四月份是招聘高峰,也常被大家称为金三银四黄金招聘期,这时候上一 ...
- python学习笔记--字符串格式化
字符串和常量 print(r'hello\py\thon') r 代表后面字符不进行转义,原样输出; 表示常量,命名时变量名字大写代表常量.NAME = 'liulixue'; 字符串表示:' ', ...
- 《Java基础复习》—常识与入门
突然发现自己Java基础的底子不到位,复习! 所记知识会发布在CSDN与博客网站jirath.cn <Java基础复习>-常识与入门 一.Java语言的知识体系图 分为三部分 编程语言核心 ...
- 部署zookeepe高可用集群
部署zookeepe高可用集群 部署规划 Nno1 192.16 ...
- 给定一个整数数组 nums 和一个目标值 target,求nums和为target的两个数的下表
这个是来自力扣上的一道c++算法题目: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标. 你可以假设每种输入只会对应一个答案 ...