很多小伙伴都对Java的内存有了解吧,本期结合大量图片给大家好好看看!!!

白嫖资料
Java 虚拟机内部使用 JMM(Java 内存模型) 将内存划分为两个逻辑单元,线程栈(或者叫本地内存)和堆。

每一个线程都有属于自己的线程栈,在线程栈中会保存局部变量(也叫做本地变量)、方法中定义的参数和异常处理器的参数(catch中的参数);这些参数和变量都属于线程局部操作,会被隔离,所以不受内存模型影响。

可见性问题

假设有下面这样一组代码,:

static boolean run = true;
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();sleep(1);run = false; // 线程 t 不会如预想的停下来
}

下面是执行步骤:

  • 线程 t 刚开始执行时,会从堆中将 run 的变量值读取一份保存到自己的线程栈中(也就是在自己的线程栈中创建了一个副本)。注意:这样做是为了减少对堆中 run 的访问,提高效率。
  • 这时线程 t 每次读取 run 的值,都是从自己的线程栈中读取的。
  • 当 1 秒之后,main 线程修改了 run 的值,并同步到堆中,而 t 读取 run 的值时,还从自己的线程栈中读取,线程栈中的 run 值并没有被改变,所以线程一直没法停止。
    白嫖资料
    总结出来就是一句话:另一个线程修改了堆中的数据,其它线程没办法感知这个数据被修改了。

下面是一个对象在内存中分布的例子:

public class MyRunnable implements Runnable() {public void run() {methodOne();}public void methodOne() {int localVariable1 = 45;MySharedObject localVariable2 = MySharedObject.sharedInstance;//... do more with local variables.methodTwo();}public void methodTwo() {Integer localVariable1 = new Integer(99);//... do more with local variable.}
}public class MySharedObject {//static variable pointing to instance of MySharedObjectpublic static final MySharedObject sharedInstance = new MySharedObject();//member variables pointing to two objects on the heap
//加裙1025684353一起吹水聊天public Integer object2 = new Integer(22);public Integer object4 = new Integer(44);public long member1 = 12345;public long member2 = 67890;
}

每个执行 methodOne() 的线程都会在各自的线程栈上创建自己的 localVariable1 和 localVariable2 的副本;localVariable1 副本之间将完全分离,只活在每个线程的线程栈上;一个线程不能看到另一个线程对 localVariable1 的做了什么改变,就是说对其它线程不可见(就算一个线程将修改的值同步回堆中,另一个线程没有重新读取该值,对这个线程来说就是不可见的),也不受到内存模型的影响。

localVariable2 在不同线程栈中也有不同的副本,但是这两个不同副本最终都指向静态变量所引用的对象(堆上的同一个对象);因此,localVariable2 的两个副本最终都指向静态变量指向的 MySharedObject 实例。MySharedObject 实例也存储在堆上。它对应于下图中的 Object 3。

注意:MySharedObject 类也包含了四个成员变量,这些成员变量和对象一起存储在堆上,其中两个成员变量指向另外两个 Integer 对象,这些 Integer 对象对应上图中的 Object 2 和 Object 4。

在 methodTwo() 方法中创建了一个名为 localVariable1 的局部变量,这个局部变量是对 Integer 对象的引用,localVariable1 引用将在每个执行 methodTwo() 的线程中存储一个副本;但是由于该方法每次执行时都会创建一个新的 Integer 对象,所以 localVariable1 引用指向一个新的 Integer 实例,对应下图中的 Object 1 和 Object 5。
白嫖资料

  • 值得注意的是:对于基本数据类型(boolean、byte、short、char、int、long、float、double)
    都是直接保存变量的副本到自己的线程栈中,而对于引用类型(Byte、Integer、Long 等) 它的引用(就是变量名)
    是保存在自己的线程栈中,对象本身是保存在堆中的。还有就是堆内存中存放了所有的实例字段(也可以叫做实例域)、静态字段(也可以叫做静态域)和数组元素(数组也是对象),堆内存在多个线程之间共享。
    最后需要注意一点,当方法中调用了类变量(也称静态变量、静态域)和实例变量(也称实例域、非静态域)时,也会保存这些变量的副本到线程栈中。

解决可见性问题

最简单的方式就是使用 volatile 修饰符,将代码改成 volatile static boolean run = true;,这样线程 t 每次使用到这个值的时候都会从堆中获取。

值得注意的是:该关键字可以用来修饰成员变量和静态成员变量。

当然也可以使用 synchronized 来解决可见性。

重排序

为了提高程序性能,在不影响单线程执行结果的前提下 CPU 和 JIT 编译器会做指令重排序操作(即生成的机器指令与字节码指令顺序不一致)。

这是一个单线程中重排序的例子,假设有下面这样一段代码:
白嫖资料

a = b + c
d = a + el = m + n
y = x + z

重排序后,可能会变成下面这个样子,由于前三行没有任何关联(关联指的是数据之间依赖,例如第六行代码依赖第一行代码,所以他们之间是有关联的),CPU 有可能会并行执行前三行,从而提高程序性能。

a = b + cl = m + n
y = x + z//加裙1025684353一起吹水聊天d = a + e

使用图形展示了多核 CPU 重排序和指令执行的情况:

通过上面的图片可以发现,CPU 执行指令并不是随随便便执行,和重排序时一样必须遵循某个规则;这个规则就是 as-if-serial 语义:所有的动作都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。Java 编译器、运行时和处理器都会保证单线程下的 as-if-serial 语义。比如,为了保证这一语义,重排序不会发生在有数据依赖的操作之中。

重排序对多线程的影响

请先看这样一段代码:

public class PossibleReordering {static int x = 0, y = 0;
static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException {Thread one = new Thread(new Runnable() {public void run() {a = 1;x = b;}});Thread other = new Thread(new Runnable() {public void run() {b = 1;y = a;}//加裙1025684353一起吹水聊天});one.start();other.start();one.join();other.join();System.out.println(“(” + x + “,” + y + “)”);
}

白嫖资料
很容易想到这段代码的运行结果可能为(1,0)、(0,1)或(1,1),因为线程 one 可以在线程 two 开始之前就执行完了,也有可能反之,甚至有可能二者的指令是同时或交替执行的。

然而,这段代码的执行结果也可能是(0,0). 因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。得到(0,0)结果的语句执行过程,如下图所示。值得注意的是,a=1 和 x=b这 两个语句的赋值操作的顺序被颠倒了,或者说,发生了指令“重排序”(reordering)。(事实上,输出了这一结果,并不代表一定发生了指令重排序,内存可见性问题也会导致这样的输出)

Happens-before 关系

Java 中对 Happens-before 定义为:如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在
Happens-before 关系。也就是说,Happens-before 的前后两个操作不会被重排序且后者对前者的内存可见。
值得注意的是:这里说的两个操作既可以是在一个线程之内,也可以是在不同线程之间。还有就是这两个操作并不是说一定要一前一后执行,Happens-before
只要求第一个操作的结果对第二个操作可见,并且第一个操作排在第二个操作之前就可以。 简单点来说就是:Happens-before
就是可见性规则;什么情况下对共享变量的写,可以对共享变量的读是可见的。

根据 Java 内存模型中的规定,可以总结出以下几条 happens-before 规则:
白嫖资料

  • 单线程中代码执行顺序:A happens-before B。之前的操作(变量赋值),对之后的操作(变量取值)是可见的(就是说,变量的值是可见的)。
  • synchronized:第一个线程释放了锁后,对第二个要加锁的线程可见。注意:假设第二个线程没有获取锁,而是直接使用共享变量,这样不行(不可见)。
  • volatile:第一个线程的写,对第二个线程的读是可见的。像个线程都是直接读写堆内存。
    线程启动。
  • 线程结束:其它线程调用了 Thread.join(等到该方法返回)或者调用了 Thread.isAlive(必须返回 false)时,才对调用的线程可见。
  • 中断:一个线程 Thread#interrupt() 另一个线程后,共享变量的值对被打断的线程可见。
  • 终结:一个对象的构造函数的结束 happens-before 于这个对象 finalizer 的开始。
  • 传递性:如果 A happens-before B,且 B happens-before C,A happens-before C。

Happens-before 关系只是对 Java 内存模型的一种近似性的描述,它并不够严谨,但便于日常程序开发参考使用,关于更严谨的 Java 内存模型的定义和描述,请阅读 JSR-133 原文或 Java 语言规范章节 17.4。

除此之外,Java 内存模型对 volatile 和 final 的语义做了扩展。对 volatile 语义的扩展保证了 volatile 变量在一些情况下不会重排序,volatile 的 64 位变量 double 和 long 的读取和赋值操作都是原子的。对 final 语义的扩展保证一个对象的构建方法结束前,所有 final 成员变量都必须完成初始化(的前提是没有 this 引用溢出)。

happens-before 与 JMM 的关系如下图所示:

JMM 会根据 happens-before 禁止处理器对某些代码重排序,并保证共享变量的可见性;关于 Java 内存模型重排序的规定以及 volatile 关键字的详解,都可以查看 volatile 关键字。

讨论-为什么堆中存放的是字段和数组元组呢?

Java 中的对象是复合型对象,是由 Object + 原生类型的的字段或数组组成。

程序中使用的和 CPU 所看到的都是数据,数据是通过物理地址加偏移(offset)得到。

总结

线程栈和堆、happens-before、重排序、内存屏障这些都属于 Java 内存模型(Java Memory Model)。

知识拓展-原子性

原子(Atomic)就是不可分割(就是不可被拆分)的意思。

就是说,多线程操作同一个共享变量时,不能交叉操作;例如 t1 线程操作一下,然后 t2 线程操作一下,这样就没有办法保证变量的原子性;所以必须要等待其中一个线程操作完成后,另一个线程才能继续操作。

知识拓展-Java 异常处理机制中的重排序

为保证 as-if-serial 语义,Java 异常处理机制也会为重排序做一些特殊处理。

例如在下面的代码中,y = 0 / 0 可能会被重排序在 x = 2 之前执行,为了保证最终不致于输出 x = 1 的错误结果,JIT 在重排序时会在 catch 语句中插入错误代偿代码,将 x 赋值为 2,将程序恢复到发生异常时应有的状态。

这种做法的确将异常捕捉的逻辑变得复杂了,但是 JIT 的优化的原则是,尽力优化正常运行下的代码逻辑,哪怕以 catch 块逻辑变得复杂为代价,毕竟,进入 catch 块内是一种“异常”情况的表现。

public class Reordering {public static void main(String[] args) {int x, y;x = 1;try {x = 2;y = 0 / 0;    } catch (Exception e) {} finally {System.out.println("x = " + x);}//加裙1025684353一起吹水聊天}
}

白嫖资料

知识拓展-内存系统重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令集并行的重排序:现在处理器采用了指令集并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    前两种在本文已经有写过,这里针对第三种做详细说明。

计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。其模型如下图所示:

在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。

这导致在同一个时间点,各 CPU 所看到同一内存地址的数据的值可能是不一致的。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。

有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”。因为这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。

从 Java 源代码到最终实际执行的指令顺序,会分别经历下面三种重排序:

最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。 可以的话请给我一个三连支持一下我哟,我们下期再见

白嫖资料

来,让我康康!里面到底是什么样子的?Java内存模型相关推荐

  1. 服务器到底长什么样子啊(#゚Д゚)?

    服务器到底长什么样子啊(#゚Д゚)? 学了计算机网络,客户机可以理解,因为手边的笔记本就是客户机,但服务器长什么样啊,也是和pc一样吗?和pc那里不一样啊?是硬件还是软件呢? 服务器一般放在机柜中,机 ...

  2. 高级程序员到底长什么样子?

    我们的中国文化,对"面子"看得特别重,所以你会发现身边到处都是高级 XXX,听着倍儿有面子,程序员也不例外. 但是你真要问每个人,你认为的高级 XXX 是什么样子的,估计每个人都有 ...

  3. Java到底是什么?学习java可以做什么呢?

    Java到底是什么?在互联网信息高速发展的时代,java技术的应用无处不在,网页.手机系统.软件.游戏等这些成果展现在我们面前,有java的功劳.综上可看出java的应用非常广,与其相应的工作岗位就多 ...

  4. 手机到底应该选128G还是64G内存呢?其实很多人都选错了

    随着科技的不断发展和进步,我们的生活也变得越来越好,智能手机取代了功能性手机成为了我们生活的必需品,而一直以来智能手机市场的竞争都是非常激烈的,想要在激烈的竞争中站稳找个也并不是一件容易的事情,为了能 ...

  5. 阿里业务中台到底是什么样子

    上篇<为什么企业要有数字平台战略?"中台"又是什么?>中,我依据自己的经验和理解,阐述了中台产生的原因以及最终建设目的.本文再通过我的收集和思考,介绍一下中台到底是什么 ...

  6. 真实的【保研夏令营】!985的计算机夏令营到底是什么样子?

    四川大学的微信公众号[川大计算机软件研究生]前几天一直在发 四川大学计算机学院的 保研夏令营的具体活动,于是小编转载过来,给同学们看看计算机学院的夏令营到底是什么样子. 为了促进高校优秀大学生之间的学 ...

  7. 大家在寻找的高级程序员到底是什么样子的?

    你好,我是Z哥. 这篇文章主题很简单,就是一个很常见的话题"什么是高级程序员?". 文章稍微长了些,但是很容易阅读. 我们的中国文化,对"面子"看的特别重,所以 ...

  8. Splunklive!2018北京站激情开场:合格的大数据处理平台到底是什么样子?

    作为本届Splunk>live!2018中国用户大会的收官之战,北京站的会场流程以及议题设置更加严谨,并且下午双会场并行,以期真正让所有参会者有所收获.在IT世界,真正的基础设施是什么?一切热门 ...

  9. 英语不好到底适不适合学习java

    相信很多同学在考虑学习java前都有一个疑问:英语不好到底可以学习java吗? 单问能不能,是可以的,但是完全不懂,这个也不太行,至少要懂点. Java中,关键字的数量有多少呢? <img sr ...

最新文章

  1. Android8.0运行时权限策略变化和适配方案
  2. 大数据分布式集群搭建(7)
  3. bfs迷宫寻路问题(一看就懂的讲解)
  4. Angular jasmine单元测试框架里expect.toHaveBeenCalled的工作原理
  5. SAP MES接收生产订单及工艺路线
  6. linux内核源码代码量,Linux内核源代码数量已经超过1000万行
  7. 计算机两年发展,计算机发展历史
  8. 8、Python-函数
  9. 怎样在VS2013/MFC中使用TeeChart绘图控件
  10. html语言format,HTML 文本格式化
  11. 产品配件类目税目分类_商品及税收分类编码选择技巧
  12. [原创]Ladon7.5大型内网渗透扫描器Cobalt Strike
  13. 联合概率分布、边缘概率分布
  14. 安卓如何调出软键盘_Android软键盘显示模式及打开和关闭方式(推荐)
  15. 尚硅谷-SpringBoot1.5.9(已过时,直接学2)
  16. MySql 如何查询某一天内的数据
  17. 将毫秒转换为时间(HH:ss:mm)
  18. 树莓派一键部署私有云
  19. 吴恩达机器学习作业2:逻辑回归(Python实现)
  20. 画一条连接两点的线,由两点坐标确定一条直线

热门文章

  1. 征途linux版服务端修改说明,挑战服务端修改技能图文教程
  2. Macbook启动台顽固应用图标删除方法
  3. Nu2menu 插件
  4. 人人都是产品经理系列一
  5. jack设计的esp01s 1路智能开关pcb
  6. java发送邮件,多人单人发送,抄送,密送,附件
  7. 2011-这个冬天有点冷
  8. 【Linux】上传和下载服务器上的文件
  9. c++版本opencv(36.霍夫直线检测37.直线类型与线段-)
  10. 安卓背景音乐开关_微信7.0.4内测版怎么申请?微信7.0.4安卓内测版下载安装教程...