精华内容
下载资源
问答
  • 内存模型
    2021-09-09 17:32:10

    经典问题

    Q:说一下 Java(Jvm)的内存模型
    A:Java内存模型是Jvm内存模型的抽象(深入理解JAVA虚拟机)

    • JVM内存模型

      • 线程安全--虚拟机栈、本地方法栈、程序计数器

      • 非线程安全--方法区、堆(新生代,老年代)、
        Meta Space(Jdk 1.8新增)

    • Java内存模型

      • 工作内存、主内存(对应JVM内存中的一部分)

    追根溯源

    Jvm内存模型

    图片

    Java虚拟机运行时数据区

    程序计数器:一块较小的内存空间,当前线程所执行的字节码行号指示器。

    虚拟机栈:线程私有,生命周期与线程相同。每个方法执行,虚拟机都会创建一个栈帧,储存局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用到执行完毕,就对应着一个栈帧的入栈和出栈。

    本地方法栈:与虚拟机栈的作用非常类似,不同的是虚拟机栈为java方法服务,本地方法栈为本地方法(Native)服务。

    :虚拟机管理的内存里最大的一块,被所有线程共享,在虚拟机启动的时候创建。堆唯一的目的就是存放对象实例,Java中几乎所有的对象实例都在堆分配内存。

    方法区:用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    运行时常量池:方法区中的部分区域,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

    Java内存模型(JMM)

           定义:是一种规范,规定了JVM如何使用计算机内存。同时,JMM向开发者保证,如果程序是正确同步的,程序的执行将具有顺序一致性(顺序一致性内存模型)

           目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从主内存中取出变量这样的底层细节(此处变量与Java编程中的变量有区别,它包括了实例字段、静态字段和构成数组对象的元素,不包括局部变量与方法参数,后者是线程私有)。

    图片

    JMM模型图

           主内存:规定所有的变量都存储到主内存中(可类比物理硬件的主内存,但此处只是虚拟机内存的一部分)

           工作内存:每条线程所有(可类比处理器高速缓存),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写内存中的变量。不同线程之间无法访问对方工作内存中的变量。

    Jvm内存模型与Java内存模型关系

           基本上没有关系,如果两者一定要勉强对应起来,那主内存对应Java堆中的对象实例数据部分,工作内存对应虚拟机栈中的部分区域。

    为什么这么说,来了解一下内存模型

           我们知道,一个程序的运行,不可能只靠“计算”就能完成,处理器至少要与内存进行交互,如读取运算数据,存储运算结果等,这个I/O很难避免。由于存储设备与处理器运算速度有几个数量级的差距,所以现代操作系统不得不引入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速执行,运算结束后,再从缓存同步到内存中,这样处理器就无须等待缓慢的内存读写了。存储结构如图

    图片

    操作系统存储结构

           基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但也引入了新的问题:缓存一致性。

           当有多个处理器系统中,每个处理器都有自己的高速内存,而他们又共享同一主内存。

           当多个处理器的运算任务都涉及同一块主内存时,将导致个子的缓存数据不一致,如果真发生这种情况,那最终结果以谁为准呢?

           为解决该问题,各个处理器访问缓存时都需要遵守一些协议,读写时要根据协议来(MSI、MESI等)。

           基于此,我们常说的内存模型,即在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

           Java内存模型(JMM):用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果(类比操作系统中寄存器、高速缓存的一些处理机制)。

           Jvm内存模型:更多的是描述Java程序运行时的各个运行区域(类比操作系统中的内存管理模块),更多的用来将Java运行时使用的内存划分清楚,便于后续管理。

           两者所解决的主要矛盾不一致,所以基本是无区别。

    注:笔者一直认为,Java虚拟机是一个小型的操作系统,操作系统中需要注意的问题,Java虚拟机中都有涉及到,解决问题的相关方案与涉及模型,与操作系统中的相关模块实现也都很类似,学习Java虚拟机时,完全可以类比操作系统来学习。

    欢迎关注GoGoCoder微信公众号交流,定期更新高频面试考点

     

     

    更多相关内容
  • 讲一讲什么是Java内存模型 Java内存模型虽说是一个老生常谈的问题 ,也是大厂面试中绕不过的,甚至初级面试也会问到。但是真正要理解起来,还是相当困难,主要这个东西看不见,摸不着。 这是一个比较开放的题目,...
  • Java 内存模型

    2018-07-23 16:45:07
    深入理解 java 内存模型是 java 程序员的必修课,看看原汁原味正宗的内存模型
  • 局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的...
  • jvm和jmm的模型图,注意在浏览器中搜索draw.io进行文件的打开。
  • 通过这个电子版,可以对JAVA的内存模型有一定的认识,使自己成为熟悉底层的高手
  • 文章目录JavaScript的逻辑内存模型一、面向对象的三要素二、JavaScript的逻辑内存模型三、JavaScript的对象与Java的实例四、window对象的内存逻辑模型五、Object构造函数的内存逻辑模型六、Function构造函数的内存...
  • java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰java程序员,本文试图揭开java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重...
  • JVM内存模型

    千次阅读 2022-05-14 21:06:48
    2.2 内存模型 Java虚拟机的运行时数据区显然是在内存中的,所以这一部分也成为JVM的内存模型,这里要与JMM(Java内存模型)区分开。 内存模型主要包含五部分的内容:堆、栈、本地方法栈、方法区(元空间)、程序计数器...

    Java在诞生之初就提出了"Write once,Run Anywhere"的口号,而这些都得益于JVM(Java Vritual Machine),可以提前在不同的运行环境(linux或windows等)上安装JDK之后,就可以让同一份代码在任何地方运行了。而这里的JDK(Java Development Kit)是Java语言、Java虚拟机和Java类库的统称。

    一、JDK体系结构与Java跨平台特性

    1.1 JDK体系结构

    JDK是一个非常庞大的体系,里面包含了JVM、Java SE API(核心类库)、常用工具(javac等)以及Java语言等等一系列内容,其具体的体系结构图如下所示:

    1.2 Java跨平台特性

    Java的跨语言特性在文章开始已经提过了,通过在不同系统安装对应的JDK(主要是JVM)版本就可以实现,大概如下图所示:

    二、JVM整体结构与内存模型

    2.1 JVM整体结构

    JVM称为Java虚拟机,作为一个虚拟机它首要的功能就是对应用程序的线程等分配内存空间,而JVM还要负责加载类、解析执行编译后的字节码文件,所以整个JVM主要就包含了运行时数据区、类加载子系统和字节码执行引擎。

    大概的结构如下图所示:

    在Java虚拟机外部,还有一些本地方法接口,这些接口也是JVM经常需要调用。

    2.2 内存模型

    Java虚拟机的运行时数据区显然是在内存中的,所以这一部分也成为JVM的内存模型,这里要与JMM(Java内存模型)区分开。

    内存模型主要包含五部分的内容:堆、栈、本地方法栈、方法区(元空间)、程序计数器。

    :JVM管理的最大一块内存空间,它是所有线程所共享的一块区域。在虚拟机启动的时候创建,该区域的唯一目的就是为了存放对象实例,几乎所有通过new创建的实例对象都会被分配在该区域。

    栈(虚拟机栈):也可以称为虚拟机线程栈,它是JVM中每个线程所私有的一块空间,每个线程都会有这么一块空间。它的生命周期是与线程的生命周期是绑定的。虚拟机栈描述了Java中方法执行时的内存模型,即每个方法被执行的时候,线程都会在自己的线程栈中同步创建一个栈帧(Stack Frame),用于存放局部变量表、操作数栈、动态连接和方法出口等信息,每个方法从调用到完成的过程,就对应着一个栈帧在线程栈中从入栈到出栈的过程。

    本地方法栈:本地方法栈与虚拟机栈的作用是相似,不同的是虚拟机栈为JVM执行的Java方法服务,而本地方法栈为JVM调用的本地方法服务。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。

    程序计数器:只需要占用一小块的内存空间,每个线程都会有自己独立的程序计数器,主要功能就是记录当前线程执行到哪一行指令了,可以看作是当前线程所执行的字节码行号指示器。

    方法区(元空间):在JDK 8之前,方法区也称之为永久代,这部分区域与堆一样,是所有线程所共享的,它主要用于存放被虚拟机加载的类型信息、常量、静态变量以及即时编译器编译后的代码缓存等数据。对于一个Class文件,除了版本、字段、方法、接口等描述信息外,还有常量池表,在上一篇文章《JVM类加载机制》中提到过,主要用于编译器生成的各种字面量和符号引用,而这部分内容在Class文件加载后是存放在方法区的运行时常量池中。这个运行时常量池自然还包括了字符串常量池,但需要注意的是,在JDK 7以后的版本中,字符串常量池和静态变量等被移至到了Java堆区,而到了JDK 8,抛弃了之前永久代的概念,通过在本地内存中实现了元空间(Meta-space)来代替永久代,并把JDK 7中永久代剩余内容(主要是类型信息)全部移至到了元空间

    所以,方法区是使用直接内存来实现,这与堆是不一样的,也就是堆和方法区用的并不是同一块物理内存。

    直接内存:直接内存并不是JVM运行时数据区的一部分,其分配不会受Java堆大小的限制。

    下面以一个列子来演示JVM内存模型的各个部分,创建一个测试类如下:

    public class Test {
    
        public int compute(){
            int a = 4;
            int b = 5;
            int c = a * b * 6;
            return c;
        }
    
        public static void main(String[] args) {
            Test test = new Test();
            int compute = test.compute();
            System.out.println(compute);
        }
    }
    

    将Test类编译之后,然后在控制台通过javap -c Test.class命令输出Class文件对应的字节码指令,下面主要分析compute()方法的字节码指令,结合JVM的内存模型。

    public int compute();
    	Code:
            0: iconst_4
            1: istore_1
            2: iconst_5
            3: istore_2
            4: iload_1
            5: iload_2
            6: imul
            7: bipush        6
            9: imul
    

    下面是执行Test的main方法时,线程栈的模型,下面结合下面的模型和字节码指令来介绍运行时数据区的各个模块。
    在这里插入图片描述

    当执行Test类的main方法时,首先会将Test.class文件加载进JVM的方法区,主要有常量池和方法定义,而常量池中此时包含一个compute()的符号引用,而在执行main方法,当调用compute()方法时,会将compute()的符号引用变成该方法具体在方法区的内存地址(直接引用),因为这个过程是在程序运行时发生的,所以称为动态链接

    在执行main()compute()方法时会分别在main线程栈中创建两个栈帧,因为main()先调用,所以其栈帧处于栈的底部。

    通过compute()内存地址找到该方法包含的字节码指令就可以执行compute()方法了。

    1、iconst_4表示将int类型数值4压入操作数栈中,此时compute()栈帧中的操作数栈中就只有一个数字4

    2、istore_1表示将操作数栈顶的int类型数值放入到局部变量表的第二个槽位中(出栈),此时操作数栈就没有元素了。

    注:局部变量表中,是以槽位为基本单位来存储数值,第一个槽位的索引下标为0,且存放的局部变量为当this引用

    3、iconst_5表示将int类型数值5压入操作数栈中

    4、istore_2表示将操作数栈顶的int类型数值放入到局部变量表的第三个槽位中

    5、iload_1表示将局部变量表的第二个槽位中int类型的数值放入到操作数栈的栈顶

    6、iload_2表示将局部变量表的第三个槽位中int类型的数值放入到操作数栈的栈顶

    7、imul表示将操作数栈的元素全部出栈交给CPU进行乘法运算,然后再将运算结果压入操作数栈的栈顶

    第七步完成后,此时操作数栈里面的数值只有一个20

    8、bipush 6表示将单字节常量6放入到操作数栈的栈顶

    9、imul将操作数栈中的int类型的元素20和6出栈交给CPU进行乘法运算,然后将计算结果120压入操作数栈的栈顶

    10、istore_3表示将操作数栈顶的int类型数值放入到局部变量表的第四个槽位中

    11、iload_3表示将局部变量表的第四个槽位中int类型的数值放入到操作数栈的栈顶

    12、ireturn表示从当前方法返回int类型的数值,即操作数栈中的数值。

    至此compute()方法就执行完成了,执行完之后,需要知道将返回到哪里去继续执行,这就是方法出口的作用,它会记录方法执行完之后,跳转到哪里。

    注:对于这些常用的字节码指令可以参考周志明的《深入理解Java虚拟机》

    三、JVM内存参数

    JVM内存模型中的这些区域,都是有大小限制的,当然也可以通过JVM提供的参数来设置这些区域所占内存的大小。

    补充:在JVM堆中的实例对象,会按照它们的年龄或大小分为新生代或老年代,而新生代的内存空间又分为Eden区和Survivor区,如下图所示:

    至于为什么要这样划分内存空间,会在下篇文章介绍JVM的垃圾收集机制时详细介绍。

    下面介绍这些常用参数的含义:

    -Xss:表示每个线程栈的大小

    -Xms:表示堆空间的初始可用大小,默认为物理内存的1/64

    -Xmx:表示堆空间的最大可以大小,默认为物理内存的1/4(这个参数在应用程序中是一定要指定的)

    -Xmn:表示新生代(年轻代)的大小

    -XX:NewRatio:默认为2,表示新生代占年老代的1/2,占整个堆内存的1/3。

    -XX:SurvivorRatio:默认为8,表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。

    -XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。

    -XX:MetaspaceSize: 指定元空间触发Full Gc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。

    由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般推荐将这两个值都设置为256M。

    展开全文
  • 基础 4 并发编程模型的分类 4 Java 内存模型的抽象 4 ... JMM,处理器内存模型与顺序一致性内存模型之间的关系 68 JMM 的设计 69 JMM 的内存可见性保证 72 JSR-133 对旧内存模型的修补 73 个人简介 74 参考文献 74
  • 深度剖析java内存模型

    2019-06-03 11:35:21
    深度剖析java内存模型深度剖析java内存模型深度剖析java内存模型
  • 详细介绍Java内存,ava线程之间的通信对程序员完全透明,内存可见性问题很容易困扰java程序员,本文试图揭开java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)...
  • linux内存模型

    千次阅读 2022-04-11 16:51:34
    前面已经分析把物理内存添加到memblock以及给物理内存建立页表映射,这里我们分析内存模型。在Linux内核中支持3种内存模型,分别为 flat memory model Discontiguous memory model sparse memory model 所谓memory ...

    前面已经分析把物理内存添加到memblock以及给物理内存建立页表映射,这里我们分析内存模型。在Linux内核中支持3种内存模型,分别为

    flat memory model
    Discontiguous memory model
    sparse memory model
    所谓memory model,其实就是从cpu的角度看,其物理内存的分布情况,在linux kernel中,使用什么的方式来管理这些物理内存。某些体系架构支持多种内存模型,但在内核编译构建时只能选择使用一种内存模型。

    在这里插入图片描述
    1. 基本概念
    1.1 page frame
    从虚拟地址到物理地址的映射过程,系统对于内存管理是以页为单位进行管理的。在linux操作系统中,物理内存是按照page size来管理的,具体page size是多少是和硬件以及linux系统配置相关的,4k是最经典的设定。因此,对于物理内存,我们将其分成一个个按page size排列的page,每一个物理内存中的page size的内存区域我们称之page frame。page frame是系统内存的最小单位,对内存中的每个页都会创建struct page实例。

     

    1.2 PFN
    对于一个计算机系统,其整个物理地址空间应该是从0开始,到实际系统能支持的最大物理空间为止的一段地址空间。在ARM系统中,假设物理地址是32个bit,那么其物理地址空间就是4G,在ARM64系统中,如果支持的物理地址bit数目是48个,那么其物理地址空间就是256T。当然,实际上这么大的物理地址空间并不是都用于内存,有些也属于I/O空间(当然,有些cpu arch有自己独立的io address space)。因此,内存所占据的物理地址空间应该是一个有限的区间,不可能覆盖整个物理地址空间。

    PFN是page frame number的缩写,所谓page frame,就是针对物理内存而言的,把物理内存分成一个个固定长度为page size的区域,并且给每一个page 编号,这个号码就是PFN。与page frame的转换关系如下图所示

    在这里插入图片描述

     

    1.3 NUMA
    在多核的系统设计中内存的架构有两种类型计算机,分别以不同的方式管理物理内存。

    UMA计算机(一致内存访问,uniform memory access):将可用内存以连续方式组织起来,系统中所有的处理器共享一个统一的,一致的物理内存空间,无论从哪个处理器发起访问,对内存的访问时间都是一样快。其架构图如下图所示

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pAW5gui-1590331718224)(D:\学习总结\内存管理单元\image-20200524204756938.png)]

     

    NUMA计算机(非一致内存访问,non-uniform memory access):每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU 和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rIYzZiOs-1590331718226)(D:\学习总结\内存管理单元\image-20200524205225109.png)]

     

    从图中可以看出,每个CPU访问local memory,速度更快,延迟更小。当然,整体的内存构成一个内存池,CPU也能访问remote memory,相对来说速度更慢,延迟更大。目前对NUMA的了解仅限于此,在内核中会遇到相关的代码,大概知道属于什么范畴就可以了。

    2. linux内存模型
    Linux提供了三种内存模型(include/asm-generic/memory_model.h),一般处理器架构支持一种或者多种内存模型,这个在编译阶段就已经确定,比如目前在ARM64中,使用的Sparse Memory Model。

    2.1 FLAT memory model(平坦内存模型)
    如果从系统中任意一个CPU的角度来看,当它访问物理内存的时候,物理地址空间是一个连续的,没有空洞的地址空间,那么这种计算机系统的内存模型就是Flat memory。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jqN5vSAO-1590331718228)(D:\学习总结\内存管理单元\image-20200524211005831.png)] 

    早期的系统物理内存不大,那个时候Linux使用平坦内存模型(flat memory model)来管理物理内存就足够有效了。一个page frame用一个struct page结构体表示,整个物理内存可以用一个由所有struct page构成的数组mem_map表示,而经过页表查找得到的PFN,正好可以用来做这个数组的小标,__pfn_to_page()函数就是专门来完成这个功能的。

    #define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET)) 
    1


    对于FLATMEM来说,物理内存本身是连续的,如果不连续的话,那么中间一部分物理地址是没有对应的物理内存,就会形成一个个洞,这就浪费了mem_map数组本身占用的内存空间。对于这种模型,其特点如下:

    内存连续且不存在空隙
    这种在大多数情况下,应用于UMA系统“Uniform Memory Access”。
    通过CONFIG_FLATMEM配置
    2.2 discontiguous memory model (不连续内存模型)
    如果CPU在访问物理内存的时候,其地址空间是有一些空洞的,是不连续的,那么这种计算机系统的内存模型就是Discontiguous memory。在什么情况下物理内存是不连续的呢?当NUMA出现后,为了有效的管理NUMA模式的物理内存,一种被称为不连续内存模型的实现于1999年被引入linux系统中。在这中模型中,NUMA中的每个Node用一个叫做pglist_data的结构体表示。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbQWbRYo-1590331718229)(D:\学习总结\内存管理单元\image-20200524214654201.png)]
    应对不连续物理内存的问题似乎是解决了,可是现在你给我一个物理page的地址,使用DISCONTIGMEM的话,我怎么知道这个page是属于哪个node的呢,PFN中可没有包含node编号啊。pfn_to_page()之前干的活多轻松啊,就是索引下数组就得到数组元素struct page了,现在PFN和page之间的对应关系不是那么直接了,pfn_to_page的任务就开始重起来了。

     

    #define __pfn_to_page(pfn)            \ 
    ({    unsigned long __pfn = (pfn);        \ 
        unsigned long __nid = arch_pfn_to_nid(__pfn);  \ 
        NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\ 
    })
    1
    2
    3
    4
    5
    物理内存存在空洞,随着Sparse Memory的提出,这种内存模型也逐渐被弃用了。这种内存模型有以下的特点

    多个内存节点不连续并且存在空隙"hole"
    适用于UMA系统和NUMA系统
    ARM在2010年已经移除了对DISCONTIGMEM的支持
    通过CONFIG_CONTIGMEM配置
    2.3 sparse memory model(稀疏内存模型)
    内存模型是一个逐渐演化的过程,刚开始的时候,由于内存比较小,使用flat memory模型去抽象一个连续的内存地址空间。但是出现了NUMA架构之后,整个不连续的内存空间被分配成若干个node,每个node上是连续的内存地址空间,为了有效的管理NUMA模型下的物理内存,就开始使用discontiguous memory model。为了解决DISCONTIGMEM存在的弊端,一种新的稀疏内存模型被使用出来。

    在sparse memory内存模型下,连续的地址空间按照SECTION被分成一段一段的,其中每一个section都是Hotplug的,因此sparse memory下,内存地址空间可以被切分的更细,支持更离散的Discontiguous memory。在SPARSEMEM中,被管理的物理内存由一个个任意大小的section(struct mem_section表示)构成,因此整个物理内存可被视为一个mem_section数组。每个mem_section包含了一个间接指向struct page数组的指针。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyWNUE4Z-1590331718230)(D:\学习总结\内存管理单元\image-20200524223000055.png)]

     

    其主要的特点如下:

    多个内存区域不连续并且存在空隙
    支持内存热插拔(hot plug memory),但性能稍逊色于DISCONTIGMEM
    在x86或ARM64内存采用该中模型,其性能比DISCONTIGMEM更优并且与FLATMEM相当
    对于ARM64平台默认选择该内存模型
    以section为单位管理online和hot-plug内存
    通过CONFIG_SPARSEMEM配置
    section大小从几十MiB到几GiB不等,取决于体系架构和内核的配置。通常在系统配置中将内存扩展单元「memory expansion unit」用作section大小。比如,如果系统内存可扩展至64GiB,并且最小内存扩展单元为1GiB,则设置section大小也为1GiB。当使用Linux系统作为hypervisor的客户操作系统「guest OS」,也是以section大小为单元在运行时向Linux系统增添内存和移除Linux系统的内存。

    3. 平台内存模型支持
    Linux支持的各种不同体系结构在内存管理方面差别很大,以下是主流的架构支持情况如下表所示,一个体系架构中可能有多种内存模型可用(ARM64只支持一种内存模型),通过可选的内核配置选项来决定使用哪种内存模型。

    系统架构FLATMEMDISCONTIGMEMSPARSEMEM
    ARM默认不支持某些系统可选配置
    ARM64不支持不支持默认
    x86_32默认不支持可配置
    x86_32(NUMA)不支持默认可配置
    x86_64不支持不支持默认
    x86_64(NUMA)不支持不支持默认


    4.小结
    这章我们学习了3种内核模型的各自原理和特点,同时我们简单介绍linux kernel,对于这三种模型使用什么样的方式来管理这些物理内存,后面的章节中会针对FLAT(ARM)和SPARSE(ARM64)模型做相应的介绍。

    展开全文
  • JVM内存模型详解

    2019-02-22 14:48:33
    jvm内存模型,jvm脑图,jvm调优,jvm垃圾回收算法,jvm垃圾回收器,逃逸算法等总结。
  • 深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM
  • Java 8 内存模型.pdf

    2018-03-22 13:50:36
    java内存模型介绍java内存模型介绍java内存模型介绍java内存模型介绍java内存模型介绍
  • Go 内存模型 - Go 编程语言。
  • java 锁 内存模型, 对于想了解cpu锁,内存模型的同学是很不错的资料
  • 一文读懂C++虚继承的内存模型

    千次阅读 多人点赞 2021-06-30 21:04:01
    一文读懂C++虚继承的内存模型1、前言2、多继承存在的问题3、虚继承简介4、虚继承在标准库中的使用5、虚继承下派生类的内存布局解析6、总结 1、前言 C++虚继承的内存模型是一个经典的问题,其具体实现依赖于编译器,...

    1、前言

    C++虚继承的内存模型是一个经典的问题,其具体实现依赖于编译器,可能会出现较大差异,但原理和最终的目的是大体相同的。本文将对g++中虚继承的内存模型进行详细解析。

    2、多继承存在的问题

    C++的多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。从概念上来讲这是非常简单的,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个,比如典型的是菱形继承,如图2-1所示:

    在这里插入图片描述

    图2-1 菱形继承


    在图2-1中,类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自A–>B–>D这条路径,另一份来自A–>C–>D这条路径。
    在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的,因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类A有一个成员变量a,那么在类D中直接访问a就会产生歧义,编译器不知道它究竟来自A–>B–>D这条路径,还是来自A–>C–>D这条路径。下面是菱形继承的代码实现:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: public A
    {
    public:
        long b;
    };
    
    
    class C: public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; } // 命名冲突
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    这段代码就是图2-1所示的菱形继承的具体实现,可以看到在类Dseta()方法中,代码试图直接访问间接基类的成员变量a,结果发生了错误,因为类B和类C中都有成员变量a(都是从类A继承的),编译器不知道选用哪一个,所以产生了歧义。

    为了消除歧义,我们可以在使用a时指明它具体来自哪个类,代码如下:

    void seta(long v) { B::a = v; }
    /* 或 */
    void seta(long v) { C::a = v; }
    

    使用GDB查看变量d的内存布局,如图2-2所示:

    在这里插入图片描述

    图2-2 变量d的GDB调试结果


    于是我们可以画出变量d的内存布局,如图2-3所示:

    在这里插入图片描述

    图2-3 变量d的内存布局

    3、虚继承简介

    为了解决多继承时命名冲突和冗余数据的问题,C++提出了虚继承这个概念,虚继承可以使得在派生类中只保留一份间接基类的成员。使用方式就是在继承方式前面加上virtual关键字修饰,示例代码如下(基于前面的例子修改):

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: virtual public A
    {
    public:
        long b;
    };
    
    
    class C: virtual public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; } // 现在不会冲突了
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    可以看到这段代码使用虚继承重新实现了前面提到的那个菱形继承,这样在派生类D中就只保留了一份间接基类A的成员变量a了,后续再直接访问a就不会出现歧义了。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的类A就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。本例的继承关系如图3-1所示:

    在这里插入图片描述

    图3-1 虚继承下菱形继承


    从这个新的继承体系中我们可以发现虚继承的一个特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在图3-1中,我们是当定义类D时才出现了对虚派生的需求,但是如果类B和类C不是从类A虚派生得到的,那么类D还是会保留间接基类A的两份成员,示例代码如下:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: public A
    {
    public:
        long b;
    };
    
    
    class C: public A
    {
    public:
        long c;
    };
    
    class D: virtual public B, virtual public C
    {
    public:
        void seta(long v) { a = v; } // 错误,不能等到定义类D时再来做虚继承的工作
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。

    4、虚继承在标准库中的使用

    C++标准库中的iostream就是一个虚继承的典型案例。iostream是从istreamostream直接继承而来的,而istreamostream又都继承自一个名为ios的类,这个就是一个典型的菱形继承。此时istreamostream必须采用虚继承,否则将导致iostream中保留两份ios的成员。

    iostream相关的源代码如下(从gcc-2.95.3版本中摘录出来的,内容有所省略):

    struct _ios_fields
    { // The data members of an ios.
        streambuf *_strbuf;
        ostream* _tie;
        int _width;
        __fmtflags _flags;
        _IO_wchar_t _fill;
        __iostate _state;
        __iostate _exceptions;
        int _precision;
    
        void *_arrays; /* Support for ios::iword and ios::pword. */
    };
    
    class ios : public _ios_fields
    {...};
    
    class istream : virtual public ios
    {...};
    
    class ostream : virtual public ios
    {...};
    
    class iostream : public istream, public ostream
    {
    public:
        iostream() { }
        iostream(streambuf* sb, ostream*tied=NULL);
    };
    

    5、虚继承下派生类的内存布局解析

    g++中是没有所谓的虚基类表的(据说vs是有单独一个虚基类表的),只有一个虚表,由于平时用的比较多的是虚函数,所以一般情况下都直接管它叫做虚函数表,在g++编译环境下这种叫法其实是不严谨的。测试程序如下:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: virtual public A
    {
    public:
        long b;
    };
    
    class C: virtual public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; }
        void setb(long v) { b = v; }
        void setc(long v) { c = v; }
        void setd(long v) { d = v; }
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
        d.seta(1);
        d.setb(2);
        d.setc(3);
        d.setd(4);
    }
    

    D在当前编译器(GCC 4.8.5)下的内存布局如图5-1所示:

    在这里插入图片描述

    图5-1 类D的内存布局


    从图5-1中可以看出这个表和之前这篇文章《一文读懂C++虚函数的内存模型》讲的虚函数表是差不多的,就多了一个vbase_offset而已。因为这里的类设计比较简单,没有把虚函数加进来,有虚函数的话_vptr.B或者_vptr.C下面的内存空间存储的就是指向对应虚函数的指针了(以下只讲_vptr.B的相关内容,_vptr.C同理就不赘述了)。

    这里可以看到_vptr.B指向的是虚函数的起始地址(因为这里没有虚函数,所以下面紧接着就是_vptr.C的内容),而不是与它相关联的全部信息的起始地址,事实上从图5-1中可以看出_vptr.B - 3 ~ _vptr.B这个范围内的数据都是类B虚表的内容(不知道编译器为什么这么设计,这里也进行揣测了),这三个特殊的内存地址存储的内容解析如下:

    1. _vptr.B - 1:这里存储的是typeinfo for D,里面的内容其实也是一个指针,指向的是类D的运行时信息,这些玩意都是为了支持RTTI的。RTTI的相关内容以后会讲,这里就先不多分析了。
    2. _vptr.B - 2:这里存储的是offset_to_top,这个表示的是当前的虚表指针距离类开头的距离,可以看到对于_vptr.B来说这个值就是0,因为_vptr.B就存在于类D的起始位置,而对于_vptr.C来说这个值是-16,大家可以算一下_vptr.C与类D的起始位置确实是差两个地址也就是16个字节(64位系统),至于为什么是负数,这是因为堆内存是向下增长的,越往下地址数值越大。

    offset_to_top深度解析:在多继承中,由于不同基类的起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this指针的偏移量也不相同。由于实际类型在编译时是未知的,这要求偏移量必须能够在运行时获取。实体offset_to_top表示的就是实际类型起始地址到当前这个形式类型起始地址的偏移量。在向上动态转换到实际类型时(即基类转派生类),让this指针加上这个偏移量即可得到实际类型的地址。需要注意的是,由于一个类型即可以被单继承,也可以被多继承,因此即使只有单继承,实体offset_to_top也会存在于每一个多态类型之中。
    (这里要注意一点就是offset_to_top只存在于多态类型中,所以我们可以看到在第二小节那个例子中,根本就没有什么所谓的虚表之类的东西,它也就不支持RTTI,最简单的大家可以使用dynamic_cast去试试,会报错说该类型不具备多态性质的。那么问题来了,怎样才能以最简短的方式让它具备多态的性质呢?很简单,定义一个析构函数,用virtual修饰即可)

    1. _vptr.B - 3:这里存储的是vbase_offset,这个表示的是当前虚表指针与其对应的虚基类的距离。从图中可以看出对于_vptr.B来说这个值是40,算一下刚好是_vptr.Ba的差距,_vptr.C同理。

    vbase_offset深度解析:以测试程序为例,对于类型为B的引用,在编译时,无法确定它的虚基类A它在内存中的偏移量。因此,需要在虚表中额外再提供一个实体,表明运行时它的基类所在的位置,这个实体称为vbase_offset,位于offset_to_top上方。

    接下来我们通过GDB来验证一下前面讲的内容,先打印出变量d的内存信息,如图5-2所示:

    在这里插入图片描述

    图5-2 变量d的内存信息


    从图5-2中可以看到变量d的内容与前面分析的差不多,接下来我们来看一下这两个虚表的内容,如图5-3所示:

    在这里插入图片描述

    图5-3 虚表内存信息


    从图5-3中可以看出前面的内存图是正确的,接下来就再看一下变量d自身的内存布局,如图5-4所示:

    在这里插入图片描述

    图5-4 变量d的内存布局


    图5-4显示出的结果和前面图5-1的完全一致,到这里调试就结束了,由调试结果可以知道图5-1的内存模型是正确的。

    这里要补充一点,就是对于虚继承下的类D,和第二节那个没有虚继承的相比,基类A的位置被移动到了类D的最末尾,不过不用担心,运行时可以靠vbase_offset找到它。

    6、总结

    本文先是对虚继承的概念以及使用场景进行了说明,然后通过一个内存模型图向大家展示了g++下虚继承的内存形态,最后使用GDB查看实际的内存情况来验证内存模型图的正确性。本文为了更直观地展示虚继承的内存模型,示例设计得很简单,类的设计中只有一个成员变量而没有成员函数、虚函数等其它内容。本文与前文《一文读懂C++虚函数的内存模型》相当于抛砖引玉,为下文作铺垫,在下一篇文章中我将对一些稍微复杂一点的情景进行分析,看看完整形态的虚表究竟是什么样的。

    最后,如果大家觉得本文写得好的话麻烦点赞收藏关注一下谢谢,也可以关注该专栏,以后会有更多优质文章输出的。

    展开全文
  • flink内存模型

    千次阅读 2022-03-29 09:26:55
    flink内存管理模型,参数配置
  • Java内存模型深度解读

    2021-01-21 17:12:16
    Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型。  如果你想设计表现良好的并发程序,理解Java...
  • JVM内存模型篇【JVM内存模型

    千次阅读 2022-02-23 09:58:11
    1、JVM内存模型 本地方法区 Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个...
  • Spark 内存模型

    千次阅读 2021-03-17 23:45:28
    spark 内存模型中会涉及到多个配置,这些配置由一些环境参数及其配置值有关,为防止后面理解混乱,现在这里列举出来,如果忘记了,可以返回来看看: spark.executor.memory :JVM On-Heap 内存(堆内内存),在使用...
  • JVM内存结构和Java内存模型别再傻傻分不清了

    万次阅读 多人点赞 2020-03-04 20:53:30
    JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺大的。 通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多...
  • 全面理解Java内存模型(JMM)及volatile关键字

    万次阅读 多人点赞 2017-06-12 11:25:05
    本篇主要结合博主个人对Java内存模型的理解以及相关书籍内容的分析作为前提,对JMM进行较为全面的分析,本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,在...
  • 旧的Java内存模型  Java使用的是共享内存的并发模型,在线程之间共享变量。Java语言定义了线程模型规范,通过内存模型控制线程与变量的交互,从而实现Java线程之间的通信。在JDK5之前,Java一直使用的是旧内存模型...
  • 内存模型就是对内存进行使用的一个规范。通过内存模型,我们能了解 C 语言是如何 对内存进行划分,如何使用每一部分内存的
  • Java 内存模型(JMM)

    千次阅读 2021-08-11 08:16:20
    文章目录什么是JMM模型JMM和JVM的区别主内存和工作内存Java内存模型与硬件内存架构的关系JMM存在的必要性 什么是JMM模型 Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组...
  • 面试官:说说什么是 Java 内存模型(JMM)?

    万次阅读 多人点赞 2021-05-05 23:23:20
    1. 为什么要有内存模型? 1.1. 硬件内存架构 1.2. 缓存一致性问题 1.3. 处理器优化和指令重排序 2. 并发编程的问题 3. Java 内存模型 3.1. Java 运行时内存区域与硬件内存的关系 3.2. Java 线程与主内存的关系 ...
  • Java内存结构与Java内存模型

    千次阅读 2021-12-02 17:13:40
    Java内存结构、内存模型、对象模型这几个概念经常会被混为一谈。理论知识容易忘写下来帮助记忆,方便以后查看。 1、Java内存结构 Java内存结构其实说的是JVM在运行时内存区域的划分,是根据Java虚拟机规范来实现的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 795,883
精华内容 318,353
关键字:

内存模型