- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「 小牛呼噜噜 」 。
大家好,我是呼噜噜,这次我们一起来看看Java内存区域,本文 基于HotSpot 虚拟机,JDK8, 干货满满 。
Java 内存区域 , 也叫 运行时数据区域、内存区域、JVM内存模型 ,和 Java 虚拟机(JVM)的运行时区域相关,是指 JVM运行时将数据分区域存储,强调对内存空间的划分。 经常与 Java内存模型(JMM) 混淆,其定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。 JVM并不是只有唯一版本的,在Java发展历史中,有许多优秀的Java虚拟机,其中目前大家最熟悉的就是HotSpot虚拟机,什么你不知道?
我们去 Oracle 官网,下载JDK,其自带的虚拟机,就是HotSpot.
HotSpot VM的最大特色: 热点代码探测 ,其可以通过执行计数器,找出最具有编译价值的代码,然后通知JIT编译器进行编译,通过编译器和解释器的协同合作,在最优程序响应时间和最佳执行性能中取得平衡.
简单介绍一下,上图的主要组成部分:
程序计数器、Java虚拟机栈、本地方法栈、本地方法区、堆 以及方法区
。下文我们以此图为基准,详细地分析各个部分,慢慢道来 程序计数器(Program Counter Register)是用于 存放下一条指令所在单元地址的一块内存 ,在虚拟机的规范里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成.
我们来对Java中class文件反编译:
在JVM逻辑上规定,程序计数器是一块较小的内存空间,可以看作是 当前线程所执行字节码的行号指示器 ,PC寄存器,也叫"程序计数器",其是CPU中寄存器的一种,偏硬件概念 。
由于程序计数器保存了 下一条指令要执行地址 ,所以在JVM中,执行指令的一般过程:执行引擎会从 程序计数器中获得下一条指令的地址,拿到其对应的操作指令,对其进行执行,当该指令结束, 字节码解释器 会 根据pc寄存器里的值选取下一条指令并修改pc寄存器里面的值 ,达到执行下一条指令的目的,周而复始直至程序结束.
字节码解释器可以拿到所有的字节码指令执行顺序,而程序计数器只是为了记录当前执行的字节码指令地址,防止线程切换找不到下一条指令地址 。
我们知道操作系统中线程是由 CPU调度 来执行指令的,JVM的多线程是通过 CPU时间片轮转 来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置.
执行程序在单线程情况下还好,但在多线程的情况下:线程在执行的指令时,CPU可能切换线程,去另一个更紧急的指令,执行完再继续执行先前的指令。特别是单核CPU的情况下,CPU会频繁的切换线程,"同时"执行多个任务。为了CPU切换线程后,依旧能恢复到先前指令执行的位置,这就需要每个线程有自己独立的程序计数器,互不影响。我们可以发现 程序计数器是线程私有的,每条线程都有一个程序计数器.
程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError(内存泄漏)的区域 ,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。因为当前线程正在执行Java中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined) 。
PC寄存器(程序计数器)与JVM中的程序计数器还是有所区别的:
- PC寄存器永远指向下一条待执行指令的内存地址(永远不会为undefined) ,并且在程序开始执行前,将程序指令序列的起始地址,即程序的第一条指令所在的内存单元地址送入PC, CPU按照PC的指示从内存读取第一条指令(取指)
- 当执行指令时,CPU会自动地修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数(指令字节数),使PC总是指向下一条将要取指的指令地址。
- 由于大多数指令都是按顺序来执行的,所以修改PC的过程通常只是简单的对PC 加“指令字节数”。当程序转移时,转移指令执行的最终结果就是要改变PC的值,此PC值就是转去的目标地址。处理器总是按照PC指向,取指、译码、执行,以此实现了程序转移。
虚拟机栈(JVM Stacks),和数据结构上的栈类似,先进后出。其与程序计数器一样,也是线程私有的,其生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡.
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个 栈帧 ,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。 栈帧在虚拟机栈中入栈到出栈(顺序: 先进后出)的过程,其实就对应Java中方法的调用至执行完成的过程 。
栈帧 是用于 支持虚拟机进行方法调用和方法执行的数据结构 ,它是虚拟机运行时数据区中的虚拟机栈的栈元素,每个栈帧存储了方法的变量表、操作数栈、动态连接和方法返回等信息.
其中:
StackOverflowError异常
; OutOfMemoryError异常
。《Java虚拟机规范》 明确允许Java虚拟机实现自行选择是否支持栈的动态扩展 , HotSpot虚拟机是选择不支持扩展 ,所以 HotSpot虚拟机在线程运行时是不会因为扩展而导致OutOfMemoryError(内存溢出)的异常 我们下面主要介绍一下栈帧的结构:
局部变量表:是存放 方法参数和局部变量 的区域,主要存放了编译期可知的 各种数据类型 (boolean、byte、char、short、int、float、long、double)、 对象引用 (reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 。
我们知道 局部变量没有赋初始值是不能使用的 ,而 全局变量 是放在堆的,有两次赋值的阶段,一次在类加载的准备阶段,赋予系统初始值;另外一次在类加载的初始化阶段,赋予代码定义的初始值。拓展见: 类加载器 。
局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间.基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个 。
在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中 第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中 可以通过关键字 this 来访问到这个隐含的参数 )其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。 关键字this详解 我们可以写个例子验证一下 。
public class Test {
void fun(){
}
}
javac -g:vars Test.java 生成Test.class文件,一定要加参数 -g:vars ,不然反编译时,无法显示局部变量表LocalVariableTable 我们接着反编译一下:
javap -v Test
Classfile /D:/GiteeProjects/study-java/study/src/com/company/test3/Test.class
Last modified 2022-11-20; size 261 bytes
MD5 checksum 72c7d1fcc5d83dd6fc82c43ae55f2b34
public class com.company.test3.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#11 // java/lang/Object."<init>":()V
#2 = Class #12 // com/company/test3/Test
#3 = Class #13 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LocalVariableTable
#8 = Utf8 this
#9 = Utf8 Lcom/company/test3/Test;
#10 = Utf8 fun
#11 = NameAndType #4:#5 // "<init>":()V
#12 = Utf8 com/company/test3/Test
#13 = Utf8 java/lang/Object
{
public com.company.test3.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/company/test3/Test;
void fun();
descriptor: ()V
flags:
Code:
stack=0, locals=1, args_size=1
0: return
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/company/test3/Test; //!!!可以看出this在Slot的第0位!!!
}
操作数栈 主要用于 存放方法执行过程中产生的中间计算结果或者临时变量 ,通过变量的入栈、出栈等操作来执行计算。 在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。我们前文说的JVM执行引擎,是基于栈的执行引擎, 其中的栈指的就是操作数栈 。
每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法 , 这就是 动态链接。 本质就是, 在方法运行时将符号引用转为调用方法的直接引用, 这种引用转换的过程具备动态性 不是所有方法调用都需要动态链接的, 有一部分符号引用会在 类加载阶段 , 将符号引用转换为直接引用, 这部分操作称之为: 静态解析 . 就是编译期间就能确定调用的版本, 包括: 调用静态方法, 调用实例的私有构造器, 私有方法, 父类方法 。
Java 方法有两种返回方式
无论何种退出情况,都将返回至方法当前被调用的位置。 方法退出的过程相当于弹出当前栈帧 我们可以发现: 栈帧随着方法调用而创建,随着方法结束而销毁 。无论方法正常完成还是异常完成都算作方法结束. 。
本地方法栈(Native Method Stack):是线程私有的,其与虚拟机栈的作用基本是一样的,有点区别的是: 虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的 ,通过 JNI (Java Native Interface) 直接调用本地 C/C++ 库,不再受JVM控制.
JNI 类本地方法最著名的应该是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系统的特性功能,复用非 Java 代码。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力 。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 。
另外在Java虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一了 。因此对于HotSpot来说, -Xoss 参数(设置 本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由 -Xss 参数来设定.
堆(Heap)是Java虚拟机所管理的最大的一块内存区域,是被所有线程共享的, Java堆唯一的目的就是存放对象实例 , 几乎所有 的对象实例都在堆上分配内存,但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟, 栈上分配、线程本地分配缓存(TLAB)也可以存放对象实例 。
Java虚拟机规范规定, Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可 ,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常.
方法区(Methed Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。其是所有 线程共享 的内存区域.
在Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),与 Java 堆区分开来.
方法区是JVM规范的一个概念定义,并不是一个具体的实现,由于Java虚拟机对于 方法区 的限制是非常宽松的,因此也就导致了不同的虚拟机上方法区有不同的表现,我们还是以HotSpot虚拟机为例:
网上许多文章喜欢拿"永久代"或者"元空间" 来代替方法区,但本质上两者并不等价。方法区是Java虚拟机规范的概念,"永久代"或者"元空间"是方法区的2中实现方式 。
方法区在JDK7之前是一块单独的区域,HotSpot虚拟机的设计团队把GC分代收集扩展到了方法区。这样HotSpot的垃圾收集器就可以向管理Java堆一样管理这部分内存。但是对于其它虚拟机(如BEA JRockit、IBM J9等)来说其实是不存在永久代的概念的.
HotSpot的团队显然也意识到了,用永久代来实现方法区并不是一个好主意:
- 字符串存在永久代中,容易出现性能问题和内存溢出
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
因此,在JDK1.8中完全废除了“永久代”,使用元空间替代了永久代,其他内容移至元空间,元空间直接在本地内存分配.
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。元空间是使用 直接内存 实现的,我们下文再详细说.
Java内存区域大致就这些了,下面我们再补充几个比较让人迷惑的概念 。
字符串属于引用数据类型,但是可以说字符串是Java中使用频繁的一种数据类型。因此,为了节省程序内存,提高性能,Java的设计者开辟了一块叫字符串常量池的区域,用来 存储这些字符串,避免字符串的重复创建 。字符串常量池是所有类公用的一块空间,在一个虚拟机中只有一块常量池区域.
在类加载完成,经过验证,准备阶段之后在 堆中 生成字符串对象实例,然后将该字符串对象实例的引用值存到字符串常量池中(这里描述指的是JDK7及以后的HotSpot虚拟机)。 在HotSpot虚拟机中字符串常量池是通过一个StringTable类来实现的。它是一个哈希表,里面存的是字符串引用 。
在JDK7以前,字符串常量池在方法区(永久代)中,此时常量池中存放的是字符串对象。而在 JDK7及其以后 中, 字符串常量池从方法区迁移到了堆内存 ,同时将 字符串对象存到了堆内存 ,只在字符串常量池中存入了字符串对象的引用.
在JDK7 就已经开始了HotSpot 的永久代的移除工作,主要由于永久代的 GC 回收效率太低。等到JDK 8 的时候,永久代被彻底移除了 Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存.
运行时常量池(Runtime Constant Pool)是方法区的一部分。我们知道Class 文件中除了有类的版本、字段、方法、接口等常见描述信息外,但还有一项信息是 常量池 (Constant Pool Table), 用于存放编译期生成的各种字面量,符号引用还有翻译出来的直接引用 ,这部分内容将在类加载后进入方法区的运行时常量池中存放。因此, 每一个类都会有一个运行时常量池 。
因为Java语言并不要求常量一定在编译期间才能生成。也就是并非预置入Class文件常量池中的内容才能进入运行时常量池, 运行期间也可以将新的常量放入常量池中 ,运行时常量池另外一个重要特征是具备 动态性 .
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常.
JDK 8 版本之后 永久代已被元空间取代, 元空间使用的就是直接内存 。直接内存(Direct Memory)并不是Java虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域.
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据.
显然, 本机直接内存的分配不会受到 Java 堆大小的限制 ,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常.
参考资料: 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》 《On Java 8》 https://www.cnblogs.com/newAndHui/p/11168791.html https://blog.csdn.net/qq_20394285/article/details/104673913 https://www.cnblogs.com/czwbig/p/11127124.html 。
本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我! 更多精彩的文章 计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「 小牛呼噜噜 」 。
最后此篇关于Java内存区域有哪些构成?的文章就讲到这里了,如果你想了解更多关于Java内存区域有哪些构成?的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在编写一个具有以下签名的 Java 方法。 void Logger(Method method, Object[] args); 如果一个方法(例如 ABC() )调用此方法 Logger,它应该
我是 Java 新手。 我的问题是我的 Java 程序找不到我试图用作的图像文件一个 JButton。 (目前这段代码什么也没做,因为我只是得到了想要的外观第一的)。这是我的主课 代码: packag
好的,今天我在接受采访,我已经编写 Java 代码多年了。采访中说“Java 垃圾收集是一个棘手的问题,我有几个 friend 一直在努力弄清楚。你在这方面做得怎么样?”。她是想骗我吗?还是我的一生都
我的 friend 给了我一个谜语让我解开。它是这样的: There are 100 people. Each one of them, in his turn, does the following
如果我将使用 Java 5 代码的应用程序编译成字节码,生成的 .class 文件是否能够在 Java 1.4 下运行? 如果后者可以工作并且我正在尝试在我的 Java 1.4 应用程序中使用 Jav
有关于why Java doesn't support unsigned types的问题以及一些关于处理无符号类型的问题。我做了一些搜索,似乎 Scala 也不支持无符号数据类型。限制是Java和S
我只是想知道在一个 java 版本中生成的字节码是否可以在其他 java 版本上运行 最佳答案 通常,字节码无需修改即可在 较新 版本的 Java 上运行。它不会在旧版本上运行,除非您使用特殊参数 (
我有一个关于在命令提示符下执行 java 程序的基本问题。 在某些机器上我们需要指定 -cp 。 (类路径)同时执行java程序 (test为java文件名与.class文件存在于同一目录下) jav
我已经阅读 StackOverflow 有一段时间了,现在我才鼓起勇气提出问题。我今年 20 岁,目前在我的家乡(罗马尼亚克卢日-纳波卡)就读 IT 大学。足以介绍:D。 基本上,我有一家提供簿记应用
我有 public JSONObject parseXML(String xml) { JSONObject jsonObject = XML.toJSONObject(xml); r
我已经在 Java 中实现了带有动态类型的简单解释语言。不幸的是我遇到了以下问题。测试时如下代码: def main() { def ks = Map[[1, 2]].keySet()
一直提示输入 1 到 10 的数字 - 结果应将 st、rd、th 和 nd 添加到数字中。编写一个程序,提示用户输入 1 到 10 之间的任意整数,然后以序数形式显示该整数并附加后缀。 public
我有这个 DownloadFile.java 并按预期下载该文件: import java.io.*; import java.net.URL; public class DownloadFile {
我想在 GUI 上添加延迟。我放置了 2 个 for 循环,然后重新绘制了一个标签,但这 2 个 for 循环一个接一个地执行,并且标签被重新绘制到最后一个。 我能做什么? for(int i=0;
我正在对对象 Student 的列表项进行一些测试,但是我更喜欢在 java 类对象中创建硬编码列表,然后从那里提取数据,而不是连接到数据库并在结果集中选择记录。然而,自从我这样做以来已经很长时间了,
我知道对象创建分为三个部分: 声明 实例化 初始化 classA{} classB extends classA{} classA obj = new classB(1,1); 实例化 它必须使用
我有兴趣使用 GPRS 构建车辆跟踪系统。但是,我有一些问题要问以前做过此操作的人: GPRS 是最好的技术吗?人们意识到任何问题吗? 我计划使用 Java/Java EE - 有更好的技术吗? 如果
我可以通过递归方法反转数组,例如:数组={1,2,3,4,5} 数组结果={5,4,3,2,1}但我的结果是相同的数组,我不知道为什么,请帮助我。 public class Recursion { p
有这样的标准方式吗? 包括 Java源代码-测试代码- Ant 或 Maven联合单元持续集成(可能是巡航控制)ClearCase 版本控制工具部署到应用服务器 最后我希望有一个自动构建和集成环境。
我什至不知道这是否可能,我非常怀疑它是否可能,但如果可以,您能告诉我怎么做吗?我只是想知道如何从打印机打印一些文本。 有什么想法吗? 最佳答案 这里有更简单的事情。 import javax.swin
我是一名优秀的程序员,十分优秀!