java内存管理机制详解之运行时数据区

正文

C++与java之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙里的人却想出来……

与C、C++程序员时刻要关注着内存的分配与释放,会不会又有哪里出现了内存泄露不同是,java程序员可以“高枕无忧”。因为这一切都已经有jvm来帮我们管理了,java程序员只需要关注具体的业务逻辑就可以了,至于内存分配与回收,交给jvm去干吧。但这样也带来一个问题,我们不再去关注内存分配了,不再去关注内存回收了。一旦出现内存泄露就束手无策了,在不同的应用场景,怎么样去做性能调优就成了一个问题。所以,对于java程序员来说,这些是必须了解的一部分。

没有对象怎么办?new一个啊。单身狗程序员每次提到new对象都激动不已,可是你的对象是怎么new出来的?new出来又放在哪里?怎么引用的?你的对象被别人动了怎么办?使用完成之后又是如何释放的?何时释放的?等等等等这些问题,如果你不能很轻松的回答出来,那么在本系列文章中你可能会找到一些答案。当然,本人才疏学浅,文笔拙劣,只是抛砖引玉,理解不周到或者有误的地方,欢迎拍砖。

JVM内存区域可以大致划分为“线程隔离区域”和“线程共享区域”。所谓“线程隔离区域”即线程非共享区域,每个线程独享的,执行指令操作机存放私有数据。不管做什么操作,不会影响到其他线程。可以想象成,你个人电脑硬盘中的苍老师,只能你一个人在夜深人静的时候拉上窗帘独自享受,别人无法同你分享,你删除或者新下载也不会对别人造成影响。而“线程共享区域”则是所有的线程共同拥有的,主要存放对象实例数据。如果A线程对这块区域的某个数据进行了修改,而刚好B线程正在使用或者需要使用该数据,则A线程对数据的修改在B线程中也会得到体现。可以想象成你把苍老师传到了某社区,这时候网上其他人都能共享你的苍老师了。当大家看得正兴奋的时候,你突然删掉了你上传的老师,这时候大家都只能去寻找新的素材了………,不知道你是否对“线程隔离区域”和“线程共享区域”的概念有了个大致了解。在jvm中,线程隔离区域包含程序计数器、本地方法栈、虚拟机栈。线程共享区域包含堆区、永久代(jdk1.8中废除永久代)、直接内存(jdk1.8中新增)

一、这是我的私人住所,我不同意,你们别来!-线程隔离区域

线程隔离区域存放什么数据呢?局部变量、方法调用的压栈操作等。线程隔离区域包含巴拉巴拉……

1、睡了一觉,刚刚我做到哪了?-程序计数器

我们都知道在多线程的场景下,会发生线程切换,如果当前执行的线程让出执行权,则线程会被挂起,当线程再次被唤醒的时候,如果没有程序计数器线程可能就懵逼了,我是谁?我在哪?我要做什么?。但是如果有了程序计数器,线程就能找到上次执行到的字节码的位置继续往下执行。程序计数器可以理解为当前线程正在执行的字节码指令的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

查阅了一些资料,列出了程序计数器的三个特点,这里也列举一下

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

2)、如果正在执行的是Native 方法,则这个计数器值为空(Undefined)。因为Native方法大多是通过C实现并未编译成需要执行的字节码指令。那native 方法的多线程是如何实现的呢? native 方法是通过调用系统指令来实现的,那系统是如何实现多线程的则 native 就是如何实现的。Java线程总是需要以某种形式映射到OS线程上,映射模型可以是1:1(原生线程模型)、n:1(绿色线程 / 用户态线程模型)、m:n(混合模型)。以HotSpot VM的实现为例,它目前在大多数平台上都使用1:1模型,也就是每个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原生平台直接执行,并不需要理会抽象的JVM层面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎样就是怎样。就像一个用C或C++写的多线程程序,它在线程切换的时候是怎样的,Java的native方法也就是怎样的。
  3)、此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域(程序运行过程中计数器中改变的只是值,而不会随着程序的运行需要更大的空间)

2、自己的事情自己做!-虚拟机栈

这个区域就是我们经常所说的栈,是java方法执行的内存模型,也是我们在开发中接触得很多的一块区域。虚拟机栈存放当前正在执行方法的时候所需要的数据、地址、指令。每个线程都会独享一块栈空间,每次方法调用都会创建一个栈帧,栈帧保存了方法的局部局部变量、操作数栈、动态链接、出口等信息。栈帧的深度也是有限制的,超过限制会抛出StackOverflowError异常。

我们结合一个例子来了解一下虚拟机栈和栈帧,我们有如下代码:

public class myProgram {
public static void main(String[] args) {
String str = "my String";
methodOne(1);
}

public static void methodOne(int i) {
int j = 2;
int sum = i + j;

// ......
methodTwo();
// .....
}

public static void methodTwo() {

if (true) {
int j = 0;
}

if (true) {
int k = 1;
}

return;
}
}

代码很简单,main调用methodOne,methodOne调用methodTwo,如果当前正在执行methodTwo方法,则虚拟机栈中栈帧的情况应该是如下图情况,栈顶为正在执行的方法。

我们能看到,每个栈帧都包含局部变量表,操作数栈、动态链接、返回地址等……

1)、局部变量表

顾名思义,局部变量表就是存放局部变量的表,局部变量包括方法形参、方法内部定义的局部变量。局部变量表由多个变量槽(slot)组成,每个槽位都有个索引号,索引的范围是从0开始至局部变量最大的slot空间,虚拟机就是通过索引定位的方式使用局部变量表。比如在methodOne方法中,形参i就是在0号索引的slot中,局部变量j就放在1号索引的slot中,我们看看结合methodOne方法的字节码进行分析(通过javap -verbose myProgram查看字节码文件)。

public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9

  • 0:加载int类型常量2
  • 1:存储到索引为1的变量中(这里指源程序中的j)
  • 2:加载索引为0的变量(这里指源程序中的i)
  • 3:加载索引为1的变量(这里指源程序中的j)
  • 4:执行add指令
  • 5:将执行结果存储到索引为2的变量中(这里指源程序中的sum)
  • 6:静态调用

需要注意的一点是,为了尽可能节省栈帧的空间,局部变量表中的slot是可以重用的,方法体重定义的变量,其作用域不一定会覆盖整个方法体,我们看看methodTwo的源码,第一个if和第二个if的作用域不一样,所以内部变量可能是用的同一个slot,我们可以通过methodTwo方法的字节码来验证一下

public static void methodTwo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iconst_1
3: istore_0
4: return
LineNumberTable:
line 19: 0
line 23: 2
line 26: 4

你看,我没骗你吧,methodTwo方法两个if中的变量j和k,使用的都是索引为0的slot。这样的设计可以节省栈帧的空间,同时也会影响jvm的垃圾回收,因为局部变量表是GC Root的一部分,局部变量表slot中当前存放的变量关联的对象为可达对象(后面讲到垃圾回收时候再详细讲)。

2)、操作数栈

操作数栈也是一个栈,也看可以成为表达式栈。操作数栈和局部变量表在访问方式上有着较大的差异,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。我们对变量的操作都是在操作数栈中完成的,我们依然拿methodOne方法来举例。再看一下methodOne方法的字节码:

public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9

3)、动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。刚开始看这一段的时候总是觉得很生涩,比较拗口。我们还是继续看那段代码的字节码文件,其中有一段叫做“Constant pool”,里面存储了该Class文件里的大部分常量的内容(包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符)。

不知道你有没有注意我们字节码中是怎么处理menthodOne方法的调用的?在main方法中调用methodone方法的字节码为invokestatic #3,这里的#3就是一个” 符号引用”,我们发现#3还引用着另外的常量池项目,顺着这条线把能传递到的常量池项都找出来(标记为Utf8的常量池项)。由此我们可以看出,invokestatic 指令就是以常量池中指向方法的符号引用作为参数,完成方法的调用。这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。我们看一下字节码中的常量池和符号引用,注意main方法中的#2 #3:

Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = String #19 // my String
#3 = Methodref #5.#20 // myProgram.methodOne:(I)V
#4 = Methodref #5.#21 // myProgram.methodTwo:()V
#5 = Class #22 // myProgram
#6 = Class #23 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 methodOne
#14 = Utf8 (I)V
#15 = Utf8 methodTwo
#16 = Utf8 SourceFile
#17 = Utf8 myProgram.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Utf8 my String
#20 = NameAndType #13:#14 // methodOne:(I)V
#21 = NameAndType #15:#8 // methodTwo:()V
#22 = Utf8 myProgram
#23 = Utf8 java/lang/Object

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String my String
2: astore_1
3: iconst_1
4: invokestatic #3 // Method methodOne:(I)V
7: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7

4)、返回地址

我们的经常使用return x;来使方法返回一个值给方法调用者,如果没有返回值的方法也可以在方法的方法需要返回的地方加上return;当然,这不是必须的,因为源码在转化为字节码的时候,总是会在方法的最后加上return指令,不信你看上面methodTwo方法的字节码那张图片。

正常情况下,方法遇到返回指令退出,这种退出方法的方式称为正常完成出口。如果方法正常返回,则当前栈帧从java栈中弹出,恢复发起调用者的方法的栈帧,如果方法有返回值,jvm会把返回值压入到发起调用方法的操作数栈。但是在异常情况下,方法执行遇到了异常,且这个异常在方法体内未得到处理,方法则会异常退出,这种退出方式称为异常完成出口。当异常抛出且没有被捕捉时,则方法立即终止,然后JVM恢复发起调用的方法的栈帧,如果在调用者中也未对异常进行捕捉,则调用者也会立即终止,层层向上,直到最外层抛出异常。

3、楼上做不了的事情,来我这做!-本地方法栈

本地方法是什么?本地方法就是在jdk中(也可以自定义)那些被Native关键字修饰的方法(下图)。这类方法有点类似java中的接口,没有实现体,但实际上是由jvm在加载时调用底层实现的,实现体是由非java语言(如C、C++)实现的,所以本地方法可以理解为连接java代码和其他语言实现的代码的入口。而本地方法栈的功能就类似于虚拟机栈,只是一个服务于java方法执行,一个服务于执行本地方法执行。

 

二、来啊,快活啊!反正有大把空间!-线程共享区域

1、 喂,你的对象都在这里!-堆

堆区域在jvm中是非常重要的一块区域,因为我们平常创建的对象的实例就存在在这个区域,这个区域的几乎是被所有线程共享。同时也是java虚拟机管理的内存中最大的一块。由于目前主流的垃圾收集器都采用分代收集算法,所以通常将堆细分为新生代、老年代,新生代又分为两块Eden区、From Survivor区、To Survivor区(这里主要针对通常使用的分代收集器,G1收集器采用不同的划分策略,后面有机会再讲)。不过不管怎么划分,目的都是为了更合理的利用内存,提高内存空间使用率,提高垃圾回收的效率和回收质量。

我们在这篇文章里只谈堆区内存的划分,关于内存分配、内存回收等会在下篇文章细讲,因为涉及的内容太多了……不过我们可以先思考几个问题1、为什么需要区分新生代、老年代?2、为什么将新生代分为Eden、Survivor区?各区大小怎么分配?有什么分配依据?

2、 治不了你?那我就废了你!-方法区

看标题可能会有些误解,其实这里废除的是永久代的概念,而不是方法区。刚开始总是搞不清这两者的关系,后来就去查阅了一些资料总算是搞清楚了一些,书上是这么说的:“JVM的虚拟机规范只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。不同JVM的方法区的实现会不一样,比如在HotSpot中使用永久代实现方法区,其他JVM并没有永久代的概念。方法区是一种规范,永久代是一种实现。”

所以,我们常说的新生代、老年代、永久代中的永久代就是方法区的一种实现,且只存在于HotSpot虚拟机中有这种概念。用过jdk1.8之前的版本(HotSpot虚拟机)的同学应该经常能碰到永久代溢出的异常“java.lang.OutOfMemoryError: PermGen space”,这里的PermGen space指的是永久代。在jdk6中,永久代包含方法区和常量池,但是在jdk1.7的版本中规划去除永久代,于是在1.7中将常量池移到了老年代中。在jdk1.8中彻底废除了永久代,取而代之的是元空间。

3、 会有天使替我去爱你!-直接内存

永久代设置太大吧,浪费资源!永久代设置太小吧,溢出了!于是让人恼火的永久代溢出的异常时常发生,并且永久代的GC效率低下,于是,在jdk1.8中彻底废除了永久区,放到了直接内存的元空间中!元空间的本质和永久代类似,都是对JVM规范中方法区的实现。元空间相比永久代有什特性呢?永久代在物理上是堆的一部分,与新生代老年代的地址是连续的,而元空间属于本地内存,不受JVM控制,也不会发生永久代溢出的异常。

直接内存也可以称为堆外内存,为什么要将方法区放入到直接内存呢?

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • 类及方法的信息等比较难确定其大小,因此永久代调优较为困难,容易发生内存溢出。
  • 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/768602.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio 中的键盘快捷方式

1. Visual Studio 中的键盘快捷方式 1.1. 可打印快捷方式备忘单 1.2. Visual Studio 的常用键盘快捷方式 本部分中的所有快捷方式都将全局应用&#xff08;除非另有指定&#xff09;。 “全局”上下文表示该快捷方式适用于 Visual Studio 中的任何工具窗口。 生成&#xff1…

【C语言】指针经典例题

题1&#xff1a; #include <stdio.h>int main() {int a[5] { 1, 2, 3, 4, 5 };int* ptr (int*)(&a 1);printf("%d,%d", *(a 1), *(ptr - 1));return 0; } //程序的结果是什么&#xff1f; 解答如下&#xff1a; 题2&#xff1a; #include <std…

Access数据操作

Access Access 作为 Office的组件之一&#xff0c;在很多 Excel难以施展其能力的场所&#xff0c;也能轻松应对。同为Office组件之一的Excel具有灵活的数据处理和分析能力&#xff0c;然而&#xff0c;其能力是有局限的&#xff0c; 比如当涉及两个数据表之间的“关联”操作时&…

【分布式数据仓库Hive】HivQL的使用

目录 一、Hive的基本操作 1. 使用Hive创建数据库test 2. 检索数据库&#xff08;模糊查看&#xff09;&#xff0c;检索形如’te*’的数据库 3. 查看数据库test详情 4. 删除数据库test 5. 创建一个学生数据库Stus&#xff0c;在其中创建一个内部表Student&#xff0c;表格…

快速下载!Windows 7旗舰版系统:集成所有补丁!

微软对Windows7系统停止支持后&#xff0c;Windows7设备不再收到安全补丁程序、修补程序。尽管如此&#xff0c;许多用户仍然认为Windows7是最好用、最经典的系统。有用户就特别喜欢Windows7旗舰版系统&#xff0c;那么接下来系统之家小编为大家带来的全补丁版本的Windows7系统…

互联网应用主流框架整合之SpringCloud微服务治理

微服务架构理念 关于微服务的概念、理念及设计相关内容,并没有特别严格的边界和定义,某种意义上说,适合的就是最好的,在之前的文章中有过详细的阐述,微服务[v1.0.0][Spring生态概述]、微服务[设计与运行]、微服务[v1.0.0][服务调用]、微服务[开发生命周期]、微服务[面临的…

LLM应用:传统NLP任务

LLM出来以后&#xff0c;知乎上就出现了“传统NLP已死”的言论&#xff0c;但是传统NLP真的就被扔进历史的垃圾桶了吗&#xff1f; 其实&#xff0c;尽管LLM具有出色的通用能力&#xff0c;但仍然无法有效应对低资源领域的自然语言处理任务&#xff0c;如小语种翻译。为了更好地…

springboot+vue+mybatis前台点菜系统+PPT+论文+讲解+售后

21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存储达到…

Linux静态库的制作

Linux操作系统支持的函数库分为&#xff1a; 静态库&#xff0c;libxxx.a&#xff0c;在编译时就将库编译进可执行程序中。 优点&#xff1a;程序的运行环境中不需要外部的函数库。 缺点&#xff1a;可执行程序大 动态库&#xff0c;又称共享库&#xff0c;libxxx.so&a…

【目标检测】DINO

一、引言 论文&#xff1a; DINO: DETR with Improved DeNoising Anchor Boxes for End-to-End Object Detection 作者&#xff1a; IDEA 代码&#xff1a; DINO 注意&#xff1a; 该算法是在Deformable DETR、DAB-DETR、DN-DETR基础上的改进&#xff0c;在学习该算法前&#…

一个专为Android平台设计的高度可定制的日历库

大家好&#xff0c;今天给大家分享一个高度可定制的日历库kizitonwose/Calendar。 Calendar专为Android平台设计&#xff0c;支持RecyclerView和Compose框架。它提供了丰富的功能&#xff0c;允许开发者根据需求定制日历的外观和功能。 项目介绍 此库是开发Android应用时&…

【计算机网络仿真】b站湖科大教书匠思科Packet Tracer——实验14 聚合了不存在的网络导致的路由环路问题

一、实验目的 1.验证由于聚合了不存在的网络而导致静态路由的路由环路问题&#xff1b; 二、实验要求 1.使用Cisco Packet Tracer仿真平台&#xff1b; 2.观看B站湖科大教书匠仿真实验视频&#xff0c;完成对应实验。 三、实验内容 1.构建网络拓扑&#xff1b; 2.验证路由…

【最长公共前缀 动态规划】2430. 对字母串可执行的最大删除数

如果有不明白的&#xff0c;请加文末QQ群。 本文涉及知识点 最长公共前缀 动态规划 动态规划汇总 LeetCode 2430. 对字母串可执行的最大删除数 给你一个仅由小写英文字母组成的字符串 s 。在一步操作中&#xff0c;你可以&#xff1a; 删除 整个字符串 s &#xff0c;或者 …

基于jeecgboot-vue3的Flowable流程-集成仿钉钉流程(一)一些样式的调整使用

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、比如下面的发起人双击后出现的界面不正常&#xff0c; 看它的样式主要是这个里面的margin-left应该太小了&#xff0c; [data-v-45b533d5] .el-tabs__content { margin-top: 50px;mar…

EE架构大跃进:特斯拉、小鹏引领舱驾融合,从域控融合走向单SoC

作者 |肖恩 编辑 |德新 智能汽车发展到今天&#xff0c;整车电气架构已经从分布式架构逐渐迈向中央集成式架构&#xff0c;传统的小控制器被集成到按功能划分的大域控里&#xff0c;下一个阶段将是跨域的融合&#xff0c;通过不同功能域的集成实现中央计算平台的最终目标。 …

Linux动态库的制作

Linux操作系统支持的函数库分为&#xff1a; 静态库&#xff0c;libxxx.a&#xff0c;在编译时就将库编译进可执行程序中。 优点&#xff1a;程序的运行环境中不需要外部的函数库。 缺点&#xff1a;可执行程序大 动态库&#xff0c;又称共享库&#xff0c;libxxx.so&#…

QAM MMA

MMA是改进的CMA&#xff0c;有RCA和CMA的优点&#xff0c;还能对相位误差进行修正。 N 5e5; % 仿真符号数 M 16; % QAM16msg randi([0 M-1],N,1); % 产生随机符号 tx qammod(msg,M); % QAM调制test_snr 20:5:30; …

Springboot 校园安全通事件报告小程序系统-计算机毕业设计源码02445

Springboot 校园安全通事件报告小程序系统 摘 要 随着中国经济的飞速增长&#xff0c;消费者的智能化水平不断提高&#xff0c;许多智能手机和相关的软件正在得到更多的关注和支持。其中&#xff0c;校园安全通事件报告小程序系统更是深得消费者的喜爱&#xff0c;它的出现极大…

PyPDF2拆分PDF文件的高级应用:指定拆分方式

本文目录 前言一、拆分方式选择1、代码讲解2、实现效果图3、完整代码前言 前两篇文章,分别讲解了将使用PyPDF2将PDF文档分割成为单个页面、在分割PDF文档时指定只分割出指定页面,如果你还没有看过,然后有需要的话,可以去看一下,我把文章链接贴到这里: PyPDF2拆分PDF文件…

近红外光谱脑功能成像(fNIRS):1.光学原理、变量选取与预处理

一、朗伯-比尔定律与修正的朗伯-比尔定律 朗伯-比尔定律 是一个描述光通过溶液时被吸收的规律。想象你有一杯有色液体&#xff0c;比如一杯红茶。当你用一束光照射这杯液体时&#xff0c;光的一部分会被液体吸收&#xff0c;导致透过液体的光变弱。朗伯-比尔定律告诉我们&#…