热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

java内存模型(转)

前提知识:Java内存模型(JMM)是一个概念模型,底层是计算机的寄存器、缓存内存、主内存和CPU等。多处理器环境下&#

前提知识:

  Java内存模型(JMM)是一个概念模型,底层是计算机的寄存器、缓存内存、主内存和CPU等。 
  多处理器环境下,共享数据的交互硬件设备之间的关系:

JMM: 

从以上两张图中,谈一谈以下几个概念:

1.缓存一致性协议(MESI):

  由于每个处理器都含有私有的高速缓存,在对缓存中数据进行更新后,其他处理器中所含有的该共享变量的缓存如果被处理器进行读操作,就会出现错误。有些计算机采用LOCK#信号对总线进行锁定,当一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞,那么该处理器就能独自共享内存。然而总线锁定的开销太大,在之后的计算机中一般都采用“缓存锁定”的方式实现。
  MESI是代表了缓存数据的四种状态的首字母,分别是Modified、Exclusive、Shared、Invalid)
  M(Modified):被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
  E(Exclusive):独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
  S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
  I(Invalid):要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。
  在缓存行中有这四种状态的基础上,通过“嗅探”技术完成以下功能:【嗅探技术能够嗅探其他处理器访问主内存和它们的内部缓存】
  - 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
  - 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
  - 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
  - 只有E和M可以进行写操作而且不需要额外操作,如果想对S状态的缓存字段进行写操作,那必须先发送一个RFO(Request-For-Ownership)广播,该广播可以让其他CPU的缓存中的相同数据的字段实效,即变成I状态。
  通过以上机制可以使得处理器在每次读写操作都是原子的,并且每次读到的数据都是最新的。

为什么要有内存模型

  在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情。要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型。

  内存模型,英文名Memory Model,他是一个很老的老古董了。他是与计算机硬件有关的一个概念。那么我先给你介绍下他和硬件到底有啥关系。

CPU和缓存一致性

  我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦。

  刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。

  可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧。

  所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。

  那么,程序的执行过程就变成了:当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

  而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

  按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。

  这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

  那么,在有了多级缓存之后,程序的执行就变成了:当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

  单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

  下图为一个单CPU双核的缓存结构:

  随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。

  单线程。cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

  单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

  多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

  在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致,如下图:

 

处理器优化和指令重排

  上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化

  除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排

  可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

并发编程的问题 

  前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系。但是关于并发编程的问题你应该有所了解,比如原子性问题,可见性问题和有序性问题。

  其实,原子性问题,可见性问题和有序性问题。是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。

  这里简单回顾下这三个问题,并不准备深入展开,感兴趣的读者可以自行学习。我们说,并发编程,为了保证数据的安全,需要满足以下三个特性:

  原子性:是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。对于单个的读/写操作,在多线程环境下保证是原子操作,但复合操作比如i++,相当于是以下三个操作:

int temp = get(); //
temp += 1; // ADD
set(temp); //

  Java主要提供了锁机制以及CAS操作实现原子性,对于单个读/写操作是通过LOCK#信号或“缓存锁定”实现的。
  除此之外,long和double类型的变量读/写是非原子性的,每次都只读/写32位数据,所以一个单个的读/写操作就变成了两个读/写操作,有可能在只读/写了其中32位操作后CPU就被其他线程抢占到。

  可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。由于每个线程都有一个私有的工作空间,并且保存一个主存中共享变量的副本,在线程对私有的工作空间中的数据进行写操作,别的线程并没有读到最新的值,就会出现问题。Java提供了volatile关键字保证了内存的可见性,底层通过LOCK#或“缓存锁定”实现。

instance = new Singleton(); // instance 是一个volatile变量

  以上代码在进行反汇编后得到的汇编代码如下:

  0x01a3de1d: movb $0×0,0×1104800(%esi);

  0x01a3de24: lock addl $0×0,(%esp);

  如果是一个volatile关键字修饰的变量,则会有第二行的汇编代码,这是一条含有lock前缀的代码。带有lock前缀的代码则会通过LOCK#或通过“缓存锁定”实现线程间的可见性。

  有序性:即程序执行的顺序按照代码的先后顺序执行。编译器和处理器会通过多种方式比如重排序对代码进行优化,然而在重排序后可能会导致运行结果与预想的不同。 

a.重排序的方式: 
  计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

- 编译器优化的重排序:
  编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。【as-if-serial原则保证,as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。】
- 指令级并行的重排序:
  现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
- 内存系统重排序:
  由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

b.内存屏障(Memory Barrier,又称内存栅栏): 

  我们知道一个线程是由一个CPU核来处理的,但现代CPU为了继续挖掘并行运算能力,出现了流水线技术,即每个内核有多个电路,调度器将串行指令分解为多步,并将不同指令的各步操作结果叠加,从而实现并行处理,指令的每步都由各自独立的电路来处理,最后汇总处理。这在相当程度上提高了并发能力。那么指令该如何分解?指令的前后执行顺序是否有关系?如何保证结果正确?这又是一个复杂的课题:重排序。

  作为CPU来说,为了提高执行效率,允许将一段程序代码分发到各个流水线去执行,但我们的程度代码可能是有前后逻辑关系的,比如int a=10;int b=a*2;这里如果将第二行移到第一行前面执行,则产生了错误结果,这里的计算b的代码我们称之为与前面的a存在数据依赖.

  下面是不同处理器对不同重排序类型的支持程度

QQ截图20140106202640

  从上面可以看到,各种处理器均不对“数据依赖”类型的语句进行重排序。那这里的一堆Load-Load、Load-Store是什么意思呢?

  Load指的是从内存中读取数据的操作&#xff0c;即if(a<1)这样的语句(只读不写)&#xff1b;Store指的是向内存中写入数据的操作&#xff0c;即a&#61;10这样的语句(只写不读)。Load-Load指先读后读的操作是否允许重排序&#xff1b;Load-Store指先读后写是否允许重排序&#xff1b;Store-Store指先写后写的两个语句是否允许重排序&#xff1b;Store-Load指先写后读的两个语句是否允许重排序。

  从上表可以看到&#xff0c;对于我们常用的Intel和AMD所使用的x86架构&#xff0c;只支持Store-Load重排序&#xff0c;这是因为CPU在写入的时候是首先写入寄存器&#xff0c;包括各级缓存&#xff0c;此时并没有刷新到内存中&#xff0c;如果等待其完成再读则太慢了&#xff0c;所以允许重排序。只要理解了写入是通过缓存批量执行的&#xff0c;那就不难理解。

说了这么多&#xff0c;还是没有提到内存屏障&#xff0c;它究竟是什么&#xff1f;仍然以ia64为例&#xff0c;它是允许Store-Store重排序的&#xff0c;但未必能保证在多线程环境下准确&#xff0c;比如这样的程序:

static class ThreadA extends Thread{public void run(){a&#61;1;flag&#61;true;}}static class ThreadB extends Thread{public void run(){if(flag){a&#61;a*1;}if(a&#61;&#61;0){System.out.println("ha,a&#61;&#61;0");}}} 

  当ThreadA执行的时候&#xff0c;按照顺序执行的逻辑&#xff0c;当flag&#61;true的时候&#xff0c;a必然等于1&#xff1b;那么ThreadB在if(flag)为真时&#xff0c;会用a&#61;1的值去计算a*1&#xff0c;但CPU对Store-Load重排序后&#xff0c;ThreadA中的a&#61;1可能会在flag&#61;true后面执行&#xff0c;如此就造成ThreadB中a&#61;a*1没有根据预期的a&#61;1来执行&#xff0c;所以操作系统需要提供一种机制&#xff0c;以禁用重排序&#xff0c;将决定是否重排序的选择权交给应用程序。如果应用程序不允许重排序&#xff0c;则插入相应的内存屏障指令将其禁用。

  Java为了能在不同平台下都能正确运行&#xff0c;提出了自己的内存模型&#xff0c;其中就定义了几种内存屏障指令(间接调用的都是底层操作系统的内存屏障指令)

  StoreLoad Barriers是一个“全能型”的屏障&#xff0c;它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障&#xff08;其他类型的屏障不一定被所有处理器支持&#xff09;。执行该屏障开销会很昂贵&#xff0c;因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中&#xff08;buffer fully flush&#xff09;。 

volatile语义中的内存屏障

  • volatile的内存屏障策略非常严格保守&#xff0c;非常悲观且毫无安全感的心态&#xff1a;

在每个volatile写操作前插入StoreStore屏障&#xff0c;在写操作后插入StoreLoad屏障&#xff1b;
在每个volatile读操作前插入LoadLoad屏障&#xff0c;在读操作后插入LoadStore屏障&#xff1b;

  • 由于内存屏障的作用&#xff0c;避免了volatile变量和其它指令重排序、线程之间实现了通信&#xff0c;使得volatile表现出了锁的特性。

final语义中的内存屏障

  • 对于final域&#xff0c;编译器和CPU会遵循两个排序规则&#xff1a;
  1. 新建对象过程中&#xff0c;构造体中对final域的初始化写入和这个对象赋值给其他引用变量&#xff0c;这两个操作不能重排序&#xff1b;&#xff08;废话嘛&#xff09;
  2. 初次读包含final域的对象引用和读取这个final域&#xff0c;这两个操作不能重排序&#xff1b;&#xff08;晦涩&#xff0c;意思就是先赋值引用&#xff0c;再调用final值&#xff09;

 

  内存屏障是一个CPU指令&#xff0c;Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。它的作用有两个&#xff1a;
  一&#xff1a;保证特定操作的执行顺序&#xff1b;
  二&#xff1a;保证某些变量的内存可见性。
  如果在指令间插入一条Memory Barrier则会告诉编译器和CPU&#xff0c;不管什么指令都不能和这条Memory Barrier指令重排序&#xff0c;也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据&#xff0c;因此任何CPU上的线程都能读取到这些数据的最新版本。

3.happens-before原则&#xff1a;
  使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内&#xff0c;也可以是在不同线程之间。因此&#xff0c;JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
  【如果A happens-before B&#xff0c;JMM并不要求A一定要在B之前执行。JMM仅仅要求A操作&#xff08;执行的结果&#xff09;对B操作可见&#xff0c;且A操作按顺序排在B操作之前。】
  1&#xff09;程序顺序规则&#xff1a;一个线程中的每个操作&#xff0c;happens-before于该线程中的任意后续操作。【在A happens-before B中&#xff0c;如果A和B重排序后不会导致结果变化&#xff0c;那么这种重排序是被允许的】
  2&#xff09;监视器锁规则&#xff1a;对一个锁的解锁&#xff0c;happens-before于随后对这个锁的加锁。
  3&#xff09;volatile变量规则&#xff1a;对一个volatile域的写&#xff0c;happens-before于任意后续对这个volatile域的读。
  4&#xff09;传递性&#xff1a;如果A happens-before B&#xff0c;且B happens-before C&#xff0c;那么A happens-before C。
  5&#xff09;start()规则&#xff1a;如果线程A执行操作ThreadB.start()&#xff08;启动线程B&#xff09;&#xff0c;那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6&#xff09;join()规则&#xff1a;如果线程A执行操作ThreadB.join()并成功返回&#xff0c;那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。

  有没有发现&#xff0c;缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以&#xff0c;后文将不再提起硬件层面的那些概念&#xff0c;而是直接使用大家熟悉的原子性、可见性和有序性。

什么是内存模型

  前面提到的&#xff0c;缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么&#xff0c;有没有什么机制可以很好的解决上面的这些问题呢&#xff1f;

  最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存&#xff0c;让CPU直接和主存交互。但是&#xff0c;这么做虽然可以保证多线程下的并发问题。但是&#xff0c;这就有点因噎废食了。

  所以&#xff0c;为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念&#xff0c;那就是——内存模型。

  为了保证共享内存的正确性&#xff08;可见性、有序性、原子性&#xff09;&#xff0c;内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作&#xff0c;从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题&#xff0c;保证了并发场景下的一致性、原子性和有序性。

  内存模型解决并发问题主要采用两种方式&#xff1a;限制处理器优化使用内存屏障。本文就不深入底层原理来展开介绍了&#xff0c;感兴趣的朋友可以自行学习。

什么是Java内存模型

  前面介绍过了计算机内存模型&#xff0c;这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢&#xff0c;不同的编程语言&#xff0c;在实现上可能有所不同。

  我们知道&#xff0c;Java程序是需要运行在Java虚拟机上面的&#xff0c;Java内存模型&#xff08;Java Memory Model ,JMM&#xff09;就是一种符合内存模型规范的&#xff0c;屏蔽了各种硬件和操作系统的访问差异的&#xff0c;保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

  提到Java内存模型&#xff0c;一般指的是JDK 5 开始使用的新的内存模型&#xff0c;主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。感兴趣的可以参看下这份PDF文档&#xff08;http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf&#xff09;

  Java内存模型规定了所有的变量都存储在主内存中&#xff0c;每条线程还有自己的工作内存&#xff0c;线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝&#xff0c;线程对变量的所有操作都必须在工作内存中进行&#xff0c;而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量&#xff0c;线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

  而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。

  这里面提到的主内存和工作内存&#xff0c;读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是&#xff0c;主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分&#xff0c;无法直接类比。《深入理解Java虚拟机》中认为&#xff0c;如果一定要勉强对应起来的话&#xff0c;从变量、主内存、工作内存的定义来看&#xff0c;主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。

  所以&#xff0c;再来总结下&#xff0c;JMM是一种规范&#xff0c;目的是解决由于多线程通过共享内存进行通信时&#xff0c;存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

Java内存模型的实现

  了解Java多线程的朋友都知道&#xff0c;在Java中提供了一系列和并发处理相关的关键字&#xff0c;比如volatilesynchronizedfinalconcurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。

  在开发多线程的代码的时候&#xff0c;我们可以直接使用synchronized等关键字来控制并发&#xff0c;从来就不需要关心底层的编译器优化、缓存一致性等问题。所以&#xff0c;Java内存模型&#xff0c;除了定义了一套规范&#xff0c;还提供了一系列原语&#xff0c;封装了底层实现后&#xff0c;供开发者直接使用。

  本文并不准备把所有的关键字逐一介绍其用法&#xff0c;因为关于各个关键字的用法&#xff0c;网上有很多资料。读者可以自行学习。本文还有一个重点要介绍的就是&#xff0c;我们前面提到&#xff0c;并发编程要解决原子性、有序性和一致性的问题&#xff0c;我们就再来看下&#xff0c;在Java中&#xff0c;分别使用什么方式来保证。

原子性

  在Java中&#xff0c;为了保证原子性&#xff0c;提供了两个高级的字节码指令monitorentermonitorexit。在synchronized的实现原理文章中&#xff0c;介绍过&#xff0c;这两个字节码&#xff0c;在Java中对应的关键字就是synchronized

  因此&#xff0c;在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性

  Java内存模型是通过在变量修改后将新值同步回主内存&#xff0c;在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

  Java中的volatile关键字提供了一个功能&#xff0c;那就是被其修饰的变量在被修改后可以立即同步到主内存&#xff0c;被其修饰的变量在每次是用之前都从主内存刷新。因此&#xff0c;可以使用volatile来保证多线程操作时变量的可见性。

  除了volatile&#xff0c;Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同&#xff0c;这里不再展开了。

有序性

  在Java中&#xff0c;可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别&#xff1a;

  volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

  好了&#xff0c;这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了&#xff0c;好像synchronized关键字是万能的&#xff0c;他可以同时满足以上三种特性&#xff0c;这其实也是很多人滥用synchronized的原因。

  但是synchronized是比较影响性能的&#xff0c;虽然编译器提供了很多锁优化技术&#xff0c;但是也不建议过度使用。

Java的内存结构

  Java的内存结构&#xff0c;也就是运行时的数据区域&#xff1b;

  Java虚拟机在执行Java程序的过程中&#xff0c;会把它管理的内存划分为几个不同的数据区域&#xff0c;这些区域都有各自的用途、创建时间、销毁时间。

Java运行时数据区分为下面几个内存区域&#xff1a;

1.PC寄存器/程序计数器&#xff1a;

  严格来说是一个数据结构&#xff0c;用于保存当前正在执行的程序的内存地址&#xff0c;由于Java是支持多线程执行的&#xff0c;所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时&#xff0c;被中断的线程的程序当前执行到哪条内存地址必然要保存下来&#xff0c;以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置&#xff0c;每个线程都需要有一个独立的程序计数器&#xff0c;各个线程之间计数器互不影响&#xff0c;独立存储&#xff0c;我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”&#xff0c;是线程安全的。

2.Java栈 Java Stack&#xff1a;

  Java栈总是与线程关联在一起的&#xff0c;每当创建一个线程&#xff0c;JVM就会为该线程创建对应的Java栈&#xff0c;在这个Java栈中又会包含多个栈帧(Stack Frame)&#xff0c;这些栈帧是与每个方法关联起来的&#xff0c;每运行一个方法就创建一个栈帧&#xff0c;每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时&#xff0c;该栈帧就会弹出栈帧的元素作为这个方法的返回值&#xff0c;并且清除这个栈帧&#xff0c;Java栈的栈顶的栈帧就是当前正在执行的活动栈&#xff0c;也就是当前正在执行的方法&#xff0c;PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用&#xff0c;当在这个栈帧中调用另外一个方法时&#xff0c;与之对应的一个新的栈帧被创建&#xff0c;这个新创建的栈帧被放到Java栈的栈顶&#xff0c;变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用&#xff0c;当这个栈帧中所有指令都完成时&#xff0c;这个栈帧被移除Java栈&#xff0c;刚才的那个栈帧变为活动栈帧&#xff0c;前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

  由于Java栈是与线程对应起来的&#xff0c;Java栈数据不是线程共有的&#xff0c;所以不需要关心其数据一致性&#xff0c;也不会存在同步锁的问题。

  在Java虚拟机规范中&#xff0c;对这个区域规定了两种异常状况&#xff1a;如果线程请求的栈深度大于虚拟机所允许的深度&#xff0c;将抛出StackOverflowError异常&#xff1b;如果虚拟机可以动态扩展&#xff0c;如果扩展时无法申请到足够的内存&#xff0c;就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中&#xff0c;可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

3.堆 Heap:

  堆是JVM所管理的内存中国最大的一块&#xff0c;是被所有Java线程锁共享的&#xff0c;不是线程安全的&#xff0c;在JVM启动时创建。堆是存储Java对象的地方&#xff0c;这一点Java虚拟机规范中描述是&#xff1a;所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域&#xff0c;从内存回收的角度来看&#xff0c;由于现在GC基本都采用分代收集算法&#xff0c;所以Java堆还可以细分为&#xff1a;新生代和老年代&#xff1b;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

4.方法区Method Area:

  方法区存放了要加载的类的信息&#xff08;名称、修饰符等&#xff09;、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息&#xff0c;当在程序中通过Class对象的getName.isInterface等方法来获取信息时&#xff0c;这些数据都来源于方法区。方法区是被Java线程锁共享的&#xff0c;不像Java堆中其他部分一样会频繁被GC回收&#xff0c;它存储的信息相对比较稳定&#xff0c;在一定条件下会被GC&#xff0c;当方法区要使用的内存超过其允许的大小时&#xff0c;会抛出OutOfMemory的错误信息。方法区也是堆中的一部分&#xff0c;就是我们通常所说的Java堆中的永久区 Permanet Generation&#xff0c;大小可以通过参数来设置,可以通过-XX:PermSize指定初始值&#xff0c;-XX:MaxPermSize指定最大值。

5.常量池Constant Pool:

  常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定&#xff0c;并保存在已编译的.class文件中。一般分为两类&#xff1a;字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候&#xff0c;根据方法名找到方法的引用&#xff0c;并以此定为到函数体进行函数代码的执行。引用量包含&#xff1a;类和接口的权限定名、字段的名称和描述符&#xff0c;方法的名称和描述符。

6.本地方法栈Native Method Stack:

  本地方法栈和Java栈所发挥的作用非常相似&#xff0c;区别不过是Java栈为JVM执行Java方法服务&#xff0c;而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

主内存和工作内存&#xff1a;

  Java内存模型的主要目标是定义程序中各个变量的访问规则&#xff0c;即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步&#xff0c;它包含了实例字段、静态字段和构成数组对象的元素&#xff0c;但不包含局部变量和方法参数&#xff0c;因为后者是线程私有的&#xff0c;不会共享&#xff0c;当然不存在数据竞争问题&#xff08;如果局部变量是一个reference引用类型&#xff0c;它引用的对象在Java堆中可被各个线程共享&#xff0c;但是reference引用本身在Java栈的局部变量表中&#xff0c;是线程私有的&#xff09;。为了获得较高的执行效能&#xff0c;Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互&#xff0c;也没有限制即时编译器进行调整代码执行顺序这类优化措施。

  JMM规定了所有的变量都存储在主内存&#xff08;Main Memory&#xff09;中。每个线程还有自己的工作内存&#xff08;Working Memory&#xff09;,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝&#xff0c;线程对变量的所有操作&#xff08;读取、赋值等&#xff09;都必须在工作内存中进行&#xff0c;而不能直接读写主内存中的变量&#xff08;volatile变量仍然有工作内存的拷贝&#xff0c;但是由于它特殊的操作顺序性规定&#xff0c;所以看起来如同直接在主内存中读写访问一般&#xff09;。不同的线程之间也无法直接访问对方工作内存中的变量&#xff0c;线程之间值的传递都需要通过主内存来完成。

线程1和线程2要想进行数据的交换一般要经历下面的步骤&#xff1a;

  1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去。

  2.线程2到主内存中去读取线程1刷新过的共享变量&#xff0c;然后copy一份到工作内存2中去。

总结

  在读完本文之后&#xff0c;相信你应该了解了什么是Java内存模型、Java内存模型的作用以及Java中内存模型做了什么事情等。关于Java中这些和内存模型有关的关键字&#xff0c;希望读者还可以继续深入学习&#xff0c;并且自己写几个例子亲自体会一下。

  可以参考《深入理解Java虚拟机》和《Java并发编程的艺术》两本书。

出处&#xff1a;https://www.hollischuang.com/archives/2550

      https://www.cnblogs.com/lewis0077/p/5143268.html

 

转:https://www.cnblogs.com/myseries/p/10730255.html



推荐阅读
  • 兆芯X86 CPU架构的演进与现状(国产CPU系列)
    本文详细介绍了兆芯X86 CPU架构的发展历程,从公司成立背景到关键技术授权,再到具体芯片架构的演进,全面解析了兆芯在国产CPU领域的贡献与挑战。 ... [详细]
  • 本文将介绍如何在混合开发(Hybrid)应用中实现Native与HTML5的交互,包括基本概念、学习目标以及具体的实现步骤。 ... [详细]
  • 为什么多数程序员难以成为架构师?
    探讨80%的程序员为何难以晋升为架构师,涉及技术深度、经验积累和综合能力等方面。本文将详细解析Tomcat的配置和服务组件,帮助读者理解其内部机制。 ... [详细]
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • Spring Boot 中配置全局文件上传路径并实现文件上传功能
    本文介绍如何在 Spring Boot 项目中配置全局文件上传路径,并通过读取配置项实现文件上传功能。通过这种方式,可以更好地管理和维护文件路径。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 阿里巴巴终面技术挑战:如何利用 UDP 实现 TCP 功能?
    在阿里巴巴的技术面试中,技术总监曾提出一道关于如何利用 UDP 实现 TCP 功能的问题。当时回答得不够理想,因此事后进行了详细总结。通过与总监的进一步交流,了解到这是一道常见的阿里面试题。面试官的主要目的是考察应聘者对 UDP 和 TCP 在原理上的差异的理解,以及如何通过 UDP 实现类似 TCP 的可靠传输机制。 ... [详细]
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 本文深入探讨了Java多线程环境下的同步机制及其应用,重点介绍了`synchronized`关键字的使用方法和原理。`synchronized`关键字主要用于确保多个线程在访问共享资源时的互斥性和原子性。通过具体示例,如在一个类中使用`synchronized`修饰方法,展示了如何实现线程安全的代码块。此外,文章还讨论了`ReentrantLock`等其他同步工具的优缺点,并提供了实际应用场景中的最佳实践。 ... [详细]
  • 【系统架构师精讲】(16):操作系统核心概念——寄存器、内存与缓存机制详解
    在计算机系统架构中,中央处理器(CPU)内部集成了多种高速存储组件,用于临时存储指令、数据和地址。这些组件包括指令寄存器(IR)、程序计数器(PC)和累加器(ACC)。寄存器作为集成电路中的关键存储单元,由触发器构成,具备极高的读写速度,使得数据传输非常迅速。根据功能不同,寄存器可分为基本寄存器和移位寄存器,各自在数据处理中发挥重要作用。此外,寄存器与内存和缓存机制的协同工作,确保了系统的高效运行。 ... [详细]
  • 开发日志:201521044091 《Java编程基础》第11周学习心得与总结
    开发日志:201521044091 《Java编程基础》第11周学习心得与总结 ... [详细]
  • 在分析和解决 Keepalived VIP 漂移故障的过程中,我们发现主备节点配置如下:主节点 IP 为 172.16.30.31,备份节点 IP 为 172.16.30.32,虚拟 IP 为 172.16.30.10。故障表现为监控系统显示 Keepalived 主节点状态异常,导致 VIP 漂移到备份节点。通过详细检查配置文件和日志,我们发现主节点上的 Keepalived 进程未能正常运行,最终通过优化配置和重启服务解决了该问题。此外,我们还增加了健康检查机制,以提高系统的稳定性和可靠性。 ... [详细]
author-avatar
手机用户2502915601
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有