JVM内存模型

JAVA内存模型(JAVA Memory Model)

1 CPU与内存的交互

由于内存的读写速度远远赶不上CPU,因此CPU在每颗CPU上加上高速缓存,虽然这样缓解了处理器和CPU的矛盾,但是引来了新的问题-缓存一致性
在多核CPU中,每个处理器都有起各自的高速缓存(High Speed cache memory,存在多个等级L1,L2,L3),而主存只有一个。
CPU读取要一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存中或者内存中查找,每个CPU有且仅有一套自己的缓存。

在CPU层面,内存屏障提供了个充分必要条件。
内存屏障(Memory Barrier)
每个CPU有自己的多级缓存,因为这些缓存,提高了数据访问性能,避免每次都向内存中读取数据,但是导致了不能够实时地和内存进行信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

硬件层的内存屏障分为两种:读屏障(Load Barrier)和写屏障(Store Barrier),内存屏障是硬件层的。

为什么需要内存屏障

由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题.
简单来说:
1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。
2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。
对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。

内存屏障的作用

CPU执行指令可能是无序的,它有两个比较重要的作用
1.阻止屏障两侧指令重排序
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

volatile的特性

可见性:对于一个volatile变量的读,总是能看到任意线程对这个变量的最后的修改
有序性:对于存在指令重排序的情况,volatile会禁止部分指令重排序

2 Java内存区域

Java内存模型就是一种符合内存规范的,屏蔽了各种硬件和操作系统访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

Java程序的内存分配是在jvm虚拟机内存分配机制下完成。
在这里插入图片描述
JVM内存区域主要分为线程私有区域(程序计数器,虚拟机栈,本地方法区),线程共享区(java堆,方法区),直接内存。

线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束,而创建/销毁在Hotspot VM内,每个线程都与操作系统的本地线程直接映射,因此这部分的内存区域的存/否跟随本地线程的生/死相对应。

线程共享区域随虚拟机的启动/关闭而创建/销毁。

直接内存并不是JVM运行时数据区的一部分,但是会被频繁使用:在JDK1.4引入的NIO提供了基于Channel与Buffer的IO方式,它可以使用Native函数库直接分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用进行操作,这样就避免了Java堆和Native堆中来回复制数据,因此在一些场景中可以显著提高性能。

根据java虚拟机规范,java数据区域分为五大数据区域:
在这里插入图片描述

2.1 Java内存区域
2.1.1 程序计数器

程序计数器是一块很小的内存空间,是当前线程所执行的字节码的信号指示器,它是线程私有的,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

为什么需要程序计数器

对于一个处理器(对于多核CPU那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确的执行位置,每个线程都需要有独立的一个程序计数器,不同线程之间的程序计数器之间互不影响, 独立存储。
如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果未native方法,那么计数器为空。这块区域是虚拟机规范中唯一没有OutOfMemory的区域

2.1.2 Java栈(虚拟机栈)

线程私有,也就是我们平时说的栈,栈描述的是Java方法执行的内存模型。

每个方法被执行时都会创建一个栈用于存储局部变量,操作栈,动态链表,方法出入口信息。每一个方法被调用的过程就对应一个栈帧在虚拟机中入栈到出栈的过程。
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking),方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁,无论是方法是正常还是异常完成,都算作方法结束。

2.1.3 Native方法(本地方法)

本地方法区和java Stack作用类似,区别是虚拟机栈为执行java方法服务,而本地方法栈为Native方法服务。如果一个VM实现使用C-linkage模型来支持Native调用,那么该栈将会是一个C栈,但HotSpot VM直接就把本地方栈和虚拟机栈合二为一。

2.1.4 堆(Heap)

创建的对象和数组都保存在java堆内存中,堆是java虚拟机管理内存最大的一块内存区域,也是垃圾收集器进行垃圾收集的最重要的内存区域。因为堆存放的对象时线程共享的,所以多线程的时候也需要同步机制。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。
逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

2.1.5 方法区/永久代

用于存储被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码
在老版jdk,方法区也被称为永久代,因为没有强制要求方法区必须实现垃圾回收,HosSpot VM把GC分代收集扩展方法区,即使用java堆的永久代来实现方法区,这样HotSpot VM的垃圾收集器就可以像管理java堆一样管理这部分内存,而不必要未方法区专门开发内存管理器(永久代的内存回收主要目标是针对常量池的回收和类型的卸载,因此受益一般很小)。不过从jdk7之后,HotSpot虚拟机便将运行常量池从永久代中移除了。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版 本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加 载后存放到方法区的运行时常量池中。 Java虚拟机对Class文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会 被虚拟机认可、装载和执行。

jdk8真正开始废弃永久代,而使用元空间(MetaSpace)。元空间的本质可永久代类似,其最大区别在于:元空间并不在虚拟机中,而是使用本地内存。因此元空间的大小受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再有MaxPermSize控制
,而是由系统实际可用空间来控制。

java虚拟机堆方法区的管理比较宽松,除了和堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾回收。

2.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储布局分为

1.对象头
2.实例数据
3.对齐填充

对象头:在32位系统,对象头8字节,64位则是18个字节(未开启压缩指针,开启后12字节)

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。
为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息。
假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

实例数据
存放对象程序中各种类型的字段类型,不管是从父类中集成下来的还是在子类中定义的。
分配策略:相同宽度的字段总是放在一起。比如double和long。

对齐策略
这部分无特殊含义,仅仅起到占位符的2作用满足JVM要求。
由于HotSpot规定对象是大小必须是8的整倍数,对象头刚好是整倍数如果实例2数据不是的话,就需要占位符对齐填充。

2.3 对象的访问定位

java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式
1.句柄访问对象
2.直接指针访问对象。(Sun HotSpot使用这种方式)

2.3.1 句柄访问

简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。
优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。
在这里插入图片描述

2.3.1 直接指针

与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样

优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】
在这里插入图片描述

3 内存溢出

内存溢出和内存泄漏的区别

内存溢出:在Java虚拟机向系统申请内存时,由于虚拟机内部的各存储区域存储空间都有限制(可以通过指定虚拟机的某些参数来优化调整内存大小),例如当堆内存被占满后,虚拟机再向系统申请内存是申请不到的,此时就会发生内存溢出。
内存泄漏:内存泄漏是针对GC(垃圾回收器)来说的,GC在进行对象回收时,一些无用对象仍然持续占有内存,无法得到及时释放,最后造成内存空间的浪费

内存溢出有以下几种分类

java堆溢出
虚拟机栈和本地方法栈溢出
方法区和运行常量池溢出

java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,当对象数量达到堆的最大容量后就会产生内存溢出异常。
还有可能是内存泄漏导致内存溢出,如果内存泄漏的比较多,浪费了大量内存空间,这样就会导致内存快速被占满,然后造成内存溢出。

虚拟机栈和本地方法栈溢出
虚拟机栈就是我们平常说到的栈,它在java虚拟机中有两种异常

两种内存溢出异常[注意内存溢出是error级别的]
1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度
2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

上面所说的栈深度其实就是栈中栈帧的数量,操作系统分给JVM的内存是有限的,而JVM分给虚拟机栈的内存也是有限的,如果方法调用过多,创建的栈帧的数量也就越多,那么最终就会导致虚拟机栈溢出;当然我们可以通过-Xss参数来指定虚拟机栈的最大深度。另外,如果将虚拟机栈设置为可动态扩展,那么同样的当栈深度不够时,JVM会自动申请扩展,如果此时申请不到足够的内存空间就会抛出OverOfMemoryError异常

方法区和运行时常量池溢出
在JDK6之前,很多人把方法区称为“永久代”,本质上两者并不等价,方法区只是java虚拟机规范的一种定义,或者说永久代来实现方法区而已,也就是说永久代仅仅是HotSpot的概念,对于其他虚拟机来说是不存在永久代概念的。而使用永久代来实现方法区,更容易遭遇到内存溢出问题,所以在JDK7的HotSpot中,原本放在永久代中的字符串常量被移到了java堆中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值