你必须了解的java内存管理机制(一)-运行时数据区-程序员宅基地

技术标签: 运维  内存管理  c/c++  

前言

  本打算花一篇文章来聊聊JVM内存管理机制,结果发现越扯越多,于是分了四遍文章(文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8),本文为其中第一篇。from 你必须了解的java内存管理机制-运行时数据区
  相关链接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8,个人技术博客www.17coding.info
  1、 你必须了解的java内存管理机制-运行时数据区
  2、 你必须了解的java内存管理机制-内存分配
  3、 你必须了解的java内存管理机制-垃圾标记
  4、 你必须了解的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控制,也不会发生永久代溢出的异常。

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

 

转载于:https://www.cnblogs.com/sujing/p/10207121.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34417183/article/details/94256877

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文