内存结构与内存模型

声明:以下内容部分来自别的博客和自己原创
参考博客

内存结构

简介

根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分
在这里插入图片描述
方法区和堆是线程共享的数据区,虚拟机栈和本地方法栈以及程序计数器为线程私有

作用

  • 程序计数器:程序计数器是很少的一部分内存空间,它保存的是程序当前执行的指令的地址,当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令.
  • (注:1.当cpu在执行本地方法,则程序计数器是未指定值(undefned)  2.在JVM中多线程是通过线程轮流切换来获得CPU执行时间的,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的.  3.程序计数器是jvm内存环境中为一个不会发生OOM异常的区域)
  • Java虚拟机栈:早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,因此它是线程私有的,其内部保存一个个的栈帧,对应着一次次的Java方法调用。当有方法调用的时候就会将方法封装到一个栈帧中进行压栈,方法执行完成后弹栈,某一时间对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。在每一个栈帧中都有一个区域存放本地变量表用于存放此方法执行过程所需要的基本类型和对象引用(注:这部分空间实在预编译阶段可预测的)。
  • (当栈的深度大于虚拟机所允许的深度时则会抛出Stack Overflow Eorror,但是一般虚拟机都会允许动态扩展栈内存,即当占内存不够时会向虚拟机申请扩大内存,如果扩展后的内存空间仍然不够的话,就会抛出OOM异常)
  • 本地方法栈:它和Java虚拟机栈的内存机制相同,本地方法栈则是为执行本地方法(Native Method)服务的。在Oracle Hotspot JVM中,本地方法栈和Java虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制
  • Java 堆Java 堆是被所有线程共享的一块内存区域,在JVM启动时创建,随着JVM的结束而消亡。此内存区域所占的面积最大,他主要是用于存放对象实例,几乎所有的对象实例都在这里分配内存。因此他也是垃圾回收器主要关注的区域,当创建的对象实例特别多导致顿内存空间不够时,此区域会发上OOM异常。堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
  • 方法区:这也是所有线程共享的一块内存区域,用于存储对象的类型数据,例如类结构信息,静态变量,以及常量池、字段、方法代码等。由于早期的Hotspot JVM实现,很多人习惯于将方法区称为永久代。注意:Oracle JDK 8中将永久代移除,同时增加了元数据区(Metaspace)。JVM对永久代垃圾回收(如,常量池回收、对象类型的卸载)非常不积极,所以当我们不断添加新类型的时候,永久代会出现OutOfMemoryError,尤其是在运行时存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题
  • (注:运行时常量池:是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。有关具体内容可)

Java1.7以后移除了方法区,使用元数据区来代替方法区。

  • 元数据区:在JDK1.7的时候,有一个JVM内存区域中有一块方法区,主要存放虚拟机加载的类信息,静态变量,常量等。JDK1.8时,移除了方法区的概念,用一个元数据区代替。元数据区存放的东西和方法区相同,不过元数据区移动到本地内存中。本地内存,又称堆外内存(Direct Memory),就是指机器内存中不是JVM管理的那部分内存,由操作系统管理。元数据区移动到本地内存以后,可以避免虚拟机加载类过多而引发的内存溢出:java.lang.OutOfMemoryError: PermGen,但是同样不能无限扩展。

内存模型

内存模型和内存结构是不同的东西。内存模型是一种抽象的虚拟机规范

CPU 和计算机内存的交互

在了解内存模型前,先了解CPU和计算机内存的交互。因为Java虚拟机内存模型定义的访问操作与计算机实分相似。

在计算机中,CPU和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区,但是和CPU相比,内存的读写速度也远远比不上CPU的速度,所以在CPU上都加上了高速缓存,用于缓解这种情况
在这里插入图片描述
CPU上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性

在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。
以我的pc为例,因为cpu成本高,缓存区一般也很小。

CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。

如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性?

为了解决这个问题,各个处理器需遵循一些协议保证一致性。【如MSI,MESI啥啥的协议。。】
在这里插入图片描述
在CPU层面,内存屏障提供了个充分必要条件

内存模型

虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致
在这里插入图片描述

内存屏障(Memory Barrier)

CPU中,每个CPU又有多级缓存,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。【内存屏障是硬件层的】

为什么需要内存屏障

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

内存屏障的作用

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

volatile型变量

当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。

可见性,对于一个该变量的读,一定能看到读之前最后的写入。
防止指令重排序,执行代码时,为了提高执行效率,会在不影响最后结果的前提下对指令进行重新排序,使用volatile可以防止,比如单例模式双重校验锁的创建中有使用到,如(https://www.jianshu.com/p/b30a4d568be4)

注意的是volatile不具有原子性,如volatile++这样的复合操作,这里感谢大家的指正

至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。简单画了个图。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值