Java内存模型和内存区域

为什么要学习JVM?

在虚拟机自动内存管理机制的帮助下,我们不需要向C/C++一样为每个new操作去写配对的delete/free代码释放内存,也就不容器出现内存泄漏内存溢出问题,但是一旦出现这两种问题,如果不了解虚拟机怎么使用内存的,就不容易追根溯源,解决问题。所以说Java与C++之间存在一堵由内存动态分配垃圾收集技术构成的围墙,墙里人想出来,墙外人想进来。

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

内存泄漏(memory leak)会最终会导致内存溢出(out of memory)!

内存溢出和内存泄漏 ->原文链接:https://blog.csdn.net/buutterfly/article/details/6617375

Java内存模型

1、前言:Cache的由来

img

从这张图我们可以看出

  • 主存<=>辅存 :为了解决存储系统容量的问题
  • CPU<=>Cache:为了解决CPU和主存速度不匹配的问题

Cache就是高速缓冲存储器,现代Cache通常设为三级Cache,按离CPU远近可命名为L1 Cache,L2 Cache,L3 Cache。离CPU越远,访问速度越慢,容量越大。

但是修改Cache内存后,如何保持主存中的内容一致性??

在《计算机组成原理》这门课程中这样介绍:

缓存命中:

  • 写回法:当CPU对Cache写命中时,只修改Cache的内容,而不立即写入主存,只有当此块被换出时才写回主存。

  • 全写法:当CPU对Cache写命中时,必须把数据同时写入Cache和主存,一般使用写缓冲(write buffer)。

缓存未命中:

  • 写分配法:把住存中的块调入Cache,在Cache中修改。搭配写回法使用。

  • 非写分配法:只写入主存,不调入Cache。搭配全写法使用。

2、主题:Java内存模型

JMM(Java Memory Model )Java内存模型

Java虚拟机是整个计算机的模型,因此这个模型自然包括一个内存模型——也就是Java内存模型。Java内存模型是由Java虚拟机规范定义的,用来屏蔽各种硬件和操作系统对内存访问的差异,指明了Java虚拟机如何使用计算机的内存(RAM),解决了多线程并发操作共享变量时带来的主存数据不一致的问题。

Java内存模型保证多线程之间操作共享变量的正确性。(正确性就是通过保证不同CPU操作的可见性保证指令的有序性保持主存和缓存中数据的一致性

img

Java内存区域

**Java内存区域:是指JVM在执行Java程序过程中会把它所管理的内存划分为若干个不同的数据区域,通常称为运行时数据区域。**这些区域都有各自的用途和生命周期。

JDK 1.7之前:

未命名文件

JDK1.8及以后:方法区被元数据区替代。

JDK 1.7 其实是并没完全移除方法区,但是不会像1.6以前报 “java.lang.OutOfMemoryError: PermGen space”,而是报 java.lang.OutOfMemoryError: Java heap space

1.7部分内容(比如 常量池、静态变量有方法区转移到了堆)

img

那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。
  2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量移至 Java Heap

线程共享和线程隔离区域

1、线程私有的数据区域

1.1 程序计数器

程序计数器是一块很小的内存空间,可以看作的当前线程所执行字节码的行号指示器。

在《微机原理》介绍程序计数器是记录着下一条将要执行的指令地址

如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器为空(Undefined)。此内存区域是为一个在Java虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

为什么需要程序计数器呢?

为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。因此程序计数器这块内存空间又是线程私有的。

1.2 Java虚拟机栈

我们经常会把java内存粗糙的分为两个部分,堆和栈,Java虚拟机栈就是栈这一部分,或者说是虚拟机栈中局部变量表部分。跟程序计数器一样,虚拟机栈也是线程私有的,它的生命周期跟线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),也就是每个栈帧对应一个被调用的方法;栈帧是一种数据结构,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。Java虚拟机运行时栈帧结构

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

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常
img
1.2.1 局部变量表

顾名思义,局部变量表就是存储局部变量方法形参的区域。具体就是编译器可知的八大基本数据类型、对象引用和ReturnAddress类型。

  • 八大基本数据类型:boolean(1 字节)、byte(1字节)、char(2字节)、short(2字节)、int(4字节)、long(8字节)、float(4字节)、double(8字节)

  • 对象引用:不是指对象本身。而是指向对象起始地址的引用指针,也可以是指向一个代表对象的句柄。

  • ReturnAddress类型:指向了一条字节码指令的地址

由于64位长度(8字节)的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型占据一个。因此局部变量表所需的内存空间在编译期间已经完成分配,当进入一个方法时,此方法对应的栈帧中的局部变量表分配的变量空间是确定的,并且在运行期间不会改变

局部变量与成员变量的对比

  • 参数表分配完毕以后,在根据方法体内定义的变量的顺序和作用域分配
  • 成员变量有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次是在“初始化”阶段,赋予变量在代码中定义的初始值。
  • 和成员变量初始化不同的是,局部变量表不存在系统初始化过程中,这就意味着一旦定了局部变量则必须进行初始化,否则无法使用。
1.2.2 操作数栈

操作数栈,也称为表达式栈主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间

存储运算方式:和局部变量表一样,操作数栈也是呗组织成一个以字节为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。

数据类型存储,虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的,对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。

1.2.3 动态链接

在一个方法里调用另一个方法,需要知道另一个方法的名字。这个名字相当于符号引用。符号引用存放class常量池中,在类加载后会进入方法区的运行时常量池。在类加载的解析阶段会把符号引用换成直接引用,这个过程叫做动态链接。动态链接的前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现

1.2.4 方法出口

方法出口就是方法返回地址:当一个方法执行结束后,要返回到之前调用它的地方,因此在栈帧中需要保存一个方法返回地址。

1.3 本地方法栈

本地方法栈和虚拟机栈作用相似,他们之间区别只不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机使用到的Native 方法(调用本地C/C++库)服务。

本地方法栈区域与虚拟机栈一样,也会抛出StackOverflowError异常OutOfMemoryError 异常

2、线程共享的数据区域

2.1 Java堆

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

堆是垃圾收集器管理的主要区域,又称为“GC堆”,可以说是Java虚拟机管理的内存中最大的一块。

现在的虚拟机(包括HotSpot VM)都是采用分代回收算法,这种算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。所以堆还可以细分为:新生代+老年代,新生代 又细分为 Eden + From Survivor + To Survivor区。

img

堆空间大小参数:

  • -Xms: 初始堆大小
  • -Xmx: 最大堆大小
  • -XX:NewSize=n: 设置年轻代大小
  • -XX:NewRatio=n: 设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
  • -XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如: 3,表示 Eden:Survivor=3: 2,一个 Survivor 区占整个年轻代的 1/5。

2.2 方法区

跟Java堆一样,方法区是各个线程共享的内存区域,此区域是用来存储已被虚拟机加载的类信息(即类的版本、字段、方法、接口等信息)、静态变量、常量以及编译器编译后的代码

JVM规范中并不区分方法区和堆,只把方法区描述为堆的逻辑部分,但是它却有一个别名叫做非堆(Non-Heap),目的就是与Java堆区分开。

很多人习惯把方法区称为“永久代”,本质上两者不等价的;实际上是使用永久代来实现方法区而已,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器。永久代是HotSpot虚拟机特有的概念,是对方法区的实现,别的JVM没有永久代的概念

2.2.1 运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期间生成的各种字面量(字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量))和符号引用,这部分内容会在类加载后进入方法区的运行时常量池中。

符号引用是什么?

符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

一个 java 类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址,及直接引用地址。

即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址,以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。

Class常量池和运行时常量池:

  • Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行,但是对运行时常量池缺没有任何细节的要求。
  • 运行时常量池相对于Class文件常量池的另外一个重要特征就是动态性,即不要求常量一定是编译期才能产生,运行期间也能将新的常量放入池中。这种动态性体现在String类的intern()方法

关于方法区和元空间的关系:

方法区是JVM规范概念,而永久代则是Hotspot虚拟机特有的概念,简单点理解:方法区和堆内存的永久代其实一个东西,但是方法区是包含了永久代。
只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”

常量池

https://blog.csdn.net/zxm490484080/article/details/81181688

https://blog.csdn.net/chenkaibsw/article/details/80848069

https://blog.csdn.net/qq_26222859/article/details/73135660

2.3 元空间

元空间是一块与堆不相连的本地内存。原本存在永久代的数据,一部分移到了java堆里面,一部分移到了本地内存里面(即元空间)(文档中原句:Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.) 。永久代中原来存储的字符串常量(池)、符号引用(这两个在jdk7普遍就已经将其放在堆上了)和类的静态变量现在存储在java堆中,其余的数据作为元数据存储在元空间中。

元空间大小参数:

  • jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
  • jdk1.8 以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
  • jdk1.8 以后大小就只受本机总内存的限制(如果不设置参数的话)

3、延伸

Class对象

Class对象是存放在堆区的,不是方法区。 Class对象是加载的最终产品,类的元数据, 包括类的方法代码,变量名,方法名,访问权限,返回值等等才是存在方法区的。

  • hotpot java虚拟机Class对象是放在 方法区 还是堆中 ? https://www.zhihu.com/question/38496907/answer/156793201

静态变量(class statics)

  • 在 JDK7.0 版本,类的静态变量(class statics)移到了 堆(Heap) 中

    • JDK-7017732 : move static fields into Class to prepare for perm gen removal : https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7017732

字面量(interned strings)

Hotspot 的 字面量(interned strings) (或叫字符串常量池, 字符串池,字符串对象池,string poolstring literal poolStringTable) 在 Java 内存区域的哪个位置

  • 在 JDK6.0 及之前版本,字符串常量池是放在 永久代 (Perm Generation) 区 中,此时常量池中存储的是对象。
  • 在 JDK7.0 版本,字符串常量池被移到了 堆(Heap) 中了。此时常量池存储的还可以是引用。在 JDK8.0 中,永久代 (Perm Generation) 被 元空间 (Metaspace)取代了。
  • https://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html 字符串常量池被分配到了Java堆的主要部分(known as the young and old generations)。也就是字符串常量池从运行时常量池分离出来了。

8张图 让你明白 Java内存区域 - 知乎 (zhihu.com)

详解JVM中的五大内存区域 - 编程开发分享者 - 博客园 (cnblogs.com)

深入理解JVM-内存模型(jmm)和GC - 简书 (jianshu.com)

JVM运行时数据区(<=JDK7 and JDK8+) - 云+社区 - 腾讯云 (tencent.com)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值