JAVA jvm系列--java内存区域

目录
  • 一、JVM的组成
  • 二、JVM运行流程
  • 三、java内存区域详解(运行时数据区域)
    • (一)程序计数器
    • (二)java虚拟机栈
    • (三)本地方法栈
    • (四)java堆
    • (五)方法区
    • 运行时常量池
    • (六)直接内存
  • 总结

JVM: Java Virtual Machine,Java虚拟机,包括处理器、堆栈 、寄存器等,是用来执行java字节码(二进制的形式)的虚拟计算机。

一、JVM的组成

JVM由以下四部分组成(两个子系统和两个组件):

类加载器(ClassLoader)
执行引擎(Execution Engine)
运行时数据区(Runtime Data Area)
本地库接口(Native Interface)

结构如图:

(1)运行时数据区域我们在本文进行详解;

(2)类加载机制会在后续文章中依次分析,本文主要介绍运行时数据区域;

(3)执行引擎:

JIT编译器:编译执行;将字节码指令变成机器指令。将机器指令放在方法区缓存。

解释器:逐行解释字节码。

垃圾回收器:内存回收的具体实现。

(4)本地方法库:

有时java应用需要与java外面的环境、操作系统交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。

jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。

二、JVM运行流程

(1)程序在执行之前先要把java代码转换成字节码(class文件);

(2)jvm首先需要把字节码通过类加载器(ClassLoader) 把文件加载到 运行时数据区(Runtime Data Area) ;

(3)字节码文件不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行;

(4)第三步过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能。

注:Java 虚拟机与 Java 语言没有什么必然的联系,它只与特定的二进制文件.Class 文件有关 。 因此无论任何语言只要能编译成.Class 文件,就可以被 Java 虚拟机识别并执行,比如Groovy、Kotlin。

三、java内存区域详解(运行时数据区域)

我们说的Java内存区域,一般都指运行时数据区域,其组成如图所示:

JDK1.8之后的内存区域布局如下:

参考文章:Java内存区域(运行时数据区域)和内存模型(JMM)

(一)程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,是当前线程所执行的字节码的行号指示器。——内存空间小
字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。——计数执行

对于一个单核cpu(或者是一个内核)来说,只能同时执行一条指令,而JVM通过快速切换线程执行指令来达到多线程的,真正处理器就能同时处理一条指令,只是这种切换速度很快,我们根本不会感知到。为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。——线程私有,多线程的实现

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。——无内存溢出

(二)java虚拟机栈

线程私有:Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同,与线程同时创建。线程的生命周期请参考我的另一篇文章:线程的生命周期。

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

(1)局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量要显示初始化,没有默认值。
存放了编译期间可知的基本数据类型、对象引用类型(引用指针)和returnAddress类型(程序就是存储在方法区的字节码指令,指向特定指令内存地址的指针)。

32位的数据类型占用一个局部变量空间(Slot),64位的long和double占2个。

在Java程序被编译为Class文件时,就在方法的Code属性(Java程序方法中的代码经过javac编译之后形成字节码存在了Code属性内)的max_locals数据项中确定了方法所需的分配的局部变量表的最大容量。

(2)操作数栈

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

i++ 和 ++i 的区别:
i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i
自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作
栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。

(3)动态链接

每个栈帧中包含一个在运行时常量池中对所在方法的引用, 目的是支持方法调用过程的动态连接。

现有动态链接,再有栈帧。

(1)每一个栈帧当中都包含指向运行时常量池栈帧所属方法的引用(invokedynamic指令);

(2)在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里;
比如:描述一个方法调用的另外的其它方法时,就是通过常量池中指向该方法的符号引用来表示,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

参考:https://www.zhihu.com/question/347395101

知乎上参考到的理解:

比如类里有个a方法,加载到了元空间的内存地址:0x0000 0001号单元 然后运行时常量池里把这个方法的符号引用转换为直接引用: a — 0x0000 0001。

然后调用a方法,创建栈帧,里面保存了常量池里指向a方法的这个直接引用 0x0000 0001。就可以从这个直接引用找到a方法代码的入口执行a方法。

线程切换恢复后也可以根据程序计数器(偏移量)结合这个引用,再次找到a方法在内存中上次执行到的位置,继续执行代码。

什么是符号引用:

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。

(4)方法返回地址

方法出口。

方法执行时有两种退出情况:

正常退出,即正常执行到任何方法的返回字节码指令;
异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

返回值压入上层调用栈帧。
异常信息抛给能够处理的栈帧。
PC计数器指向方法调用后的下一条指令。

(三)本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

(四)java堆

对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。——线程共享

jdk1.8之后,字符串常量池从方法区移到了堆中。

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。

(1)从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

(2)从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。——内存溢出

(五)方法区

作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

回收:垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。

异常:当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配,元空间的大小取决于本地内存的大小。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。(当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

(六)直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。

在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

总结

如图所示:

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java数组与堆栈相关知识总结

    一.数组创建 1.1 声明并赋值 int[] a = {1,2,3}; 1.2 声明数组名开辟空间并且赋值 int[] a; a = new int[]{1,2,3}; 1.3 声明数组时指定元素个数然后赋值 int[] a= new int[3]; 这里Java会默认数组元素值为0 1.4 在以上的基础上创建多维数组 int[][] a = {{1,2,3},{4,5,6},{7,8,9}}; //每个子数组元素个数不要求均相同 int[][] a = new int[m][n]; //其中n

  • 深入JVM剖析Java的线程堆栈

    在这篇文章里我将教会你如何分析JVM的线程堆栈以及如何从堆栈信息中找出问题的根因.在我看来线程堆栈分析技术是Java EE产品支持工程师所必须掌握的一门技术.在线程堆栈中存储的信息,通常远超出你的想象,我们可以在工作中善加利用这些信息. 我的目标是分享我过去十几年来在线程分析中积累的知识和经验.这些知识和经验是在各种版本的JVM以及各厂商的JVM供应商的深入分析中获得的,在这个过程中我也总结出大量的通用问题模板. 那么,准备好了么,现在就把这篇文章加入书签,在后续几周中我会给大家带来这一系列的专

  • java中利用栈实现字符串回文算法

    问题 给定一个由多个a和b组成的字符串数组,字符串中有一个特殊的字符X,位于字符串的正中间,例如(aaaabbbbXabaabbbb),如何判定该字符串是否回文 简单算法 定义两个下标分别指向字符串的头和尾,每次比较两个下标位置的值是否相等,如果不相等,那么输入的 字符串不是回文,如果相等,左边的下表加1,右边的下表减1,重复上述步骤直至两个下标都指向字符串的正中间或者确定字符串不是回文 /** * 判断字符串是否是回文 */ public int isPalindrome(String inp

  • JAVA JVM运行时数据区详解

    目录 一.前言 二.运行时数据区整体概架构 三.程序计数器 四.虚拟机栈 1.栈的特点 2.栈帧的内部结构 3.局部变量表 4.操作数栈 5.动态链接 6.方法返回地址 五.本地方法栈 六.堆 1.设置堆大小的参数 2.对象分配过程 3.堆中的GC 4.内存分配策略 5.什么是TLAB 6.堆是分配对象存储的唯一选择吗? 七.方法区 1.方法区概述 2.设置方法区内存大小 3.如何解决OOM问题? 4.方法区存储什么 5.方法区的演进细节 6.方法区的GC 总结 一.前言 这是JVM系列文章的第

  • Java 实现栈的三种方式

    栈:LIFO(后进先出),自己实现一个栈,要求这个栈具有push().pop()(返回栈顶元素并出栈).peek() (返回栈顶元素不出栈).isEmpty()这些基本的方法. 一.采用数组实现栈 提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用Arrays.copyOf()进行扩容 import java.util.Arrays; /** * 数组实现栈 * @param <T> */ class Mystack1<T> { //实现栈的数组 private Object

  • Java 内存模型(JMM)

    目录 四.Happens-Before 规则 Java 内存模型 一.什么是 Java 内存模型 Java 内存模型定义如下: 内存模型限制的是共享变量,也就是存储在堆内存中的变量,在 Java 语言中,所有的实例变量.静态变量和数组元素都存储在堆内存之中.而方法参数.异常处理参数这些局部变量存储在方法栈帧之中,因此不会在线程之间共享,不会受到内存模型影响,也不存在内存可见性问题. 通常,在线程之间的通讯方式有共享内存和消息传递两种,很明显,Java 采用的是第一种即共享的内存模型,在共享的内存

  • java数据结构基础:栈

    目录 准备工作 编码环节 push方法 pop方法 empty方法 全部代码 总结 准备工作 工具:idea+jdk8 技术要求:java基础语法 编码环节 首先,我们得先确定下来,用什么数据来模拟栈的操作.由于是一个一个的元素放入栈里面,我们可以考虑用数组来实现. 以上是Java官方文档中的栈定义,我们也只需要实现三个方法:判断是否为空.移除栈顶对象.添加元素到栈的尾部 所以我们事先得定义一个数组: Objects[] arr; 数组定义好了之后呢,想想,我们怎么去获取到栈尾部或者栈首的元素呢

  • java括号匹配算法求解(用栈实现)

    如何使用栈来判定括号是否匹配 对于给定的表达式,可以使用栈来实现括号匹配判定,这个算法在编译器中非常重要,解析器每次读入 一个字符,如果字符是一个开分隔符,如(,[,{,入栈,若读入的是闭分隔符),],},出栈,如果两者匹配,继续解析字符串,如果不匹配,解析器错误 算法思路 1.创建一个栈 2.当(当前字符不等于输入的结束字符) (1)如果当前字符不是匹配的字符,判断栈内是否为空,如果栈为空,括号必然不完整 (2)如果字符是一个开分隔符,那么将其入栈 (3)如果字符是一个闭分隔符,,且栈不为空,

  • Java的最大栈深度与JVM核心知识介绍

    目录 一.Java最大支持栈深度有多大? 1.测试案例 1.1.测试线程栈大小对栈深度的影响 1.2.测试方法参数个对栈深度的影响 2.结论 二.重温JVM知识1. JDK,JRE,JVM的联系是啥? 2. JVM的作用是啥? 3.JVM运行时数据区 3.1程序计数器 3.2虚拟机栈 3.3本地方法栈 3.4Java堆 3.5方法区 4.JVM内存模型 一.Java最大支持栈深度有多大? 从Java运行时数据区域我们知道,线程中的 栈结构如下: 每个栈帧包含:本地变量表,操作数栈,动态链接,返回

  • Java数据结构之栈的线性结构详解

    目录 一:栈 二:栈的实现 三:栈的测试 四:栈的应用(回文序列的判断) 总结 一:栈 栈是限制插入和删除只能在一个位置上进行的表,此位置就是表的末端,叫作栈顶. 栈的基本操作分为push(入栈) 和 pop(出栈),前者相当于插入元素到表的末端(栈顶),后者相当于删除栈顶的元素. 二:栈的实现 public class LinearStack { /** * 栈的初始默认大小为10 */ private int size = 5; /** * 指向栈顶的数组下标 */ int top = -1

  • 详解java中jvm虚拟机栈的作用

    jvm虚拟机栈的作用 jvm虚拟机栈栈帧的组成 jvm虚拟机栈,也叫java栈,它由一个个的栈帧组成,而栈帖由以下几个部分组成 局部变量表-存储方法参数,内部使用的变量 操作数栈-在变量进行存储时,需要进行入栈和出栈 动态连接-引用类型的指针 方法出口-方法的返回 一段原程序代码 package com.lind.basic; public class Demo1 { static int hello() { int a = 1; int b = 2; int c = a + b; return

随机推荐