我是靠谱客的博主 大力飞机,最近开发中收集的这篇文章主要介绍深入学习Java虚拟机:内存区域,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在Java中,分配内存和回收内存都由JVM自动完成。

 

内容:首先从操作系统层面简单介绍物理内存的分配和Java运行的内存分配之间的关系,明白在Java中使用的内存与物理内存区别。Java如何使用从物理内存申请下来的内存,以及如何来划分它们,如何分配和回收内存。最如何解决OutOfMemoryError,并提供一些处理这类问题的常用手段

 

 

内存的不同形态-物理内存和虚拟内存:

物理内存(RAM (随机存储器)):

计算机中,有一个存储单元叫寄存器,用于存储计算单元执行指令(如浮点、整数等运算时)的中间结果。

寄存器的大小决定了一次计算可使用的最大数值连接处理器和RAM或者处理器和寄存器的是地址总线

总线的宽度影响了物理地址的索引范围,因为总线的宽度决定了处理器一次可以从寄存器或者内存中获取多少个bit。也决定了处理器最大可以寻址的地址空间,如32位地址总线可寻址范围为0x0000 0000~0xffffffff。这个范围是232=4 294 967 296个内存位置,每个地址会引用一个字节,所以32位总线宽度可以有4GB的内存空间。

通常情况下,地址总线和寄存器或RAM有相同的位数,这样更容易传输数据,但是也有不一致的情况,如x86的32位寄存器宽度的物理地址可能有两种大小,分别是32位物理地址和36位物理地址,拥有36位物理地址的是Pentium Pro和更高型号。

 

除了在学校的编译原理的实践课或者要开发硬件程序的驱动程序时需要直接通过程序访问存储器外,大部分情况下都调用操作系统提供的接口来访问内存,在Java中甚至不需要写和内存相关的代码。

 

要运行程序,都要向操作系统先申请内存地址。通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能 访问自己的内存空间。这主要是从程序的安全性来考虑的,也便于操作系统来管理物理内存。

进程的内存空间的独立主要是指逻辑上独立,由操作系统来保证的,但是真正的物理空间是不是只能由一个进程来使用就不一定了。因为随着程序越来越庞大和设计的多任务性,物理内存无法满足程序的需求,在这种情况下就有了虚拟内存的出现

虚拟内存使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享,在逻辑上仍然是不能相互访问的。虚拟地址不但可以让进程共享物理内存、提高内存利用率,而且还能够扩展内存的地址空间,如一个虚拟地址可能被映射到一段物理内存、文件或者其他可以寻址的存储上。一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件中(即通常Windows系统上的页面文件,或者Linux系统上的交换分区),而真正高效的物理内存留给正在活动的程序使用。在这种情况下,在我们重新唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,并且会有一个短暂的停顿得到印证,这时操作系统又会把磁盘上的数据重新交互到物理内存中。但是必须要避免这种情况的经常出现,如果操作系统频繁地交互物理内存的数据和磁盘数据,则效率将会非常低,尤其是在Linux服务器上,我们要关注Linux中swap的分区的活跃度。如果swap分区被频繁使用,系统将会非常缓慢,很可能意味着物理内存已经严重不足或者某些程序没有及时释放内存

 

 

内存使用形式-内核空间和用户空间:

一个计算机通常有一定大小的内存空间,但是程序并不能完全使用这些地址空间,因为这些地址空间被划分为内核空间和用户空间

程序只能使用用户空间的内存,这里所说的使用是指程序能够申请的内存空间,并不是程序真正访问的地址空间。

 

为何需要内存空间和用户空间的划分呢?为了保证操作系统的稳定性,运行在操作系统中的用户程序不能访问操作系统所使用的内存空间。这也是从安全性上考虑的,如访问硬件资源只能由操作系统来发起,用户程序不允许直接访问硬件资源。如果用户程序需要访问硬件资源,如网络连接等,可以调用操作系统提供的接口来实现,这个调用接口的过程也就是系统调用。每一次系统调用都会存在两个内存空间的切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户空间的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是也牺牲了一部分效率。但是现在已经出现了很多其他技术能够减少这种从内核空间到用户空间的数据复制的方式,如Linux系统提供了sendfile文件传输方式

 

内核空间主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑。

 

内核空间和用户空间的大小如何分配?

更多地分配给用户空间供用户程序使用,还是首先保住内核有足够的空间来运行,这要平衡一下。

如果是一台登录服务器,很显然要分配更多的内核空间,因为每一个登录用户操作系统都会初始化一个用户进程,这个进程大部分都在内核空间里运行。

在当前32位Linux系统中默认的比例是1:3 (1GB的内核空间,3GB的用户空间)

 

 

 

Java自己中哪些组件需要使用内存?

Java启动后也作为一个进程运行在操作系统中,这个进程有哪些部分需要分配内存空间呢?

1、Java堆:用于存储Java对象的内存区域,堆的大小JVM启动时就一次向操作系统申请完成,通过-Xmx和-Xms两个选项来控制大小,Xmx表示堆的最大大小,Xms表示初始大小。一旦分配完成,堆的大小就将固定,不能在内存不够时再向操作系统重新申请,同时当内存空闲时也不能将多余的空间交还给操作系统。

在Java堆中内存空间的管理由JVM来控制,对象创建由Java应用程序控制,但是对象所占的空间释放由管理堆内存的垃圾收集器来完成。根据垃圾收集(GC)算法的不同,内存回收的方式和时机也会不同

2、线程:JVM运行实际程序的实体是线程,线程需要内存空间来存储一些必要的数据。每个线程创建时JVM都会为它创建一个堆栈,堆桟的大小根据不同的JVM实现而不同,通常在256KB〜756KB之间。线程所占空间相比堆空间来说比较小。如果线程过多,线程堆栈的总内存使用量可能也非常大。当前有很多应用程序根据CPU的核数来分配创建的线程数,如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致比较差的性能和更高的内存占用率

3、类和类加载器:类和加载类的类加载器本身同样需要存储空间,在Sun JDK中被存储在堆中,这个区域叫做永久代(PermGen区)

JVM是按需来加载类的,

JVM如果要加载一个jar包是否把这个jar包中的所有类都加载到内存中?显然不是。JVM只会加载那些在你的应用程序中明确使用的类到内存中。要查看JVM到底加载了哪些类,可以在启动参数上加上-verbose:class

 

3个默认类加载器Bootstrap ClassLoader、ExtClassLoader和AppClassLoader都不可能满足这些条件,因此,任何系统类(如java.lang.String)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放

4、NIO:一种基于通道和缓冲区来执行I/O的新方式。就像在Java堆上的内存支持I/O缓冲区一样,NIO使用java.nio.ByteBuffer.allocateDirect()方法分配内存,这种方式也就是通常所说的NIO direct memory。ByteBuffer.allocateDirect()分配的内存使用的是本机内存而不是Java堆上的内存,每次分配内存时会调用操作系统的Os::malloc()函数。另外一方面直接ByteBuffer产生的数据如果和网络或者磁盘交互都在操作系统的内核空间中发生,不需要将数据复制到Java内存中,执行这种I/0操作要比一般的从操作系统的内核空间到Java堆上的切换操作快得多,可以避免在Java堆与本机堆之间复制数据。如果你的I/O频繁地发送很小的数据,这种系统调用的开销可能会抵消数据在内核空间和用户空间复制带来的好处。

直接ByteBuffer对象会自动清理本机缓冲区,这个过程只能作为Java堆GC的一部分来执行,因此它们不会自动响应施加在本机堆上的压力。GC仅在Java堆被填满,以至于无法为堆分配请求提供服务时发生,或者在Java应用程序中显示请求时发生。当前在很多NIO框架中都在代码中显式地调用System.gc()来释放NIO持有的内存。这种方式会影响应用程序的性能,因为会增加GC的次数,一般情况下通过设置-XX:+DisableExplicitGC来控制System.gc()的影响,但是又会导致NIO direct memory内存泄漏问题。

 

5、JNI: Java运行时本身也依赖于JNI代码来实现类库功能,如文件操作、网络I/O操作或者其他系统调用。所以JNI也会增加Java运行时的本机内存占用。

 

 

 

JVM内存结构:JVM是按照运行时数据的存储结构来划分内存结构的,JVM在运行Java程序时,将它们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据(Runtime Data)。运行时数据包括Java程序本身的数据信息和JVM运行Java程序需要的额外数据信息,如要记录当前程序指令执行的指针(又称为PC指针)等。

在Java虚拟机规范中将Java运行时数据划分为6种:PC寄存器数据、Java栈、堆、方法区、本地方法区、运行时常量池

 

JVM内存模型:JVM运行时数据区(JVM Runtime Area)

指JVM在运行期间,其对计算机内存空间的划分和分配

包括:程序计数器、虚拟机栈、本地方法栈、Java堆、方法区以及方法区中的运行时常量池

方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区

 

1.程序计数器PC(Program Counter Register):线程私有

JVM会为每个线程创建一个程序计数器,不相互影响,在线程创建时被创建,指向下一条指令的地址。

程序计数器是一块较小的内存,保存当前线程所执行的行号指示器(内存地址)

字节码解释器工作时就是通过改变计数器值来选下一条需执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

当有多个线程交叉执行时,被中断线程的程序当前执行到哪条的内存地址必然要保存下来,以便于它被恢复执行时再按照被中断时的指令地址继续执行下去。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

1如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址

2如果正在执行的是Native方法. 这个计数器则为空undefinednative关键字说明其修饰的方法是原生态方法,方法对应的实现不在当前文件,而是用其他语言(如C/C++)实现的文件中。Java本身不能对操作系统底层进行访问和操作,但可通过JNI接口调用其他语言实现对底层的访问。JNIJava本机接口(Java Native Interface),JNI允许Java代码使用以其他语言编写的代码和代码库

 

2.Java虚拟机栈(常说的栈)(VM stack):

线程私有,所以不必关心数据一致性问题,以及同步锁问题。

虚拟机栈是一个先入后出的栈,每创建一个线程时,JVM会为这个线程创建一个对应的私有虚拟机栈。

线程对方法的调用就对应着一个栈帧的入栈和出栈的过程。

每调用一个方法就创建一个栈帧(虚拟机栈的生命周期和线程一样)。

 

栈帧是用来存储数据和存储部分过程结果的数据结构,也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

每个栈帧会含有一些内部变量、操作数栈、方法返回值等信息。方法执行完成后,栈帧被销毁。

存储方法运行时所产生的数据,成为栈帧(保存一个方法的局部变量、操作数栈、常量池指针)。

线程在运行的过程中,只有一个栈帧是处于活跃状态,这个栈帧在栈顶。

 

描述的Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,用于储存局部变量表操作数栈(Java无寄存器,所有参数传递使用操作数栈)、动态链接方法(指向运行时常量池的引用)方法出口(返回地址)等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈的生命周期和线程相同

栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分

Java虚拟机规范对这个区域规定了两种异常状况:

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。栈溢出,常发生在递归中

如果虚拟机扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。栈可以设置大小

 

注:

栈帧(Stack Frame):

栈帧用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址和一些额外的附加信息等信息。每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219244133078.png

1.局部变量表(Local Variable Table):

1、局部变量表(其内容空间在编译期就完成了分配):保存方法的参数以及局部变量

存放编译期可知的各种基本数据类型,引用类型(reference),returnAddress类型(指向了一条字节码指令的地址)。当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小。因为局部变量表只是存储对象的引用。

局部变量表所需的内存空间在编译期间完成分配。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

其中64位长度的long和double类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。

 

用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的

=====

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小,只是很有导向性地说到每个 Slot 都应该能存放一个 boolean、byte、char、short、int、float、reference (注:Java 虚拟机规范中没有明确规定 reference 类型的长度,它的长度与实际使用 32 还是 64 位虚拟机有关,如果是 64 位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取 32 位虚拟机的 reference 长度)或 returnAddress 类型的数据,这 8 种数据类型,都可以使用 32 位或更小的物理内存来存放,但这种描述与明确指出 “每个 Slot 占用 32 位长度的内存空间” 是有一些差别的,它允许 Slot 的长度可以随着处理器、操作系统或虚拟机的不同而发送变化。只要保证即使在 64 位虚拟机中使用了 64 位的物理内存空间去实现一个 Slot,虚拟机仍要使用对齐和补白的手段让 Slot 在外观上看起来与 32 位虚拟机中的一致。

 

Java 虚拟机的数据类型。一个 Slot 可以存放一个 32 位以内的数据类型,Java 中占用 32 位以内的数据类型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 种类型。

reference 类型表示对一个对象实例的引用,虚拟机规范既没有说明他的长度,也没有明确指出这种引用应有怎样的结构。但一般,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现 Java 语言规范中定义的语法约束约束。

returnAddress 类型目前已经很少见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,很古老的 Java 虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。

 

对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。Java 语言中明确的(reference 类型则可能是 32 位也可能是 64 位)64 位的数据类型只有 long 和 double 两种。值得一提的是,这里把 long 和 double 数据类型分割存储的做法与 “long 和 double 非原子性协定” 中把一次 long 和 double 数据类型读写分割为两次 32 位读写的做法有些类似,读者阅读到 Java 内存模型时可以互相对比一下。不过,由于局部变量建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的 Slot 是否为原子操作,都不会引起数据安全问题。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。如果访问的是 32 位数据类型的变量,索引 n 就代表了使用第 n 个 Slot,如果是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不允许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。

为了尽可能节省栈帧空间,局部变量中的 Slot 是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超出了某个变量的作用域,那这个变量对应的 Slot 就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot 的复用会直接影响到系统的垃圾收集行为

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

             byte[] placeholder = new byte[64 * 1024 * 1024]; 

             System.gc(); 

         } 

}

在虚拟机运行参数中加上“-verbose:gc” 来看看垃圾收集的过程,发现在 System.gc() 运行后并没有回收这 64 MB 的内存:

[GC 68516K->66144K(186880K), 0.0014354 secs]

[Full GC 66144K->66008K(186880K), 0.0127933 secs]

没有回收 placeholder 所占的内存能说得过去(full Gc回收之后的内存占用66008k所以说明没有被回收),因为在执行 System.gc() 时,变量 placeholder 还处于作用域之内,虚拟机自然不敢回收 placeholder 的内存。那把代码修改一下,如下:

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

                   {

                            byte[] placeholder = new byte[64 * 1024 * 1024]; 

                   }

             System.gc(); 

         } 

}

结果:

[GC 68516K->66120K(186880K), 0.0011906 secs]

[Full GC 66120K->66008K(186880K), 0.0093979 secs]

 

加入了花括号后,placeholder 的作用域被限制在花括号之内,从代码逻辑上讲,在执行 System.gc()时,placeholder 已经不可能再被访问了,但执行一下这段程序,会发现运行结果如下,还是有 64MB 的内存没有被回收(gc之后使用内存为6608k说明内存未被回收),这又是为什么呢?

在解释为什么之前,先对这段代码进行第二次修改,在调用 System.gc() 之前加入一行 “int a = 0;”,变成代码清单如下:

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

                   {

                            byte[] placeholder = new byte[64 * 1024 * 1024]; 

                   }

                   int a =0;

             System.gc(); 

         } 

}

运行结果:

[GC 68516K->66176K(186880K), 0.0012137 secs]

[Full GC 66176K->472K(186880K), 0.0095775 secs]

这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了(gc之后使用内存变为472k明显小于64m)。

在代码清单1 ~ 代码清单 3 中,placeholder 能否被回收的根本原因是:局部变量中的 Slot 是否还存在关于 placeholder 数组对象的引用。第一次修改中,代码虽然已经离开了 placeholder 的作用域,但在此之后,没有任何局部变量表的读写操作,placeholder 原本占用的 Slot 还没有被其他变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 的编译条件)下的 “奇技” 来使用。Java 语言的一本著名书籍《Practical Java》中把 “不使用的对象应手动赋值为 null” 作为一条推荐的编码规则。

虽然代码清单 1 ~ 代码清单 3 的代码示例说明了赋 null 值的操作在某些情况下确实是有用的,但笔者的观点是不应当对赋 null 值的操作又过多的依赖,更没有必要把它当做一个普遍的编码规则来推广。原因有两点,从编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法,如代码清单 3 那样的场景并不多见。更关键的是,从执行角度来将,使用赋 null 值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的,而概念模型与实际执行过程是外部看起来等效,内部看上去则可以完全不同。在虚拟机使用解释器执行时,通常与概念模型还比较接近,但经过 JIT 编译器后,才是虚拟机执行代码的主要方式, null 值的操作在经过 JIT 编译优化后就会被消除掉,这时候将变量设置为 null 就是没有意义的。字节码被编译为本地代码后,对 GC Roots 的枚举也与解释执行时期有巨大差别,以前面例子来看,代码清单 2 在经过 JIT 编译后,System.gc() 执行时就可以正确回收掉内存,无须写成代码清单 3 的样子。

关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 “准备阶段”。通过之前的讲解,已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何情况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样的默认值。如代码清单所示,

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

         int a;

         System.out.println(a);

         } 

}

这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败

 

2.操作数栈:(Operand Stack)

对于每个方法调用,JVM都会建立一个操作数栈,供计算使用。保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。后入先出的栈。方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递

 

同局部变量表一样,操作数栈的最大深度也在编译的时候写入到 Code 属性的 max_stacks 数据项中。操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。如,在做算术运算的时候是通过操作数栈来进行的,又或者再调用其他方法的时候是通过操作数栈来进行参数传递的。

举个例子,整数加法的字节码指令 iadd 在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个 int 型的数值,当执行这个指令时,会将这两个 int 值出栈并相加,然后将相加的结果入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。以iadd 指令为例,这个指令用于整型数加法,它执行时,最接近栈顶的两个元素的数据类型必须为 int 型,不能出现一个 long 和一个 float 使用 iadd 命令相加的情况。

 

在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递,重叠的过程如图 8-2 所示。

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219246235674.png

 

 

3.动态连接:

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking

Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分成为动态连接

 

根据动态连接可以动态地确定该栈帧属于哪个方法

 

4.方法返回地址:

当一个方法开始执行以后,有两种方法可以退出当前方法:

1.正常完成出口(Normal Method Invocatino Completion):当执行引擎遇到返回字节码指令,会将返回值传递给上层的方法调用者,一般,调用者的PC计数器可以作为返回地址

2.异常完成出口 (Abrupt Method Invocation Completion):当执行引擎遇到异常,且当前方法体内没有处理,导致方法退出,此时没有返回值,返回地址要通过异常处理器表来确定

 

无论何种退出方式,方法退出后,都需返回到方法被调用的位置,程序才能继续执行,方法返回时可能要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的 PC 计数器的值可作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的三个操作:

  1. 恢复上层方法的局部变量表和操作数栈
  2. 把返回值(如果有的话)压入调用者栈帧的操作数栈中,
  3. 调整 PC 计数器的值以指向方法调用指令后面的一条指令等。

 

 

5.额外的附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

 

 

3.本地方法栈(Native Method Stack):线程私有,

本地方法栈和虚拟机栈发挥的作用类似,但本地方法栈为虚拟机使用的 Native 方法服务。

区别:

虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,

本地方法栈则为虚拟机使用到的Native方法服务。 

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

 

4. java堆(堆内存)(Heap):线程共享,

Java堆是被所有线程共享的一块最大的内存区域在虚拟机启动时创建,唯一目的是存放对象实例几乎所有的对象实例和数组都在堆上分配内存。 

Java堆是垃圾收集器管理的主要区域。由于不同对象生命周期不同,进行分代处理(适合GC)。Java堆细分为新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation3个区域【JDK8前】。JDK8后Heap = { Old + NEW = {Eden, from, to} }
Java堆可以处于物理上不连续的内存空间中,逻辑上连续是连续即可。若在堆中没有完成实例分配,且堆无法再扩展时,会抛出OutOfMemoryError异常

 

详细分析:并不是所有的对象实例都存储在堆空间的

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219249314571.jpg

1.新生代

Eden 与 2个Survivor Space(S0S1FromTo)构成,大小通过-Xmn参数指定,Eden Survivor Space 的内存大小比例默认为8:1【这样空间利用率可以达到90%】,可通过-XX:SurvivorRatio 参数指定,如新生代为10M 时,Eden分配8M,S0和S1各分配1M。

1)、Eden伊甸园:大多情况下,对象在Eden中分配,Eden没有足够空间时,会触发一次Minor GC

2)、Survivor幸存者:新生代发生GC(Minor GC)时,存活对象会反复在S0S1之间移动,同时清空Eden区,当对象从Eden移动到Survivor或在Survivor间移动时,对象的GC年龄自动累加,当GC年龄超过默认阈值15时,该对象将被移动到老年代,可通过参数-XX:MaxTenuringThreshold 对GC年龄阈值设置。

 

2.老年代

空间大小即-Xmx 与-Xmn 两个参数之差,用于存放经过几次Minor GC之后依旧存活的对象。当老年代的空间不足时,会触发Major GC/Full GC,速度一般比Minor GC10倍以上。

 

3.永久代(JDK1.7前)

用于存放静态类型数据,如 Java Class, Method 等。JDK8前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可通过参数-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会抛出OOM异常。

 

JDK8的HotSpot中,把永久代从Java堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称为元空间。

 

逃逸分析和栈上分配优化技术是降低GC回收频率和提升GC回收效率的有效方法,

 

 

5.方法区(Method Area):线程共享

方法区主要保存的信息是类的元数据。

方法区存储虚拟机加载的类信息常量、静态变量、即时编译器编译后的代码等数据。(深入JVM中的描述)

线程共享,用于存储已被虚拟机加载的信息【JDK6时,String等常量信息置于方法区,JDK7时移动到堆】,通常和永久代(Perm)关联在一起。一般保存类相关信息,具体不同虚拟机实现

 

 

方法区大小一般在程序启动后一段时间内就固定了,JVM运行一段时间后,需要加载的类通常都已经加载到JVM中了。

方法区有点特殊,它不像其他Java堆一样会频繁地被GC回收器回收,它存储的信息相对比较稳定,但是它仍然占用了Java堆(永久代)的空间,所以仍然会被JVM的GC回收器来管理。

由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。

 

关于方法区: (移除永久代的工作在JDK7就开始了)
JDK7前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变; 
JDK7
中,存储在永久代的部分数据就转移到Java HeapNative memory。但还存在永久代,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap 
java8
中,取消永久代,方法存放于元空间(Metaspace),元空间仍与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。 
Native memory
:本地内存,也称C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC 

 

方法区是JVM的规范。永久代和元空间都是JVM规范的一种实现。只有HotSpot才有永久代。其他诸如JRockitOracle)、J9IBM)都没有。

 

两个属性:

-XX:MaxPermSize设置上限 
-XX:PermSize设置最小值 例:VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M 

默认情况下,-XX:MaxPermSize为64M,如果系统产生大量的类,就需要设置一个相对合适的方法区,避免永久区内存溢出。

 

关于运行时常量池:

Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 
运行时常量池(Runtime Constant Pool)相对于Class文件常量池的一个重要特征是具备动态性:即除了Class文件中常量池的内容能被放到运行时常量池外,运行期间也可能将新的常量放入池中,比如String类的intern()方法

 

当两个线程同时需要加载一个类型时,只有一个类会请求 ClassLoader  加载,另一个线程则会等待

 

方法区的垃圾回收:

在 HotSpot虚拟机中,方法区也被称为永久区,是一块独立于 Java 堆的内存空间。永久区中的对象也可被 GC 回收,只是GC 的对应策略与Java 堆空间略有不同。GC 针对永久区的回收,通常主要从两个方面分析:

一是 GC 对永久区常量池的回收,

二是永久区对类元数据的回收。

HotSpot 虚拟机对常量池的回收策略是很明确的, 只要常量池中的常量没有被任何地方引用 , 就可以被回收。

 

String. intern()方法的含义:

如果常量池中已经存在当前String,则返回池中的对象:

如果常量池中不存在当前 String对象,则先将 String 加入常量池,井返回池中的对象引用。

因此,不停地将 String 对象加入常量池会导致永久区饱和,如果 GC 不能回 收永久区的这些常量数据,那么就会抛出OutofMemoryError 错误。

 

 

逃逸分析与栈上分配:

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。

计算机软件方面,逃逸分析指的是计算机语言编译器语言优化管理中,分析指针动态范围的方法。

通俗点讲,如果一个对象的指针被多个方法或线程引用时,可以称这个指针发生了逃逸。

 

3中指针逃逸场景:全局变量赋值、方法返回值、实例引用传递

public  class  G {

public  static B  b;

public  void  globalVariablePointerEscape() {// 给全局变量赋值,发生逃逸

b=new  B();

  }

public  B  methodPointerEscape() {  // 方法返回值,发生逃逸

return  new  B();

  }

public  void  instancePassPointerEscape() {

methodPointerEscape() .printClassName(this);  //实例引用发生逃逸

}

}

逃逸的分析研究对于 Java 虚拟机的好处:

Java 对象总是在堆中被分配的,对象的创建和回收对系统的开销是很大的。

JDK6 里的 Swing 内存和性能消耗的瓶颈就是由于发生逃逸所造成的。栈里只保存了对象的指针,当对象不再被使用后,需要依靠 GC 来遍历引用树井回收内存,如果对象数量较多,会给 GC 带来较大压力,也直接或间接地影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。

 

 

一般是在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸,因为在方法体内未将引用暴露给外面,按照JVM内存分配机制,首先会在堆内创建变量类的实例,然后将返回的对象指针压入调用拢,继续执行。这是JVM优化前的方式。

可以采用逃逸分析原理对 JVM 进行优化,即针对拢的重新分配方式。首先要分析并且找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无须进入堆),分配完成后,继续在调用技内执行,最后线程结束,找空间被回收,局部变量对象也被回收。通过这种优化方式,与优化前的方式的主要区别在于栈空间直接作为临时对象的存储介质,从而减少了临时对象在堆内的分配数量。

基于逃逸分析的JVM优化原理很简单,但是在应用过程中还是有诸多因素需要被考虑。如,由于与 Java 的动态性有冲突,所以逃逸分析不能在静态编译时进行,必须在JIT里完成。

因为你可以在运行时通过动态代理改变一个类的行为,此时,逃逸分析是无法得知类已经变化

 

public void mymethod() {

V  v=new  V();

//use  v

v=null ;

}

在这个方法中创建的局部对象被赋给了 v, 但是没有返回,没有赋给全局变量等操作,因此这个对象是没有逃逸的,是可以在运行时在栈上进行分配和销毁的对象。没有发生逃逸的对象,由于生命周期都在一个方法体内,因此它们可以在运行时在栈上分配井销毁。

这样在 TIT 编译 Java 伪代码时,如果能分析出这种代码,那么非逃逸对象其创建和回收就可以在栈上进行,从而能大大提高 Java 的运行性能。

 

为什么要在逃逸分析之前进行内联分析呢?这是因为往往有些对象在被调用过程中创建井返回给调用过程,调用过程使用完该对象就被销毁了。这种情况下如果将这些方法进行内联,它们就由两个方法体变成一个方法体了,这种原来通过返回传递的对象就变成了方法内的局部对象,就变成了非逃逸对象了,这样这些对象就可以在同一椅上进行分配了。

 

Java7 开始支持对象的栈分配和逃逸分析机制。这样的机制除了能将堆分配对象变成栈分配对象外,逃逸分析还有其他两个优化应用。

·同步消除。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发性和性能。

·矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在 CPU 寄存器内,这样能大大加快访问速度。

 

Java7 完全支持栈式分配对象,JIT支持逃逸分析优化,此外 Java7 还默认支持 OpenGL 的加速功能。

 

 

 

 

Java虚拟机提供的优化技术,基本思想是一些不可能被其他线程访问的私有对象,可以打散分配到栈上,不分配到堆上,分配到栈上能在方法调用结束后自动销毁,不需要垃圾回收器,性能好。

运行参数为第一个时,会优化,在栈上分配,系统性能较高

运行参数为第二个时,堆上分配,系统性能不高,因为gc要不断回收

栈上分配一般用于小对象【因为栈很小】,在没有逃逸的情况下,可以直接分配在栈上,

直接分配在栈上,可以自动回收,减轻GC压力

大对象或逃逸对象无法再栈上分配

好处是方法结束后,分配的东西随着栈帧被销毁(也就是c++的new和delete的思想)

逃逸:如果这个对象在当前线程被用,其他线程也被用的时候,不能分配在栈上

 

 

直接内存和运行时常量池

运行时常量池:属于方法区。那些字符串什么的都在方法区的常量池

运行时可以产生常量

Intern可以把对象对应区域变成常量区

普通的常量成为字节码常量

运行时产生的常量叫运行时常量

 

直接内存:不是java虚拟机规范的一个区域。但确实真的存在的。

 

 

探究:JVM内存分配(不同版本)

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219322434375.jpg

JDK7及前期的JDK版本中,JVM空间可以分成三个大区,新生代、老年代、永久代

新生代划分为三个区,Eden区,两个幸存区。

一个对象被创建以后首先被放到新生代中的Eden内存中,如果存活期超两个Survivor之后就会被转移到老年代(Old Generation)中。

 

永久代中存放着对象的方法、变量等元数据信息

如果永久内存不够,会得到如下错误:java.lang.OutOfMemoryError: PermGen
 

JDK8 HotSpot JVM 将移除永久区,用本地内存来存储类元数据信息并称之为:元空间(Metaspace);

所以JDK8中JVM空间分为两大区:新生代、老年代。

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219327750476.jpg

永久代被移除后它的JVM参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。
 

关于元空间(Metaspace)

Metaspace 容量:

默认,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。
JVM参数MaxMetaspaceSize用于限制本地内存分配给类元数据的大小。若没有指定这个参数,元空间会在运行时根据需要动态调整。
Metaspace 垃圾回收:

对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。
适时地监控和调整元空间对于减小垃圾回收频率和减少延时很有必要。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

 

元空间特性:

1.      充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致

2.      每个加载器有专门的存储空间

3.      只进行线性分配

4.      不会单独回收某个类

5.      省掉了GC扫描及压缩的时间

6.      元空间里的对象的位置是固定的

7.      如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉

 

元空间的内存分配模型

1.      绝大多数的类元数据的空间都从本地内存中分配

2.      用来描述类元数据的类也被删除了

3.      分元数据分配了多个虚拟内存空间

4.      给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些

5.      归还内存块,释放内存块列表

6.      一旦元空间的数据被清空了,虚拟内存的空间会被回收掉

7.      减少碎片的策略

 

 

永久代:

JDK6和JDK7中,方法区可理解为永久区(Perm)。JDK8中,永久区被彻底移除,取而代之的是元数据区,

元空间和永久代的联系与区别

联系:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。(方法区是规范,元空间和永久代是实现)

区别:

1元空间并不在虚拟机中,而是使用本地内存,默认情况下,元空间的大小仅受本地内存的限制。与永久区不同,如果不指定大小,虚拟机会消耗所有可用的内存

可通过参数来指定元空间的大小:

  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

2永久区可使用-XX:PermSize和-XX:MaxPermSize指定。默认为64M。大的永久区可保存更多的类信息,如果系统使用一些动态代理,有可能会在运行时生产大量的类,要设置合理的数值,确保永久区不会内存溢出。

 

永久代溢出问题:

永久代是存在垃圾的,理论上使用的Java类越多,需要占用的内存也会越多,还有一种情况是可能会重复加载同一个类。通常情况下JVM只会加载一个类到内存一次,但是如果是自己实现的类加载器会出现重复加载的情况,如果PermGen区不能对已经失效的类做卸载,可能会导致PermGen区内存泄漏

PermGen区内存回收问题。通常一个类能够被卸载,有如下条件需要被满足:

在Java堆中没有对表示该类加载器的java.lang.ClassLoader对象的引用。
Java堆没有对表示类加载器加载的类的任何java.lang.Class对象的引用。
在Java堆上该类加载器加载的任何类的所有对象都不再存活(被引用)。

 

垃圾回收对永久代不显著。但有些应用可能动态生成或调用一些Class,如CGLib 等,就需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。但是需要设置多大才不会OOM并且不会浪费空间呢?

 

为什么移除永久代?

1、字符串存在永久代中,容易出现性能问题和内存溢出

2、永久代大小不容易确定,PermSize指定太小容易造成永久代OOM

3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

4、Oracle 可能会将HotSpot 与 JRockit 合二为一。(JRockit没有永久代)

 

对永久代的调优过程非常困难,永久代的大小很难确定,涉及到太多因素,如类的总数、常量池大小和方法数量等,永久代的数据可能会随着每一次Full GC而发生移动

类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间,可避免永久代的内存溢出问题,不过需要监控内存的消耗情况,一旦发生内存泄漏,会占用大量的本地内存。

 

移除永久代的应用:在JDK7中, 把原本放在永久代的字符串常量池移出, 放在堆中. 为什么这样做呢?

因为使用永久代来实现方法区不是个好主意, 很容易遇到内存溢出的问题. 通常使用PermSize和MaxPermSize设置永久代的大小, 这个大小就决定了永久代的上限, 但是不是总是知道应该设置为多大的, 如果使用默认值容易遇到OOM错误

 

移除永久代后数据存放的策略:

类的元数据, 字符串池, 类的静态变量将会从永久代移除, 放入Java heap或者native memory。

建议JVM的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入java堆中. 这样加载多少类的元数据就不在由MaxPermSize控制, 而由系统的实际可用空间来控制.

这样做的原因: 减少OOM只是表因, 深层原因是要合并HotSpot和JRockit的代码, JRockit没永久代, 但是运行良好, 也不需要开发运维人员设置这么一个永久代的大小。不用担心运行性能问题, 在覆盖到的测试中, 程序启动和运行速度降低不超过1%, 但是这一点性能损失换来了更大的安全保障。

 

 

JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。

 

关于方法区: (移除永久代的工作在JDK7就开始了)
JDK7前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变; 
JDK7
中,存储在永久代的部分数据就转移到Java HeapNative memory。但还存在永久代,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap 
java8
中,取消永久代,方法存放于元空间(Metaspace),元空间仍与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。 
Native memory
:本地内存,也称C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC 

 

通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

public class StringOomMock {
   
static String  base = "string";
    public static void
main(String[] args) {
        List<String> list =
new ArrayList<String>();
        for
(int i=0;i< Integer.MAX_VALUE;i++){   // 不断产生新的字符串,快速消耗内存
           
String str = base + base;
           
base = str;
           
list.add(str.intern());
       
}
    }
}

分别使用-XX:PermSize=8m -XX:MaxPermSize=8m参数进行运行

JDK8多采用-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m方式运行

JDK1.6:

JDK1.7:

JDK1.8:

-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m 方式:

https://file2.kaopuke.com:8081/files_image/2023110822/202311082219376339916.jpg  

这次不再出现永久代溢出,而是出现了元空间的溢出

JDK 6下,会出现“PermGen Space”的内存溢出,

在JDK7和 JDK8 中,会出现堆内存溢出,

并且 JDK 8中 PermSize 和 MaxPermGen 已经无效。

可大致验证 JDK 7 和 8 将字符串常量由永久代转移到堆中,并且 JDK 8 中已经不存在永久代的结论。

 

 

Q:永久带和方法区的区别?Java8后的元数据区又是怎么回事?

永久代和元数据区都是对方法区的实现。

方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。

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

 

HotSpot 虚拟机把 GC 分代收集扩展到方法区,即使用永久代来实现方法区,像 GC 管理 Java 堆一样管理方法区,从而省去专门为方法区编写内存管理代码,内存回收目标是针对常量池的回收和堆类型的卸载;

 

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

Java语言并不要求常量一定只有编辑期才能产生,也可能将新的常量放入池中,这种特性被开发人员利用得比较多是便是String类的intern()方法。 

当常量池无法再申请到内存时会抛出OutOfMemoryError异常

 

垃圾回收在方法区的行为

异常的定义

 

 

 

关于直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是Java程序中重要的组成成份。

直接内存跳过了Java堆,使Java程序可直接访问原生堆空间,一定程度上加快了内存空间的访问速度。

广泛用于NIO中。

直接内存使用达到上线时,会触发垃圾回收,如果不能有效释放空间,也会引起系统的OOM。

 

相关配置参数:

-XX:MaxDirectMEmorySize:如果不设置默认值为最大堆空间,即-Xmx。

 

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

 

 

 

 

 

最后

以上就是大力飞机为你收集整理的深入学习Java虚拟机:内存区域的全部内容,希望文章能够帮你解决深入学习Java虚拟机:内存区域所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(40)

评论列表共有 0 条评论

立即
投稿
返回
顶部