-
2021-12-10 20:38:43
近期在学习研究内存,那么Jvm内存结构和Jvm的内存模型即JMM(Java momery model)这两项内容都是学习java虚拟机、java内存知识的基础。为了让自己加深理解,有不至于嵌入到底层细节太深,会通过一段代码片段类比到内存结构图中的真实呈现。
一、Jvm体系的内存结构图
JVM运行数据区,也就是我们所说的内存结构主要分为两个大区:
1、线程共享区
**方法区(Method Area)😗*存储运行时的常量池、被虚拟机加载过的类信息、常量、静态变量、即时编译器编译后的代码数据。
**堆区(Heap Area)😗*所有的对象实例以及数组都要在堆上分配,也是垃圾回收器主要管理的对象。
2、线程独占区
**虚拟机栈:**又称Java栈,存储局部变量表、操作栈、动态链接、方法出口,对象指针。
**本地方法栈:**为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行,Android中的Jni本地方法调用亦该如此。
**程序计数器:**当前线程执行的字节码的行号指示器。
接下来我们通过一段简单的代码来看实际内存结构图
二、内存结构图实例分析
先看实例代码:
public class MemoryStructure { static int sSize =10;//静态变量 方法区 static MemoryObj memoryObj=new MemoryObj(sSize);//声明部分(静态类变量)放在方法区,对象实例在堆中 int initSize=20;//类的成员变量 堆区 MemoryObj memoryObj2=new MemoryObj(initSize);//声明部分(引用类型成员变量),实例都在 堆中 public static void main(String[] args) { //MemoryStructure memoryStructure 声明部分(引用类型) 放在虚拟栈 MemoryStructure memoryStructure = new MemoryStructure(); int size3=30;//栈区 //MemoryObj memoryObj3声明部分(引用类型) 放在虚拟栈 MemoryObj memoryObj3= new MemoryObj(size3); //新开线程 :新的独享虚拟栈 new Thread(){ @Override public void run() { int k=40;//栈区 setSize(40); } void setSize(int size){ memoryStructure.memoryObj2.size=size; //说明: //新线程访问主线程的局部变量是无法访问的,当前编译器没有报错,是因为idea工具编译做了处理 //查看一下编译后的字节码: final MemoryObj memoryObj3 = new MemoryObj(size3); MemoryObj temp=memoryObj3; } }.start(); } } class MemoryObj{ int size; public MemoryObj(int size) { this.size = size; } }
上述代码,对照内存结构图的分布位置如下:
正如代码中的注释和上述代码片段在各区中的分布图,能很清晰的知晓Jvm的内存结构。
补充一些堆和方法区的理解,虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。
方法区是为类信息服务的,存储类信息、常量、静态变量。成员变量直接存在在堆中。
虚拟栈区可以理解成为程序的执行服务的,方法中定义的局部变量,被保存在这个位置,最终方法的执行是通过出栈压栈的方式执行。
方法区、堆区是共享区域;虚拟栈区是独享区域:
每一个线程都会对应一个虚拟栈(又称java栈),上述newThread中的MemoryObj temp=memoryObj3;,若memoryObj3是无final修饰,是无法直接访问的,栈内数据是独享的。
这篇是通过一个实例分析的方式,对jvm的内存结构有了更清晰的认识。如若想做更深度的了解,推荐《深入理解 Java 虚拟机》这本书籍。
获得更多更新,关注gongzhonghao:Hym4Android
更多相关内容 -
JVM内存结构
2017-03-03 14:59:58写的不够详细,有错误的地方还望各位朋友指出,谢谢 -
JVM内存结构.pdf
2020-03-03 18:29:42jvm内存结构 -
JVM内存结构详解
2022-04-19 16:43:40JVM内存结构 堆(Heap) 方法区(Method Area) 程序计数器(Program Counter Register) 虚拟机栈(JVM Stacks) 本地方法栈(Native Method Stacks) 小结 你是否遇到这样的困惑: 堆内存该设置多大? ...目录
程序计数器(Program Counter Register)
你是否遇到这样的困惑:
堆内存该设置多大?
OutOfMemoryError异常到底是怎么引起的?
如何进行JVM调优?
JVM的垃圾回收是如何?
创建一个String对象,JVM都做了些什么?
这些疑问随着学习的深入都会慢慢得到解答,而要解决这些问题的第一步,就是先了解JVM的构成。
JVM内存结构
java虚拟机在执行程序的过程中会将内存划分为不同的数据区域,看一下下图。
如果理解了上图,JVM的内存结构基本上掌握了一半。通过上图我们可以看到什么?外行看热闹,内行看门道。从图中可以得到如下信息。
第一:
JVM分为五个区域:虚拟机栈、本地方法栈、方法区、堆、程序计数器。PS:大家不要排斥英语,此处用英文记忆反而更容易理解。
第二:
JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。图中已经用颜色区分,绿色表示“通行”,橘黄色表示停一停(需等待)。
第三:
JVM不同区域的占用内存大小不同,一般情况下堆最大,程序计数器较小。那么最大的区域会放什么?当然就是Java中最多的“对象”了。
学习延伸:如果你记住了这张图,是不是就可以说出关于JVM的内存结构了呢?可以尝试一下,切记不用死记硬背,发挥你的想象。
堆(Heap)
上面已经得出结论,堆内存最大,堆是被线程共享,堆的目的就是存放对象。几乎所有的对象实例都在此分配。
当然,随着优化技术的更新,某些数据也会被放在栈上等。
枪打出头鸟,因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。
因为GC的存在,且现代收集器基本都采用分代收集算法,所以堆又被细化了。
同样,对上图呈现内容汇总分析。
第一,堆的GC操作采用分代收集算法。
第二,堆区分了新生代和老年代;
第三,新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。
Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area)
方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。
正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。
方法区个性化的是,它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。
程序计数器(Program Counter Register)
关于程序计数器我们已经得知:占用内存较小,现成私有。它是唯一没有OutOfMemoryError异常的区域。
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
虚拟机栈(JVM Stacks)
虚拟机栈线程私有,生命周期与线程相同。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
**局部变量表(Local Variable Table)**是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
**操作数栈(Operand Stack)**也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks)与虚拟机栈作用相似,也会抛出StackOverflowError和OutOfMemoryError异常。
区别在于虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。
小结
经过上面的讲解,想必大家已经了解到JVM内存结构的基本情况。下面对照脑图,归纳总结一下,看你能说出来多少。
-
JVM初识-JVM内存结构
2022-05-19 16:51:29初识JVM,主要介绍JVM的内存结构:堆、栈、方法区、程序计数器等,还有常量池等介绍,以及一些经典例题,后续还有进阶JVM是什么呢?
JVM(Java Virtual Machine,Java虚拟机)
Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。
JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。
- JVM内存结构
1、JVM内存结构主要由三部分组成,分别是堆、方法区、栈
- 堆:内存占用最大,包括新生代(new space)和老年代(old space),新生代包含伊甸园(Eden)区、From Survivor空间、To Survivor空间.默认情况下按照8:1:1的比例进行分配.
- 堆是Java虚拟机所管理的内存中最大的一块存储区域,堆内存被所有线程共享.主要存放使用new关键字创建的对象.所有对象实例以及数组都要再对上分配.垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间,而不是对象本身). 堆中对象都需要考虑线程安全的问题,有垃圾回收机制
- 新生代存储“新生对象”,我们新创建的对象存储再年轻代中,当年轻代中内存不足以放下新对象时,则会触发Minor GC,清理年轻代内存空间.
- 老年代存储长期存活的对象和大对象,年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储,老年代空间不足以存放新晋升的对象时,则会触发Full GC,Full GC是清理整个堆空间,包括年轻代和老年代,如果Full GC后,堆中仍无法储存对象,则会抛出OutOfMemory内存溢出异常.
- 至于幸存区的两个空间,在垃圾回收阶段解释其作用.
2、方法区:
注意:在JDK1.8以前,静态成员储存在方法区(永久代)中,此时方法区的实现叫做永久代,在JDK1.8以后,永久代被移除,此时方法区的实现更改为元空间,但由于元空间主要用于存储字节码文件,因此静态成员的存储位置从方法区更改到了堆内存中.在JDK1.6及以前,常量池存储在方法区(永久代)中,在JDK1.7中,方法区被整合到堆内存中,常量池存储在堆内存中,在JDK1.8后,方法区从堆内存中独立出来,常量池存储在方法区(永久代)中.
1、方法区同Java堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量(JDK1.8以前)、即时编译器编译后的代码(即使编译器在类加载器阶段解释),更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中,常量池也是方法区的一部分.
2、常量池中存储编译器生成的各种字面量和符号引用.字面量就是java中常量的意思,比如文本字符串,final修饰的常量等.方法引用则包括类和接口的权限定名、方法名和描述符,字段名和描述符等.常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息.
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址JDK1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回,JDK1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回. StringTable也存在垃圾回收,如果想提高其性能,可以将其内部的桶buffer数提高,并且使用intern()方法可以避免重复字符串过多造成内存滥用.
比如下面这段代码:
S3==s5的返回值为true,因为在执行s3=“ab”时,就会在常量池中查询”ab”字符串,有则直接将“ab”字符串的引用赋值给s3,无则在常量池中储存“ab”字符串,再将引用返回并赋值给s3,当执行s5=“a”+”b”时,在编译阶段就已经优化为“ab”了,所以在常量池中查询”ab”字符串,发现已经存在,则将其引用赋值给s5,所以s5与s3的引用地址相同,则==运算自然为true,但s4!=s3,s4!=s5,因为s4是s1+s2,变量并不能在编译器进行优化,所以会使用StringBuilder().append()进行拼接,并返回新的字符串对象,所以虽然s4、s3、s5内容相同但引用地址不同.所以s4!=s3,s4!=s5
常量池中的字符串仅是符号,第一次用到时才变为对象,利用串池的机制,来避免重复创建字符串对象 字符串变量拼接的原理是stringBuilder ,字符串常量拼接的原理是编译期优化,可以使用intern方法,主动将串池中还没有的字符串对象放入串池
小练习:
输出为false,true,可以自己试试.
再试一试下面这段代码,加深理解:
False,true,true, x1==x2的结果: true,如果调换位置,则为false,不调换位置,jdk1.6的话则为false.
3、常量池作用:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享.
3、栈:
1、JVM中的栈包括java虚拟机栈和本地方法栈,两者的区别就是,java虚拟机栈为JVM执行java方法服务,本地方法则为JVM使用到的Native方法服务.两者作用是及其相似的,以下java虚拟机栈简称为栈.
2、定义:限定仅在表头进行插入和删除操作的线性表.即压栈和弹栈都是堆栈顶元素进行操作的,所以栈是先进后出的.栈是线程私有的,它的生命周期与线程相同,每个线程都会被分配一个栈的空间,因此每个线程都具有它私有的一个栈空间,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常.
3、栈帧,栈帧是栈的元素(是用于支持虚拟机进行方法调用和方法执行的数据结构),每个线程在调用某个方法,比如方法A(),当线程调用A时,jvm就会在该线程的栈中创建一个栈帧,栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息.每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程.
4、栈帧中,由一个局部变量表存储数据。
局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量〈包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(Iong、double),JVM会为其分配两个连续的变量槽来存储。以下简称Slot 。JVM通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的Slot数量。普通方法与static方法在第О个槽位的存储有所不同。非static方法的第О个槽位存储方法所属对象实例的引用。关于局部变量表的使用,将在字节码指令部分讲解. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
5、每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,
持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解另外的一部分将在每一次运行时期转化为直接引用,这部分称为动态连接.
6、当一个方法开始执行后,只有2种方式可以退出这个方法:
方法返回指令︰执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出︰在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
4、程序计数器(为每个线程私有):
程序计数器(Program Counter Register)是一块较小的内存空间(储存在栈中),可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。如果线程执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,计数器值为Undefined。程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。
-
一文搞懂JVM内存结构
2019-04-11 20:30:23举个最简单的例子,JVM 基本上是每家招聘公司都会问到的问题,它们会这么无聊问这些不切实际的问题吗?很显然不是。由 JVM 引发的故障问题,无论在我们开发过程中还是生产环境下都是非常常见的。比如 ...1. 前言
Java 虚拟机是中、高级开发人员必须修炼的知识,有着较高的学习门槛,很多人都不情愿去接触它。可能是觉得学习成本较高又或者是感觉没什么实用性,所以干脆懒得“搭理”它了。其实这种想法是错误的。举个最简单的例子,JVM 基本上是每家招聘公司都会问到的问题,它们会这么无聊问这些不切实际的问题吗?很显然不是。由 JVM 引发的故障问题,无论在我们开发过程中还是生产环境下都是非常常见的。比如 OutOfMemoryError(OOM) 内存溢出问题,你应该遇到过 Tomcat 容器中加载项目过多导致的 OOM 问题,导致 Web 项目无法启动。这就是JVM引发的故障问题。那到底JVM哪里发生内存溢出了呢?为什么会内存溢出呢?如何监控?最重要的就是如何解决问题呢?能解决问题的技术才是最实用最好的技术。然而你对JVM的内存结构都不清楚,就妄想解决JVM引发的故障问题,是不切实际的。只有基础打好了,对于JVM故障问题才能“披荆斩棘”。本文通过代码与图示详细讲解了JVM内存区域,相信阅读本文之后,你将对JVM内存的堆、栈、方法区等有一个清晰的认知。
2. 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。每个区域都有各自的作用。
分析 JVM 内存结构,主要就是分析 JVM 运行时数据存储区域。JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等。而 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
2.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。
为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。
如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。
2.2 栈
JVM 中的栈包括 Java 虚拟机栈和本地方法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 方法服务,本地方法栈则为 JVM 使用到的 Native 方法服务。两者作用是极其相似的,本文主要介绍 Java 虚拟机栈,以下简称栈。
Native 方法是什么?
JDK 中有很多方法是使用 Native 修饰的。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。个人理解Native 方法是与操作系统直接交互的。比如通知垃圾收集器进行垃圾回收的代码 System.gc(),就是使用 native 修饰的。
public final class System { public static void gc() { Runtime.getRuntime().gc(); } } public class Runtime { //使用native修饰 public native void gc();
什么是栈?
定义:限定仅在表头进行插入和删除操作的线性表。即压栈(入栈)和弹栈(出栈)都是对栈顶元素进行操作的。所以栈是后进先出的。
栈是线程私有的,他的生命周期与线程相同。每个线程都会分配一个栈的空间,即每个线程拥有独立的栈空间。
栈中存储的是什么?
栈帧是栈的元素。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
2.2.1 局部变量表
栈帧中,由一个局部变量表存储数据。局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。
JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。
Slot 复用?
为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。
public void test(boolean flag) { if(flag) { int a = 66; } int b = 55; }
当虚拟机运行 test 方法,就会创建一个栈帧,并压入到当前线程的栈中。当运行到 int a = 66时,在当前栈帧的局部变量中创建一个 Slot 存储变量 a,当运行到 int b = 55时,此时已经超出变量 a 的作用域了(变量 a 的作用域在{}所包含的代码块中),此时 a 就失效了,变量a 占用的 Slot 就可以交给b来使用,这就是 Slot 复用。
凡事有利弊。Slot 复用虽然节省了栈帧空间,但是会伴随一些额外的副作用。比如,Slot 的复用会直接影响到系统的垃圾收集行为。
public class TestDemo { public static void main(String[] args){ byte[] placeholder = new byte[64 * 1024 * 1024]; System.gc(); } }
上段代码很简单,先向内存中填充了 64M 的数据,然后通知虚拟机进行垃圾回收。为了更清晰的查看垃圾回收的过程,我们再虚拟机的运行参数中加上“-verbose:gc”,这个参数的作用就是打印 GC 信息。
打印的GC信息如下:
可以看到虚拟机没有回收这 64M 内存。为什么没有被回收?其实很好理解,当执行 System.gc() 方法时,变量 placeholder 还在作用域范围之内,虚拟机是不会回收的,它还是“有效”的。
我们对上面的代码稍作修改,使其作用域“失效”。
public class TestDemo { public static void main(String[] args){ { byte[] placeholder = new byte[64 * 1024 * 1024]; } System.gc(); } }
当运行到 System.gc() 方法时,变量 placeholder 的作用域已经失效了。它已经“无用”了,虚拟机会回收它所占用的内存了吧?
运行结果:
发现虚拟机还是没有回收 placeholder 变量占用的 64M 内存。为什么所想非所见呢?在解释之前,我们再对代码稍作修改。在System.gc()方法执行之前,加入一个局部变量。
public class TestDemo { public static void main(String[] args){ { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; System.gc(); } }
在 System.gc() 方法之前,加入 int a = 0,再执行方法,查看垃圾回收情况。
发现 placeholder 变量占用的64M内存空间被回收了,如果不理解局部变量表的Slot复用,很难理解这种现象的。
而 placeholder 变量能否被回收的关键就在于:局部变量表中的 Slot 是否还存有关于 placeholder 对象的引用。
第一次修改中,限定了 placeholder 的作用域,但之后并没有任何对局部变量表的读写操作,placeholder 变量在局部变量表中占用的Slot没有被其它变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。所以 placeholder 变量没有被回收。
第二次修改后,运行到 int a = 0 时,已经超过了 placeholder 变量的作用域,此时 placeholder 在局部变量表中占用的Slot可以交给其他变量使用。而变量a正好复用了 placeholder 占用的 Slot,至此局部变量表中的 Slot 已经没有 placeholder 的引用了,虚拟机就回收了placeholder 占用的 64M 内存空间。
2.2.2 操作数栈
操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。
通过一段代码来了解操作数栈。
public class OperandStack{ public static int add(int a, int b){ int c = a + b; return c; } public static void main(String[] args){ add(100, 98); } }
使用 javap 反编译 OperandStack 后,根据虚拟机指令集,得出操作数栈的运行流程如下:
add 方法刚开始执行时,操作数栈是空的。当执行 iload_0 时,把局部变量 0 压栈,即 100 入操作数栈。然后执行 iload_1,把局部变量1压栈,即 98 入操作数栈。接着执行 iadd,弹出两个变量(100 和 98 出操作数栈),对 100 和 98 进行求和,然后将结果 198 压栈。然后执行 istore_2,弹出结果(出栈)。
下面通过一张图,对比执行100+98操作,局部变量表和操作数栈的变化情况。
栈中可能出现哪些异常?
StackOverflowError:栈溢出错误
如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出 StackOverflowError
OutOfMemoryError:内存不足
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
如何设置栈参数?
使用 -Xss 设置栈大小,通常几百K就够用了。由于栈是线程私有的,线程数越多,占用栈空间越大。
栈决定了函数调用的深度。这也是慎用递归调用的原因。递归调用时,每次调用方法都会创建栈帧并压栈。当调用一定次数之后,所需栈的大小已经超过了虚拟机运行配置的最大栈参数,就会抛出 StackOverflowError 异常。
2.3 Java堆
堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。
Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。
老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。
Java堆设置常用参数
参数 描述 -Xms 堆内存初始大小 -Xmx(MaxHeapSize) 堆内存最大允许大小,一般不要大于物理内存的80% -XX:NewSize(-Xns) 年轻代内存初始大小 -XX:MaxNewSize(-Xmn) 年轻代内存最大允许大小,也可以缩写 -XX:NewRatio
新生代和老年代的比值
值为4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio=8 年轻代中Eden区与Survivor区的容量比例值,默认为8
表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError
内存溢出时,导出堆信息到文件
-XX:+HeapDumpPath
堆Dump路径
-Xmx20m -Xms5m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/a.dump
-XX:OnOutOfMemoryError
当发生OOM内存溢出时,执行一个脚本
-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p
%p表示线程的id pid
-XX:MaxTenuringThreshold=7 表示如果在幸存区移动多少次没有被垃圾回收,进入老年代
2.4 方法区(Method Area)
方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分。
注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:
- MetaSpaceSize:初始化元空间大小,控制发生GC阈值
- MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。方法引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。
常量池有什么用 ?
优点:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
举个栗子: Integer 常量池(缓存池),和字符串常量池
Integer常量池:
我们知道 == 基本数据类型比较的是数值,而引用数据类型比较的是内存地址。
public void TestIntegerCache() { public static void main(String[] args) { Integer i1 = new Integer(66); Integer i2 = new integer(66); Integer i3 = 66; Integer i4 = 66; Integer i5 = 150; Integer i6 = 150; System.out.println(i1 == i2);//false System.out.println(i3 == i4);//true System.out.println(i5 == i6);//false } }
i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。
i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。
而 IntegerCache 是 Integer的静态内部类,作用就是将 [-128,127] 之间的数“缓存”在 IntegerCache 类的 cache 数组中,valueOf 方法就是调用常量池的 cache 数组,不过是将 i3、i4 变量引用指向常量池中,没有真正的创建对象。而new Integer(i)则是直接在堆中创建对象。
IntegerCache 类中,包含一个构造方法,三个静态变量:low最小值、high最大值、和Integer数组,还有一个静态代码块。静态代码块的作用就是在 IntegerCache 类加载的时候,对high最大值以及 Integer 数组初始化。也就是说当 IntegerCache 类加载的时候,最大最小值,和 Integer 数组就已经初始化好了。这个 Integer 数组其实就是包含了 -128到127之间的所有值。
IntegerCache 源码
private static class IntegerCache { static final int low = -128;//最小值 static final int high;//最大值 static final Integer cache[];//缓存数组 //私有化构造方法,不让别人创建它。单例模式的思想 private IntegerCache() {} //类加载的时候,执行静态代码块。作用是将-128到127之间的数缓冲在cache[]数组中 static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1];//初始化cache数组,根据最大最小值确定 int j = low; for(int k = 0; k < cache.length; k++)//遍历将数据放入cache数组中 cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } }
而 i5 == i6 为 false,就是因为 150 不在 Integer 常量池的最大最小值之间【-128,127】,从而 new 了一个对象,所以为 false。
再看一段拆箱的代码。
public static void main(String[] args){ Integer i1 = new Integer(4); Integer i2 = new Integer(6); Integer i3 = new Integer(10); System.out.print(i3 == i1+i2);//true }
由于 i1 和 i2 是 Integer 对象,是不能使用+运算符的。首先 i1 和 i2 进行自动拆箱操作,拆箱成int后再进行数值加法运算。i3 也是拆箱后再与之比较数值是否相等的。所以 i3 == i1+i2 其实是比较的 int 型数值是否相等,所以为true。
String常量池:
String 是由 final 修饰的类,是不可以被继承的。通常有两种方式来创建对象。
//1、 String str = new String("abcd"); //2、 String str = "abcd";
第一种使用 new 创建的对象,存放在堆中。每次调用都会创建一个新的对象。
第二种先在栈上创建一个 String 类的对象引用变量 str,然后通过符号引用去字符串常量池中找有没有 “abcd”,如果没有,则将“abcd”存放到字符串常量池中,并将栈上的 str 变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。
对于 String 类,equals 方法用于比较字符串内容是否相同; == 号用于比较内存地址是否相同,即是否指向同一个对象。通过代码验证上面理论。
public static void main(String[] args){ String str1 = "abcd"; String str2 = "abcd"; System.out.print(str1 == str2);//true }
首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。
public static void main(String[] args){ String str1 = new String("abcd"); String str2 = new String("abcd"); System.out.print(str1 == str2);//false }
str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。
关于字符串 + 号连接问题:
对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了。
String a = "a1"; String b = "a" + 1; System.out.println((a == b)); //result = true String a = "atrue"; String b = "a" + "true"; System.out.println((a == b)); //result = true String a = "a3.4"; String b = "a" + 3.4; System.out.println((a == b)); //result = true
关于字符串引用 + 号连接问题:
对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。
public static void main(String[] args){ String str1 = "a"; String str2 = "ab"; String str3 = str1 + "b"; System.out.print(str2 == str3);//false }
对于上边代码,str3 等于 str1 引用 + 字符串常量“b”,在编译期无法确定,在运行期动态的分配并将连接后的新地址赋给 str3,所以 str2 和 str3 引用的内存地址不同,所以 str2 == str3 结果为 false
通过 jad 反编译工具,分析上述代码到底做了什么。编译指令如下:
经过 jad 反编译工具反编译代码后,代码如下
public class TestDemo { public TestDemo() { } public static void main(String args[]) { String s = "a"; String s1 = "ab"; String s2 = (new StringBuilder()).append(s).append("b").toString(); System.out.print(s1 = s2); } }
发现 new 了一个 StringBuilder 对象,然后使用 append 方法优化了 + 操作符。new 在堆上创建对象,而 String s1=“ab”则是在常量池中创建对象,两个应用所指向的内存地址是不同的,所以 s1 == s2 结果为 false。
注:我们已经知道了字符串引用的 + 号连接问题,其实是在运行期间创建一个 StringBuilder 对象,使用其 append 方法将字符串连接起来。这个也是我们开发中需要注意的一个问题,就是尽量不要在 for 循环中使用 + 号来操作字符串。看下面一段代码:
public static void main(String[] args){ String s = null; for(int i = 0; i < 100; i++){ s = s + "a"; } }
在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作。
public static void main(String[] args){ StringBuilder s = new StringBuilder(); for(int i = 0; i < 100; i++){ s.append("a"); } }
使用final修饰的字符串
public static void main(String[] args){ final String str1 = "a"; String str2 = "ab"; String str3 = str1 + "b"; System.out.print(str2 == str3);//true }
final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。
String对象的intern方法。
public static void main(String[] args){ String s = "ab"; String s1 = "a"; String s2 = "b"; String s3 = s1 + s2; System.out.println(s3 == s);//false System.out.println(s3.intern() == s);//true }
通过前面学习我们知道,s1+s2 实际上在堆上 new 了一个 StringBuilder 对象,而 s 在常量池中创建对象 “ab”,所以 s3 == s 为 false。但是 s3 调用 intern 方法,返回的是s3的内容(ab)在常量池中的地址值。所以 s3.intern() == s 结果为 true。
参考《深入理解Java虚拟机》
觉得本文对你有帮助,请不要吝啬你的赞!
-
jvm内存结构以及Java内存模型JMM的模型图
2021-10-03 11:23:30jvm和jmm的模型图,注意在浏览器中搜索draw.io进行文件的打开。 -
JVM内存结构解析
2022-05-20 14:40:09JVM内存结构解析 JVM 内存结构是什么?我们先看图: (一)JVM内存结构一共分为5个区:Java虚拟机栈、本地方法栈、程序计数器、堆、方法区。 (二)我先分别解释每个区各自的作用: Java虚拟机栈:用于存储局部... -
JVM之JVM运行时内存结构, JDK1.7 JVM内存结构, JDK1.8 JVM内存结构, JVM堆内存结构
2020-05-26 17:53:321. JVM内存图 1.1 JDK1.7 JVM内存图 程序计数器: 线程私有的(每个线程都有一个自己的程序计数器), 是一个指针. 代码运行, 执行命令. 而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了. Java... -
jvm内存结构与java内存模型
2022-04-23 12:38:35前几天在面试网易的时候被问到java内存结构与内存模型,傻傻分不清,其实JVM内存结构是与JVM的内部存储结构相关,而java内存模型是与多线程有关。 JVM构成: Java源代码编译成Java Class文件后通过类加载器... -
JVM内存结构和Java内存模型别再傻傻分不清了
2020-03-04 20:53:30JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺大的。 通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多... -
基于JDK1.8的JVM 内存结构【JVM篇三】
2021-07-23 10:19:03在我的上一篇文章别翻了,这篇文章绝对让你...是的,这两个问题就涉及到了JVM 内存结构的知识了,那么这篇文章将进行解答。 @目录1、内存结构还是运行时数据区?2、运行时数据区3、线程共享:Java堆、方法区3.1、Jav... -
jvm讲解-jvm内存结构详解
2022-01-15 17:46:01以上这个图可以看出内存结构的构成: 方法区 堆 虚拟机栈 程序计数器 本地方法栈 下面开始详细介绍这些内容; 1. 方法区(线程共享) 1.1 定义: 被所有线程共享的一块内存区域。 用于存储已被虚拟机... -
jvm内存模型、jvm内存结构、Java内存结构、Java内存模型(JMM)、Java对象模型的区别(吐血研究整理)
2020-06-06 19:30:39jvm内存结构: 等同于Java内存结构,汉语虽然博大精深,但是也经常会因为命名很雷同让人懵逼或者混淆不清。 Java内存结构: Java内存模型(JMM): java内存模型又称为JMM。为了解决Java多线程对共享数据的... -
JVM内存结构概述
2019-08-20 23:32:58本节将会介绍一下JVM的内存结构,JVM...JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试... -
jvm内存结构
2021-02-25 17:06:33jvm内存结构 jvm内存结构包含什么? Java堆(Heap) 方法区(Method Area) 程序计数器 JVM栈 本地方法栈 jvm内存结构详细介绍 方法区:主要是存储类信息,常量池(static常量和static变量),编译后的代码... -
JVM内存结构 VS Java内存模型 VS Java对象模型
2019-10-21 15:12:37比如本文我们要讨论的JVM内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。 可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的... -
JVM篇-JVM内存结构与存储机制
2021-06-13 22:33:25JVM内存结构 首先分清楚几个词:JVM内存结构、Java内存模型、Java对象模型 JVM内存结构 Java程序执行过程中,内存会被划分为不同的数据区域,各个区域有各自的用途。 有些区域随虚拟机的启动而存在。有些区域随线程... -
JVM内存结构相关知识解析
2020-08-25 10:24:34主要介绍了JVM内存结构相关知识解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 -
深入理解JVM内存结构及运行原理全套视频加资料
2019-11-20 20:51:422019最新深入理解JVM内存结构及运行原理(JVM调优)高级核心课程视频教程下载。JVM是Java知识体系中的重要部分,对JVM底层的了解是每一位Java程序员深入Java技术领域的重要因素。本课程试图通过简单易懂的方式,系统... -
JVM内存结构的组成、各部分功能作用
2022-02-22 09:13:16作用:是记住下一条jvm指令的执行地址 特点: 是线程私有的 不会存在內存溢出 二、虚拟机栈 每个线程运行时所需要的内存,称为虚拟机栈 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的... -
【java】java Jvm内存结构
2021-02-22 18:16:49转载:java Jvm内存结构 2.运行时数据区 著作权归https://pdai.tech所有。 链接:https://www.pdai.tech/md/java/jvm/java-jvm-struct.html 内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作... -
最简单的JVM内存结构图
2021-03-11 20:42:03JVM内存结构图 方法区 堆 栈 程序计数器 本地方法栈 直接内存 内存分配性能优化-逃逸分析 总结 JVM内存结构图 大家好,好几天没有更新了,今天的内容有点多,我们详细介绍下JVM内部结构图,还是和之前...