JMM java内存模型图文详解

高洛峰

高洛峰

2017-03-19

1969人浏览

原创

  jmm对于一个想要深入了解java的程序猿来说是不可避免的一关,本文偏理论性,尽可能说的通俗易懂,如有不对的地方希望多多指正。

  那我们先说一下jvm的主内存分配

  JMM java内存模型图文详解

 

  1 java虚拟机栈(java virtual stack)

  虚拟机栈是线程私有的,每个线程都有一个自己的虚拟机栈,是java方法执行的内存模型,每个方法执行的时候都会在虚拟机栈上创建一个栈帧,栈帧是一个数据结构,主要存储的是方法中的局部变量(基本类型,对象的引用,returnAddress类型(指向一条字节码指令的地址)),操作栈(指的就是方法编译后的操作指令的栈),动态链接,方法出口。通常所说的java内存分为栈和堆,其中所说的栈就是指的虚拟机栈。但java的内存分配并没有这么简单。

  动态链接解释如下:

  每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

  方法出口的解释如下:

  • 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。

  • 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

  虚拟机栈会出现两种异常,一种就是常见的OOM 另一种就是StackOverFlowError。StackOverflowError一般是递归调用所导致的,栈深度在虚拟机中也是有限制的,否则无限制的递归调用虚拟机会哭的。OOM就不用说了,当所请求的内存大于当前虚拟机栈所持有的就会出现OOM(虚拟机栈空间可以动态扩展,但分配给jvm的内存也是有限的,所以虚拟机栈也不是无限扩展的)。

  2 本地方法栈

  本地方法栈和虚拟机栈基本是类似的,只不过虚拟机栈中执行的是class字节码,而本地方法栈中执行的就是本地方法的服务,其实就是调用一些由c或c++根据不同的os平台所写的同一个方法的不同的实现。

  3 方法区(method area)

  方法区是线程共享的区域,用于存储已经被虚拟机加载的类信息(类的字节码数据,这里要注意如果你同时加载的类很多的话需要调大方法区的空间,否则会OOM,只是对于类较少的情况下可以那么做。如果类特别多,那么可以用懒加载等机制进行处理,如spring的懒加载机制,尽量避免同时加载过多的类),常量,静态变量和即时编译器(JIT)编译后的代码等数据。方法区其实就是我们所说的永久代区域(只限于hotspot虚拟机的实现机制),之所以说是永久代,是此处的数据几乎很少进行垃圾回收,原因是加载的类并不是一时半刻就会消亡,很多方法会根据类在堆中创建对象,而静态变量一般是,gc的跟搜索算法的root节点,而常量是根本不会变的数据,所以都很少进行清理。

      Java虚拟机规范对这个区域的限制也非常宽松,除了可以是物理不连续的空间外,也允许固定大小和扩展性,还可以不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的(所以常量和静态变量的定义要多注意)。方法区的内存收集还是会出现,不过这个区域的内存收集主要是针对常量池的回收和对类型的卸载。

      一般来说方法区的内存回收比较难以令人满意。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。

  4 运行时常量池

  JDK1.6之前字符串常量池位于方法区之中。 
  JDK1.7字符串常量池已经被挪到堆之中。

 

  java是一种动态连接的语言,常量池的作用非常重要,常量池中除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String数组)的常量值还,还包含一些以文本形式出现的符号引用,比如:

  类和接口的全限定名;

  字段的名称和描述符;

  方法和名称和描述符。

  在C语言中,如果一个程序要调用其它库中的函数,在连接时,该函数在库中的位置(即相对于库文件开头的偏移量)会被写在程序中,在运行时,直接去这个地址调用函数;

  在Java语言中这样,一切都是动态的。编译时,如果发现对其它类方法的调用或者对其它类字段的引用的话,记录进class文件中的,只能是一个文本形式的符号引用,在连接过程中,虚拟机根据这个文本信息去查找对应的方法或字段。

  所以,与Java语言中的所谓“常量”不同,class文件中的“常量”内容很非富,这些常量集中在class中的一个区域存放,一个紧接着一个,这里就称为“常量池”。

  java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。

  整个常量池会被JVM的一个索引引用,如同数组里面的元素集合按照索引访问一样,JVM针对这些常量池里面存储的信息也是按照索引方式进行,实际上常量池在Java程序的动态链接过程起到了一个至关重要的作用(上面有说到),下文摘自《深入理解java虚拟机》。  

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

      运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java虚拟机并不要求常量只能在编译期产生,也就是并非预置入Class文件常量池的内容才能进入方法区的运行时常量池中,运行期间也可将新的常量放入常量池中。

      常量池是方法区的一部分,所以受到内存的限制,当无法申请到足够内存时会抛出OutOfMemoryError异常

  5 堆(heap)

  堆就是内存中最大的一块区域,唯一用于存储对象实例的地方。这个地方也是gc算法主要的战场。不过随着JIT(即时编译)的发展和逃逸技术成熟,并不是所有的对象都在堆上创建。下文摘自《深入理解java虚拟机》。

  在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。当你写好一个Java程序后,源语言的语句将由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码(比如,Intel的Pentium微处理器或IBM的System/390处理器)。字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。

  java的内存分配大致就是这个样子,jvm中也配有很多的参数对上面的数据进行调节。这里就不进行列举,会在单独的一篇gc相关的文章中进行详细的说明。下面说一下在多核处理器的时代,jvm是如何处理并发带来的问题的。

  并发控制

  多核的cpu可以并发的执行多个线程,而每个线程都有一个自己的本地工作区(其实就是分配给每个核的系统缓存和寄存器),存储从上面主内存获取的数据作副本在工作区中运行,如果数据是多线程中共享的,而且线程之间是不能进行数据交换,这就涉及了共享变量数据不一致的问题。java通过sychronized volatile Lock锁等机制控制共享变量的可见性。

JMM java内存模型图文详解

  synchronized和lock会有单独的章节分别讲解实现机制, 这两个不用说在可见性和原子性上都得道了保障。而volatile仅保证了数据的可见性,仅当数据在read 和load的时候数据在其他线程中改变会在当前线程中有所感知,如果过了这两个阶段,那只能不好意思了,数据不一致,(其实volatile所做的就是避免使用缓存不将主存上的数据存储到线程工作内存中,在read和load阶段都是从主存中获取数据这样就能够感知到其他线程对变量的修改)。volatile仅仅是在早期的jdk版本中,由于synchronized的性能不好而出现的一个保证可见性的一个解决方案。现在的jdk版本的synchronized和lock都得到了一定的优化,所以一般的情况下是不建议采用volatile变量的,除非你知道你现在用volatile到底在干什么,因为它并不能保证并发的正确性。

JMM java内存模型图文详解

read and load 从主存复制变量到当前工作内存,use and assign  执行代码,改变共享变量值,store and write 用工作内存数据刷新主存相关内容,其中use and assign 可以多次出现。volitale适合一些幂等操作。这个会在lock的nofairsync的实现中解说。

说到这里就不得不说一下happen before原则了。它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

  下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序(jvm为了能够充分的利用cpu,提高利用率,jvm会将前后无关的代码或者说是操作进行重排序,让那些需要等待IO或者其他资源的操作排在后面,而其他能够瞬间完成的操作放在前面先执行,充分的利用cpu的资源)。

    1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。

    2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。

    3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。

    4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。

    5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

    6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。

    7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

    8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
墨刀AI提示词教学
墨刀AI提示词教学

本合集由PHP中文网精心整理,为您提供全面的墨刀AI提示词教学。内容涵盖高质量原型撰写公式与实操窍门,助您轻松掌握AI设计工具。无论是零基础入门还是进阶技巧,都能让您快速上手,大幅提升产品设计与协作效率。

2026.08.04

9

21

墨刀AI完整入门
墨刀AI完整入门

PHP中文网为您倾力打造墨刀AI保姆级入门指南完整版!本合集从零基础讲起,涵盖AI生成原型、提示词优化、图片转原型及多轮对话等核心功能。无论您是新手还是进阶用户,都能轻松掌握产品设计全流程。快来PHP中文网,一键解锁高效设计技巧,让想法即刻成型!

2026.08.04

7

20

墨刀AI进阶技巧
墨刀AI进阶技巧

本合集由PHP中文网精心整理,为您提供墨刀AI核心进阶策略指南。内容涵盖高效提示词写作、原型智能生成与微调、结构化导图制作及行业分析报告输出等实战技巧。助您轻松掌握AI设计工具,大幅提升产品设计与团队协作效率。

2026.08.04

8

14

火山引擎实名认证失败怎么办
火山引擎实名认证失败怎么办

火山引擎实名认证失败可能与证件信息填写错误、姓名或企业信息不一致、证件照片不清晰、营业执照状态异常、手机号验证失败或审核资料不完整有关。本专题整理个人认证、企业认证、资料上传、审核退回、重新提交和认证不通过的常见处理方法。

2026.08.04

4

10

火山引擎域名备案流程详解
火山引擎域名备案流程详解

火山引擎域名备案适合需要在火山引擎云服务器、对象存储、CDN或网站服务上绑定域名的用户参考。本专题整理备案入口、账号实名认证、备案类型选择、主体信息填写、网站信息提交、资料上传、初审核验、管局审核和备案失败排查,帮助用户完成网站上线前的备案流程。

2026.08.04

1

10

火山引擎DNS解析配置步骤
火山引擎DNS解析配置步骤

使用火山引擎DNS解析网站域名时,需要确认域名已完成管理接入,并正确配置服务器IP、CNAME地址或验证记录。本专题整理域名添加、记录类型选择、TTL设置、解析状态检查、备案和访问测试等流程,适合新手搭建网站时参考。

2026.08.04

3

10

火山引擎对象存储使用教程
火山引擎对象存储使用教程

火山引擎对象存储适合用于网站图片、视频文件、备份数据、静态资源和应用附件管理。本专题整理TOS控制台入口、存储桶创建、地域选择、权限设置、文件上传、访问链接生成、CDN加速、费用查看和常见上传或访问失败问题,帮助用户快速掌握对象存储基础操作。

2026.08.04

1

10

火山引擎云服务器使用教程
火山引擎云服务器使用教程

火山引擎云服务器使用教程适合第一次购买、部署和管理云服务器的用户参考。本专题整理控制台入口、实例创建、地域和配置选择、系统镜像设置、安全组放行、远程连接、网站部署、续费计费和常见连接失败问题,帮助用户快速完成云服务器基础使用流程。

2026.08.04

5

10

火山引擎API Key绑定大模型教程
火山引擎API Key绑定大模型教程

火山引擎API Key怎么绑定大模型适合需要在火山方舟、应用后台、脚本工具或AI编程软件中调用模型的开发者参考。本专题整理控制台服务开通、API Key创建、模型权限检查、模型ID选择、Base URL填写、调用测试和鉴权失败排查,帮助用户完成从密钥到模型调用的配置流程。

2026.08.04

2

10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.4万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 131.8万人学习