JVM内存划分:

Jvm各个部分存储的内容

file

:1、用来存储new出来的对象和其属性。2、线程共享

:1、存储运行时的数据(方法、对象、变量等) 2、每个线程都有自己的栈,线程私有  3、先进后出(类似子弹夹最后一个压进去的子弹最先打出来)

方法区:1、字节码文件加载时候存放区域,分为静态区(静态方法、变量、长量)和非静态区2、线程共享

程序计数区:程序执行到某一行的行号指示器,唯一一个不会发生溢出的,也是线程私有

本地native区:提供给java程序调用本地程序的中间程序,线程私有一般用c或者c++写的,我们只管调用即可。

结合下图分析:执行main函数时候JVM做了什么?

file

执行Demo1_Memory中main方法时,JVM做了以下内容(按顺序进行):

  • 1、【进栈】加载Demo1_Memory.class到方法区因为这里有main方法是程序的入口,在Demo1_Memory方法区 中的静态区加载main方法,此类无非静态方法,所以非静态区无内容

  • 2、当前线程会在栈中开辟属于main方法的私有栈空间。(每一个线程都有自己的私有栈空间,线程间不共享不可见)

  • 3、执行main方法 Student s=new Student();这句执行了很多步骤具体如下:

    1. 3.1 将student.clas加载到方法区

    2. 3.2将student类中的静态方法 school放入静态区并初始化值null并完成赋值。非静态区的name 、age不赋值,并加载非静态方法show();

    3. 3.3在堆内存中开辟一块空间存放new的student实例。

    4. 3.4初始化堆内存空间的student实例的变量值,age=0,name=null

    5. 3.5将堆内存student实例的地址给Student s=new Student()中的变量

    6. 3.6执行s.name="张三",修改堆内存中student实例name变量的值

    7. 3.7根据堆内存中student的成员方法show的地址,找到方法并执行。

  • 4.【弹栈】执行完毕main方法内容(忽略 Student s2=new Student()因为顺序和上面一样)。弹出main方法这块,释放main的栈空间,重新执行其他内容(若有)

 

注意:蓝色内容处多线程时会发生指令重排,我们希望顺序是3.3->3.4->3.5但有可能是3.4->3.5->3.4解决这个问题可以使用volatile关键词修饰变量即可。

补充知识: 想看懂上面必须了解以下知识点

1、多线程间与堆内存交互模型

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图 file

2、多线程下存在的问题

1、缓存一致性问题 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等 file 2、指令重排序问题 cpu执行一条指令分为以下几个步骤: 1、取指 IF 2、译码和取寄存器操作数 ID 3、执行或者有效地址计算 EX 4、存储器访问 MEM 5、写回 WB 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序 举例说明: file 两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现x1 = 1 和x2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况 file 这种执行顺序下就有可能出现x1 = 1 和x2 = 2 的情况,这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

volatile意义

多线程都需要并存储在自己的栈空间一份“共享变量的副本”,若其中某个线程修改了共享变量并刷新到堆内存中,这时候,其他变量不知情,存储的依旧是旧的数据。此时如果使用了volatile就可以避免这样的情况,因为:

  • 1、保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。原理:当读取一个volatile变量时,JMM会把该栈的“共享变量的副本”置为无效,那么该线程将只能从主内存中重新读取共享变量。

  • 拓展: JMM就是一组规则,是JVM进程,是抽象的策略,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

  • 2、禁止指令重排序优化

volatile是如何实现禁止指令重排优化?

  • 先了解一个概念,内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化,如下图: file

    关于版权:本文是我学习过程中根据自己学习心得,整理所成,视频教程来自传智播客黑马程序员,部分图片来自CSDN