JVM内存总结
1.几个概念
下面这些概念是为了方便初学者理解文章中一些比较官方的名词,不是初学者直接略过即可
1.1JVM内存模型
- JVM内存模型(又称JVM运行时数据区域),指Java虚拟机的内存空间划分
- 根据Oracle公司发布的JVM实现规范,JVM运行时区域分为方法区、堆、虚拟机栈、本地方法栈、程序计数器五个部分
- 虚拟机常见的实现方式有两种:Stack based 的和 Register based。比如基于 Stack 的虚拟机有Hotspot JVM(这里的基于栈就是指操作数栈)、.net CLR,这种基于 Stack 实现虚拟机是一种广泛的实现方法。而基于 Register (寄存器和栈的结构&指令集是不一样的)的虚拟机有 Lua 语言虚拟机 LuaVM 和 Google 开发的安卓虚拟机 DalvikVM。
1.2Java内存模型
- Java内存模型(Java Memory Model,简称 JMM )与JVM内存模型有严格不同,Java内存模型是与多线程并发编程相关
- Java内存模型中定义了线程和本机内存中的抽象关系==>即JMM定义了Java虚拟机运行时在计算机内存(RAM)中的工作方式(程序运行时的抽象表现–>个人理解)
- Java 内存模型主要包含线程私有的程序计数器、java虚拟机栈、本地方法栈和线程共享的堆空间、元数据区、直接内存。
1.3其他常用概念
常量池分为静态常量池,运行时常量池,还有字符串常量池。
静态常量池储存的就是字面量以及符号引用
符号引用不会加载到虚拟机内存中,而直接引用是在内存中的。
在java中,一个java类会变编译为字节码文件,在编译时,java类并不知道所引用类的实际地址(也就是直接引用),所以要用符号引用来代替,在编译完成后,类加载时,在解析这一步来将符号引用来转变为直接引用。
例如:比如org.simple.People类引用了org.simple.Language类,在编译People类时并不知道Language类的实际内存地址,
因此只能使用符号org.simple.Language。(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)
直接引用:也就是对象在堆中的真实的地址。
1.3.1字面量
字面量是源代码中的固定值的表示法,通过字面就能知道其值的含义。字面量包括整数、浮点数和字符串字面量
字面量就是指由字母、数字构成的字符串或者数值常量,只可以右值出现,即等号右边的值
1.3.2符号引用
符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现
符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用
符号引用是编译原理中的概念,是相对于直接引用来说的,主要包含以下三类常量
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
1.3.3直接引用
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
1.3.4常量池vs运行时常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在class文件中
- 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池
1.3.5其他概念
1.简单名称(Simple Name)
没有类型和参数修饰的方法或字段名称
例如inc方法和字段name
2.全类名
全类名是某个文件在项目中的位置,格式为包名.类名
3.全限定名(Fully Qualified Name)
一个类的全限定名是将类全名的.全部替换为/
例如com/itheima/dao/IUserDao.xml
4.路径分为相对路径和绝对路径。
绝对路径是指这个文件在操作系统中的位置,
相对路径通过这个文件的上一级 ./ 或下一级/ 来指定文件内容
5.描述符(Descriptor)
描述符是表示字段或方法类型的字符串.
6.签名(Signatures)
Java代码层面的方法特征签名:方法名称 + 参数顺序 + 参数类型
字节码层面的方法特征签名: + 返回值 + 受查异常表
7.符号引用
符号引用包括域和方法的符号引用,符号引用由两部分组成:
1)所属的类或接口
2)字段和方法 名字+描述符
8.四种实体类类型概念:VO、DTO、DO、PO
————————————————
版权声明:本文为CSDN博主「焰火青年·」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a111042555/article/details/125033334
2.JVM内存模型&JMM工作方式
JDK8内存模型
JDK8之前内存模型
- 在 HotSpot JVM 中,永久代中用于存放类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
- 永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen ,为此我们不得不对虚拟机做调优。
- 那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?我总结了两个主要原因:
- 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
- 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量移至 Java Heap。
————————————————
版权声明:本文为CSDN博主「王树民」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangshuminjava/article/details/107057790
HotSpot JVM 与 JRockit VM 的融合
在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。 Oracle公司宣布在不久的将来(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工作,使之优势互补。 整合的方式大致上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务, 使用HotSpot的JIT编译器与混合的运行时系统。
2.1Java虚拟机栈(线程私有)【重点】
2.1.1几个概念
- 虚拟机栈就是我们平时所说的栈区
- 它描述的是Java方法执行时的内存模型
- 栈帧(Stack Frame,是方法运行的基础数据结构)
- JVM内存模型是基于栈帧的,
JVM 的执行引擎是基于栈的执行引擎(其中的栈指的就是操作栈) - 整个 JVM 指令执行的过程就是局部变量表与操作数栈之间不断 load、store 的过程
2.1.2JMM工作方式(对应方法模型)
- 栈区是线程私有的==>每个线程都对应一个私有的栈区,随着线程的创建而创建,线程结束后该栈会释放,生命周期和该线程保持一致,不存在垃圾回收问题
- 虚拟机栈描述的是Java方法执行的内存模型>每个方法在执行的同时都会创建一个栈帧(Stack Frame,方法运行的基础数据结构)用于存储**局部变量表、操作数栈、动态连接、方法返回地址(方法出口)**等信息。>每个方法从调用到执行完成的过程,就对应着一个栈桢在虚拟机中入栈到出栈的过程
- 在活动线程中,只有位于栈顶的帧才是有效的,叫做当前栈帧。当前栈帧所对应的方法叫做当前方法。栈帧是方法运行的基本结构,在执行引擎运行时,所有指令都只能针对当前栈帧进行操作
2.1.3虚拟机栈(Java Virtual Machine Stacks)的数据结构
- 虚拟机栈是在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址等信息。使用先进后出(FILO)的数据结构
- 虚拟机栈是基于线程的,是以线程方式运行,同时也是一个线程执行的区域,保存着一个线程中方法的调用状态。
- 一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈是线程私有的,独有的,随着线程的创建而创建。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
- 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧,一个方法调用完成,就会把该栈帧从栈中弹出。
- 大小限制:缺省为1M,可用参数 –Xss 调整大小,如-Xss256k
官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
如下a、b、c3个方法,a栈帧入栈,接着b栈帧入栈,接着c栈帧入栈;然后c栈帧出栈,b栈帧出栈,直到a栈帧出栈
2.1.4栈帧(Stack Frame)的数据结构
- 栈由一个个栈帧组成,对应一个线程的一个个方法,
- 每个栈帧包含四个区域:局部变量表、操作数栈、动态连接、方法返回地址
- 栈帧上的每个区域的数据结构都和栈类似,都是先进后出的数据结构,只支持出栈和入栈两种操作
局部变量表
- 局部变量表是存放方法参数和局部变量的区域。
- 局部变量表最基本的存储单位是Slot(变量槽)
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
- 参数值的存放总是在局部变量数组的index 0开始,到数组长度-1的索引结束。
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每个slot上。
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用其起始索引即可。(比如:访问long或者double类型变量),虚拟机不允许采用任何方式单独访问其中的某一个,如果遇到了这个问题,会在类加载的校验中抛出异常。
- 如果当前帧是由构造方法或者实例方法(不是由static修饰的方法)创建的,那么该对象引用this将会存放在局部变量表index为0的slot处,其余的参数按照参数表顺序继续排列。
- 局部变量表的大小在编译期间就已经确定
- 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数
- 局部变量没有准备阶段(即没有隐式初始化),必须显式初始化(显式初始化即为手工给予初值,否则为隐式初始化,将内容设置为默认值。)
变量槽(Slot)
它是一个32的长度(即一个引用变量占 4 个字节),主要存放基础数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference) 类型以及 returnAddress 类型。
在局部变量表里,32位以内的类型只占用一个slot(包括引用类型,returnAddress类型),64位的类型(long和double)占用两个slot
byte、short、char在存储前被转换成int,boolean也被转换成int,0表示false,非0表示true。
long和double则占据两个slot。
操作数栈
- 操作栈是个初始状态为空的桶式结构栈
- 操作数栈以压栈和出栈的方式存储操作数。主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
- 栈的大小同样也是在编译期间确定
- 在方法执行的过程中,局部变量表中的变量通过相关指令加载至操作数栈中作为操作数
- 在方法调用时,操作数栈也用来准备调用方法的参数和接收方法返回的结果。
- JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。
- 字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。
- 整个 JVM 指令执行的过程就是局部变量表与操作数栈之间不断 load、store 的过程
简单的出栈入栈流程
如下图,iadd 指令用来将两个 int 类型的数值相加,它要求执行之前操作数栈已经存在两个由前面其它指令放入的 int 型数值,在 iadd 指令执行时,两个 int 值从操作数栈中出栈,相加求和,然后将求和的结果重新入栈。
1 + 2 的代码执行过程如下:
i = i++在JVM底层的实现
所以说整个 JVM 指令执行的过程就是局部变量表与操作数栈之间不断 load、store 的过程
部分常用的JVM指令集
2.1.5动态连接
- 动态连接主要服务一个方法需要调用其他方法的场景。
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用—>持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
- 在Java源文件被编译成字节文件时,所有变量和方法引用都作为符号引用(Symbolic Reference)保存在Class文件的常量池中。
- 当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
- 动态连接的作用就是为了将符号引用转换为调用方法的直接引用
2.1.6方法返回地址
-
存放调用该方法的pc寄存器的值
-
无论通过哪种方式退出一个方法,都要回到该方法被调用的位置。
-
正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
-
异常退出时,返回地址通过异常表来确认,栈帧一般不会保存此信息。
-
当执行引擎遇到任意方法返回的字节码指令(return),会有返回值传递给上层的方法调用者。
注:一个方法调用完成后使用哪一个返回指令需要根据方法返回值的实际类型而定。包括ireturn, lreturn, freturn, dreturn, areturn(引用类型),除此之外还有一个return指令供void、实例初始化方法、类和接口的初始化方法使用。 -
当方法执行时出现异常,并且没有在方法内部处理(即本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出)
-
方法执行时有两种退出情况:
- 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
- 异常退出。
-
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:
- 返回值压入上层调用栈帧。
- 异常信息抛给能够处理的栈帧。
- PC计数器指向方法调用后的下一条指令。
2.2程序计数器(线程私有)
2.2.1程序计数器(PC 寄存器、计数器)
-
程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。
-
在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。
-
可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
-
倘若执行的是 native 方法,则程序计数器中为空
————————————————
版权声明:本文为CSDN博主「墨鸦_Cormorant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/footless_bird/article/details/128921448
2.2.2程序计数器
-
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
-
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。
-
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
-
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
————————————————
版权声明:本文为CSDN博主「王树民」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangshuminjava/article/details/107057790
2.2.3程序计数器 (The pc Register)
-
一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。
-
假如线程A正在执行到某个地方,突然失去CPU的执行权,切换到线程B,然后当线程A再获得CPU执行权的时候,怎么能继续执行?这时候就需要在线程中维护一个变量,记录线程执行到的位置,它就是程序计数器。
-
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
-
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
程序计数器主要有两个作用:
-
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
-
多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿。
注意:
-
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则这个计数器为空
-
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
————————————————
版权声明:本文为CSDN博主「CodeDevMaster」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38628046/article/details/127138029
2.2.4寄存器
寄存器定义
- 寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,包含的寄存器有累加器(ACC)。
- 寄存器是内存阶层中的最顶端,也是系统获得操作资料的最快速途径。寄存器通常都是以他们可以保存的位元数量来估量,举例来说,一个 “8 位元寄存器”或 “32 位元寄存器”。寄存器现在都以寄存器档案的方式来实作,但是他们也可能使用单独的正反器、高速的核心内存、薄膜内存以及在数种机器上的其他方式来实作出来。
- 寄存器通常都用来意指由一个指令之输出或输入可以直接索引到的暂存器群组。更适当的是称他们为 “架构寄存器”。
- 例如,x86 指令集定义八个 32 位元寄存器的集合,但一个实作 x86 指令集的 CPU 可以包含比八个更多的寄存器。
- 寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。
特点及原理
- 寄存器又分为内部寄存器与外部寄存器,所谓内部寄存器,其实也是一些小的存储单元,也能存储数据。但同存储器相比,寄存器又有自己独有的特点:
- ①寄存器位于CPU内部,数量很少,仅十四个;
- ②寄存器所能存储的数据不一定是8bit,有一些寄存器可以存储16bit数据,对于386/486处理器中的一些寄存器则能存储32bit数据;
- ③每个内部寄存器都有一个名字,而没有类似存储器的地址编号。
- 寄存器的功能十分重要,CPU对存储器中的数据进行处理时,往往先把数据取到内部寄存器中,而后再作处理。外部寄存器是计算机中其它一些部件上用于暂存数据的寄存器,它与CPU之间通过“端口”交换数据,外部寄存器具有寄存器和内存储器双重特点。有些时候我们常把外部寄存器就称为“端口”,这种说法不太严格,但经常这样说。
- 外部寄存器虽然也用于存放数据,但是它保存的数据具有特殊的用途。某些寄存器中各个位的0、1状态反映了外部设备的工作状态或方式;还有一些寄存器中的各个位可对外部设备进行控制;也有一些端口作为CPU同外部设备交换数据的通路。所以说,端口是CPU和外设间的联系桥梁。CPU对端口的访问也是依据端口的“编号”(地址),这一点又和访问存储器一样。不过考虑到机器所联接的外设数量并不多,所以在设计机器的时候仅安排了1024个端口地址,端口地址范围为0–3FFH。
寄存器用途
- 1.可将寄存器内的数据执行算术及逻辑运算;
- 2.存于寄存器内的地址可用来指向内存的某个位置,即寻址;
- 3.可以用来读写数据到电脑的周边设备。
计算机中寄存器的定义,寄存器,寄存器是什么意思-CSDN博客
2.3本地方法栈(线程私有)
2.3.1本地方法栈(Native Method Stacks)
本地方法栈是为JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以它通常又叫C栈
与虚拟机栈的区别:
-
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。如果在Java方法执行的时候调用native的方法,则会动态链接到本地方法栈执行
-
虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
-
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
-
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowError和OutOfMemoryError两种错误。
————————————————
版权声明:本文为CSDN博主「CodeDevMaster」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38628046/article/details/127138029
2.3.2本地方法栈
-
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
-
线程开始调用本地方法时,会进入 个不再受 JVM 约束的世界。本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器(程序计数器),具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。
-
JNI 类本地方法最著名的应该是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系统的特性功能,复用非 Java 代码。 但是在项目过程中, 如果大量使用其他语言来实现 JNI , 就会丧失跨平台特性。
————————————————
版权声明:本文为CSDN博主「王树民」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangshuminjava/article/details/107057790
2.3.3本地方法栈(Native Method Stacks)
-
本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。
-
本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。
-
本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。
虚拟机栈和本地方法栈的主要区别:
- 虚拟机栈执行的是 java 方法
- 本地方法栈执行的是 native 方法
————————————————
版权声明:本文为CSDN博主「墨鸦_Cormorant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/footless_bird/article/details/128921448
2.4直接内存(线程共享)
2.4.1直接内存(Direct Memory)
- 直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
- 在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
- 本机直接内存的分配不会受到Java堆大小的限制,但是还是会受到本机总内存的大小及处理器寻址空间的限制。
- 服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
直接内存与堆内存比较
-
直接内存申请空间耗费更高的性能
-
直接内存读取IO的性能要优于普通的堆内存
-
直接内存作用链: 本地IO -> 直接内存 -> 本地IO
-
堆内存作用链:本地IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地IO
其他内存
Code Cache:JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,如JIT编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面
GC功能需要运行在本地线程中,类似部分都需要占用内存空间。
————————————————
版权声明:本文为CSDN博主「CodeDevMaster」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38628046/article/details/127138029
————————————————
版权声明:本文为CSDN博主「王树民」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangshuminjava/article/details/107057790
2.4.2直接内存(Direct Memory)
-
直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。
-
常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。
-
分配、回收成本较高,但读写性能高。
-
直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。
————————————————
版权声明:本文为CSDN博主「墨鸦_Cormorant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/footless_bird/article/details/128921448
2.4.3JMM内存分类
Java 程序内存 = JVM 内存 + 本地内存
-
JVM 内存(JVM 虚拟机数据区)
- Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。
- JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM
-
本地内存(元空间 + 直接内存)
- 对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。
- 本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。
- 虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM
堆外内存
-
直接内存
- 直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。
- 可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样
- 内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;
-
线程堆栈
- 可通过 -Xss 调整大小
- 内存不足时抛出
- StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)
- OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)
-
Socket 缓存区
- 每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。
- 如果无法分配,可能会抛出 IOException:Too many open files异常
-
JNI 代码
- 如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存
-
虚拟机和垃圾收集器
- 虚拟机、垃圾收集器的工作也是要消耗一定数量的内存
————————————————
版权声明:本文为CSDN博主「墨鸦_Cormorant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/footless_bird/article/details/128921448
2.5方法区(线程共享)
-
方法区属于是JVM运行时数据区域的一块逻辑区域,是各个线程共享的内存区域,在虚拟机启动时创建
-
Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
-
当虚拟机要使用一个类时,它需要读取并解析Class文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
-
Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
2.5.1方法区的发展
-
JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。
-
为什么要使用元空间取代永久代的实现?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- 将 HotSpot 与 JRockit 合二为一。
-
永久代有一个JVM本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
-
元空间里面存放的是类的元数据,加载多少类的元数据就不由MaxPermSize控制, 而由系统的实际可用空间来控制,这样能加载的类就更多。
-
方法区参数设置
- JDK8之前
- -XX:PermSize:设置永久代初始分配空间,默认值是20.75M
- -XX:MaxPermSize:设定永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M
- 当永久代溢出时会得到错误:java.lang.OutOfMemoryError: PermGen space
- JDK8
- XX:MaxMetaspaceSize:设置最大元空间大小,默认值为unlimited,只受系统内存的限制
- -XX:MetaspaceSize:定义元空间的初始大小,如果未指定,则Metaspace将根据运行时的应用程序需求动态地重新调整大小
- 当元空间溢出时会得到错误: java.lang.OutOfMemoryError: MetaSpace
- JDK8之前
JDK版本 | 方法区实现 | 各版本变化 |
---|---|---|
jdk1.6 | 永久代(PermGen) | 字符串常量池,运行时常量池,静态变量都在永久代中 |
jdk1.7 | 永久代(PermGen) | 字符串常量池和静态变量被移动到了堆中,运行时常量池还在永久代中 |
jdk1.8 | 元空间(Metaspace) | 字符串常量池和静态变量还在堆中,运行时常量池、类型信息、常量、字段、方法被移动到元空间中,元空间取代了永久代,且不再占用JVM内存,直接用物理内存(本地内存)实现 |
2.5.2常量池(静态常量池)–>class文件的一部分
静态常量池储存的就是字面量以及符号引用
这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载(解析过程)中变为直接引用,或运行时会被转变变为被加载到内存区域的代码的直接引用(即动态链接)。
-
一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包含各种字面量和对类型、域、方法的符号引用
-
为什么需要常量池:
- 项目一个java源文件中的类、接口,编译后产生一个字节码文件。二java中的字节码文件需要数据支持,通常这种数据会很大以至于不能直接存到字节码这里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态连接的时候也会用到运行时常量池
-
常量池中有什么
- 数量值、字符串值、类引用、字段引用、方法引用
-
常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
2.5.3运行时常量池
- 运行时常量池(Runtime Constant Pool)是方法区的一部分
- 常量池表是Class文件的一部分,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型都维护一个常量池,通过索引访问
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换成真实地址
- 运行时常量池,相对于Class文件常量池的另一重要特性:具备动态性
- 运行时常量池类似于传统编程语言中的符号表,但是它所包含数据却比符号表要更加丰富一些
- 当创建类或接口的运行时常量池时,如果构造运运行时常量池所需的内存空间超过了方法区所能提供的最大值,则抛KKM
2.5.4字符串常量池
设计思想
- 字符串的分配,和其他对象分配一样,耗费高昂的时间和空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度的影响程序的性能
- JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 在创建字符串常量时,首先查询字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
发展历史
-
字符串常量池是JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
-
JDK7之前,字符串常量池存放在永久代。JDK7字符串常量池和静态变量从永久代移动到Java堆中。
-
这是因为永久代的GC回收效率太低,只有在Full GC的时候才会被执行回收。Java程序中通常会有大量被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
2.6堆(线程共享)
2.6.1堆内存模型【重点】
Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
2.6.1.1 JDK7的堆内存模型
JDK7的堆内存由:Young、Tenured、Perm、Virtual等区构成
。
- Young年轻区(代)
- Young区被划分为三部分: Eden区、两个大小严格相同的Survivor区(S0+S1),S0和S1一样大,也叫From和To
- S0和S1某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
- Tenured/Old年老区
- Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
- Perm永久区
- Perm代主要保存class、method、filed对象,这部份空间一般不会溢出,除非一次性加载很多的类
- Virtual区
- 最大内存和初始内存的差值,就是Virtual区。
2.6.1.2 JDK8的堆内存模型
JDK7与JDK8 堆内存的区别:
-
最大的Perm区,用Metaspace元数据空间进行替换。Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间。
-
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
-
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen,基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。
2.6.2Java 堆(Java Heap)
-
Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。
-
Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。
-
Java 堆的分区:
- 在 jdk1.8 之前,分为新生代、老年代、永久代
- 在 jdk1.8 及之后,只分为新生代、老年代
- 永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代
-
Java 堆内存大小:
- 堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)
- 既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)
- 如果堆无法扩展或者无法分配内存时报 OOM
-
主要存储的内容是:
- 对象实例
- 类初始化生成的对象
- 基本数据类型的数组也是对象实例
- 字符串常量池
- 字符串常量池原本存放在方法区,jdk8 开始放置于堆中
- 字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table
- 静态变量
- static 修饰的静态变量,jdk8 时从方法区迁移至堆中
- 线程分配缓冲区(Thread Local Allocation Buffer)
- 线程私有,但是不影响 java 堆的共性
- 增加线程分配缓冲区是为了提升对象分配时的效率
-
堆和栈的区别:
- 管理方式,堆需要GC,栈自动释放
- 大小不同,堆比栈大
- 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
- 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
- 效率:栈的效率比堆高
————————————————
版权声明:本文为CSDN博主「墨鸦_Cormorant」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/footless_bird/article/details/128921448
2.6.3Java堆
-
对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
-
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
-
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
————————————————
版权声明:本文为CSDN博主「王树民」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangshuminjava/article/details/107057790
2.6.4Survivor区及垃圾回收机制【重点】
- Survivor区是为了:减少老年代对象的产生,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC(回收新生代)还能在新生代中存活的对象,才会被移动到老年代。
- 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会移动到老年代
- 当老年代满了后,触发Major GC(回收老年代),注意:Major GC一般伴随着Minor GC,也可以看做触发了Full GC
- 老年代内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,会影响大型程序的执行和响应速度。
- 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长
- 假如减少老年代空间,虽然GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
- S0与S1区的出现是为了解决碎片化
- 如果只有一个Survivor区,新建对象在Eden中,一旦Eden满了,触发Minor GC,Eden中存活对象移动到Survivor区。如此循环,当下一次Eden区进行Minor GC时,Eden和Survivor各有一些存活对象,此时把Eden区的存活对象放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化的产生。
- 新生代中Eden:S1:S2的比例默认是8:1:1
- 新生代中的可用内存:复制算法用来担保的内存为9:1
- 可用内存中Eden:S1区为8:1
- 即新生代中Eden:S1:S2 = 8:1:1
- 堆内存中都是线程共享的区域吗?
- JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
- 对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
逃逸分析
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对。从JDK7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(未逃逸出去),那么对象可以直接在栈上分配内存。
2.6.5对象的创建在堆的实现
新创建的对象都会被分配到Eden区,一些特殊的大对象会直接分配到Old区。当Eden区空间不足GC后进入Survivor区的From区,或者进入Survivor区的To区,来回切换。通常直到GC回收18次后进入Old区。
大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入S0或者S1,并且对象的年龄还会加1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold设置。
堆内存溢出
当创建大量对象时,堆空间不足,则会堆内存溢出
设置堆大小
-Xmx20M -Xms20M
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at com.example.demo.DemoApplicationTests.main(DemoApplicationTests.java:44)
记住一句话:有方法就有栈,有对象就有堆,有类就有方法区。