文章目录
- 运行时数据区
- 程序计数器
- 栈
- 栈帧
- 堆
- 方法区
- 本地方法栈
- 直接内存
- 变量存储位置
- 面试题
- 本地方法栈有什么用?
- 没有程序计数器会怎么样?
- 说一说Java的内存分布情况
- 类存放在哪里?
- 局部变量存放在哪里?
运行时数据区
java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区.
程序计数器
- 程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的的字节码指令的地址。
- 在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。
- 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。
- 程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
- 在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前线程解释执行到那一句指令,等该线程抢到时间片后继续解释运行。因此,为了线 程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互 不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
- 此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。
栈
- 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[插图](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 栈内存溢出
- Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出。Java虚拟机栈内存溢出时会出现StackOverflowError的错误。
- 如果Java虚拟机栈容量可以动态扩展,当栈扩 展时无法申请到足够的内存会抛出OutOfMemoryError异常。
- java虚拟机栈:设置大小
- 语法:
-Xss 栈大小
- 单位:字节(默认,必须是1024的倍数)
- 语法:
栈帧
栈帧是组成 Java 虚拟机栈的基本单位,每个栈有多个栈帧组成,每个栈帧存储一个方法。主要存储局部变量表、操作数栈、帧数据。
- 局部变量表:在方法执行过程中存放所有的局部变量。编译成字节码文件时就可以确定局部变量表的内容。
- 字节码文件中局部变量表的内容如下
- 栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot) ,long和double类型占用两个槽,其 他类型占用一个槽
注意:1)实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。
2)方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。
3) 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。
4)为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。
-
操作数栈
-
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。
-
在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小
![image.png](https://img-blog.csdnimg.cn/img_convert/05cfc8a2dd17752f4efd9d82eb04ed7e.png)
-
-
帧数据
帧数据主要包含动态链接、方法出口、异常表的引用
- 动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
- 方法出口:指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
- 异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
堆
- 一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。 栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。堆中存放的资源大概有以下几种
- 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
- 字符串常量池:
- 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
- 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
- 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
- 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
- 堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory 错误
- 堆空间有三个需要关注的值,used total max。
- used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
- 如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等
- 在arthus中堆内存used total max三个值可以通过dashboard -i 刷新频率(ms)命令看到。
- 如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。
- 设置
- Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。
- 在 Java7 中堆内会存在年轻代、老年代和方法区(永久代):
- Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
- Old 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
- Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理
方法区
方法区是存放基础信息的位置,线程共享,主要包含三部分内容:类的元信息、运行时常量池、字符串常量池
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式
- 方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:
- JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
- JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
-
类的基本信息(元信息),一般称之为**InstanceKlass对象。**在类的加载阶段完成。存放了类的基本信息,包括类的方法、参数、接口以及常量池表。
-
运行时常量池。存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。
- 当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
- 字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123就会被放入字符串常量池。
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
- 运行时常量池和字符串常量池的关系。
- JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。
本地方法栈
本地方法栈是为虚拟机执行本地方法时提供服务的
JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植
- 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
- 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
- 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
- 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 直接从本地内存的堆中分配任意数量的内存
- 可以直接使用本地处理器中的寄存器
原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数
- dlopen 函数:Linux 系统加载和链接共享库
- dlclose 函数:卸载共享库
直接内存
直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。直接向操作系统申请内存
在 JDK 1.4 中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题:
1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。
现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
- 要创建直接内存上的数据,可以使用Bytebuffer.
语法:Bytebuffer directbuffer =Bytebuffer.allocatedirect(size);
- 如果需要手动调整直接内存的大小,可以使用
-xx:MaxDirectMemorySize=大小
- 示例以不同的单位说明如何将直接内存大小设置为1024kb:
-xx:MaxDirectMemorySize=1m<br />-xx:MaxDirectMemorySize=1024k<br />-xx:MaxDirectMemorySize=1048576
- 示例以不同的单位说明如何将直接内存大小设置为1024kb:
变量存储位置
变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置
静态内部类和其他内部类:
- 一个 class 文件只能对应一个 public 类型的类,这个类可以有内部类,但不会生成新的 class 文件
- 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证)
类变量:
- 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 Java 进程产生和销毁
- 在 Java8 之前把静态变量存放于方法区,在 Java8 时存放在堆中的静态变量区
实例变量:
- 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分
- 在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
局部变量:
- 局部变量是定义在类的方法中的变量
- 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,
类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
- 类常量池与运行时常量池都存储在方法区,而字符串常量池在 Jdk7 时就已经从方法区迁移到了 Java 堆中
- 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符
- 在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池
- 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池
什么是字面量?什么是符号引用?
- 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示
int a = 1; //这个1便是字面量
String b = "iloveu"; //iloveu便是字面量
- 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用它的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址
面试题
本地方法栈有什么用?
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法 服务。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此 具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方 法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
没有程序计数器会怎么样?
没有程序计数器,Java程序中的流程控制将无法得到正确的控制,多线程也无法正确的轮换。
解释:
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来 选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢 复等基础功能都需要依赖这个计数器来完成。
- 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个cpu都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互 不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域
说一说Java的内存分布情况
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各 自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用 户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包 括以下几个运行时数据区域。
1. 程序计数器
- 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个 确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因 此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线 程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
- Java虚拟机栈
- 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[插图](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩 展时无法申请到足够的内存会抛出OutOfMemoryError异常。
- 本地方法栈
- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
- 《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定, 因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接 就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展 失败时分别抛出StackOverflowError和OutOfMemoryError异常。
- Java堆
- 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例, Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看, 随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现 在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已 经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
- 根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于 大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连 续的内存空间。
- Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无 法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
- 方法区
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
类存放在哪里?
答案:方法区
扩展:
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
局部变量存放在哪里?
栈帧中的局部变量表里面。
解释:
- 与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起 始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress类型(指向了一条字节码指令的地址)。