实验室网站建设重要性,东莞做网站贴吧,网络推广培训班哪家好,厦门网站制作计划JVM 基础 - Java 内存模型引入 很多人都无法区分Java内存模型和JVM内存结构#xff0c;以及Java内存模型与物理内存之间的关系。本文是JVM第六讲#xff0c;从堆栈角度引入JMM#xff0c;然后介绍JMM和物理内存之间的关系, 为后面JMM详解, JVM 内存结构详解, Java 对象模型详…JVM 基础 - Java 内存模型引入 很多人都无法区分Java内存模型和JVM内存结构以及Java内存模型与物理内存之间的关系。本文是JVM第六讲从堆栈角度引入JMM然后介绍JMM和物理内存之间的关系, 为后面JMM详解, JVM 内存结构详解, Java 对象模型详解等做铺垫。 文章目录 JVM 基础 - Java 内存模型引入1、JMM引入1.1、从堆栈说起1.2、堆栈里面放了什么?1.3、线程栈如何访问堆上对象?1.4、线程栈访问堆示例 2、JMM与硬件内存结构关系2.1、硬件内存结构简介2.2、JMM与硬件内存连接 - 引入2.3、JMM与硬件内存连接 - 对象共享后的可见性2.4、JMM与硬件内存连接 - 竞态条件 3、参考文章 1、JMM引入
1.1、从堆栈说起
JVM内部使用的Java内存模型在线程栈和堆之间划分内存。 此图从逻辑角度说明了Java内存模型
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
1.2、堆栈里面放了什么?
线程堆栈还包含正在执行的每个方法的所有局部变量(调用堆栈上的所有方法)。 线程只能访问它自己的线程堆栈。 由线程创建的局部变量对于创建它的线程以外的所有其他线程是不可见的即使两个线程正在执行完全相同的代码两个线程仍将在每个自己的线程堆栈中创建该代码的局部变量。 因此每个线程都有自己的每个局部变量的版本。
基本类型的所有局部变量(booleanbyteshortcharintlongfloatdouble)完全存储在线程堆栈中因此对其他线程不可见。 一个线程可以将一个基本类型变量的副本传递给另一个线程但它不能共享原始局部变量本身。
堆包含了在Java应用程序中创建的所有对象无论创建该对象的线程是什么。 这包括基本类型的包装类(例如ByteIntegerLong等)。 无论是创建对象并将其分配给局部变量还是创建为另一个对象的成员变量该对象仍然存储在堆上。 局部变量可以是基本类型在这种情况下它完全保留在线程堆栈上。
局部变量也可以是对象的引用。 在这种情况下引用(局部变量)存储在线程堆栈中但是对象本身存储在堆(Heap)上。
对象的成员变量与对象本身一起存储在堆上。 当成员变量是基本类型时以及它是对象的引用时都是如此。
静态类变量也与类定义一起存储在堆上。
1.3、线程栈如何访问堆上对象?
所有具有对象引用的线程都可以访问堆上的对象。 当一个线程有权访问一个对象时它也可以访问该对象的成员变量。 如果两个线程同时在同一个对象上调用一个方法它们都可以访问该对象的成员变量但每个线程都有自己的局部变量副本。 两个线程有一组局部变量。 其中一个局部变量(局部变量2)指向堆上的共享对象(对象3)。 两个线程各自对同一对象具有不同的引用。 它们的引用是局部变量因此存储在每个线程的线程堆栈中(在每个线程堆栈上)。 但是这两个不同的引用指向堆上的同一个对象。
注意共享对象(对象3)如何将对象2和对象4作为成员变量引用(由对象3到对象2和对象4的箭头所示)。 通过对象3中的这些成员变量引用两个线程可以访问对象2和对象4。
该图还显示了一个局部变量该变量指向堆上的两个不同对象。在这种情况下引用指向两个不同的对象(对象1和对象5)而不是同一个对象。 理论上如果两个线程都引用了两个对象则两个线程都可以访问对象1和对象5。 但是在上图中每个线程只引用了两个对象中的一个。
1.4、线程栈访问堆示例
那么什么样的Java代码可以导致上面的内存图? 好吧代码就像下面的代码一样简单
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 heappublic Integer object2 new Integer(22); //对象引用变量public Integer object4 new Integer(44); //对象引用变量public long member1 12345; // 基本类型变量public long member1 67890; // 基本类型变量
}如果两个线程正在执行run()方法则前面显示的图表将是结果。 run()方法调用methodOne()methodOne()调用methodTwo()。
methodOne()声明一个局部基本类型变量(类型为int的localVariable1)和一个局部变量它是一个对象引用(localVariable2)。
执行methodOne()的每个线程将在各自的线程堆栈上创建自己的localVariable1和localVariable2副本。 localVariable1变量将完全相互分离只存在于每个线程的线程堆栈中。 一个线程无法看到另一个线程对其localVariable1副本所做的更改。
执行methodOne()的每个线程也将创建自己的localVariable2副本。 但是localVariable2的两个不同副本最终都指向堆上的同一个对象。 代码将localVariable2设置为指向静态变量引用的对象。 静态变量只有一个副本此副本存储在堆上。 因此localVariable2的两个副本最终都指向静态变量指向的MySharedObject的同一个实例。 MySharedObject实例也存储在堆上。 它对应于上图中的对象3。
注意MySharedObject类还包含两个成员变量。 成员变量本身与对象一起存储在堆上。 两个成员变量指向另外两个Integer对象。 这些Integer对象对应于上图中的Object 2和Object 4。
另请注意methodTwo()如何创建名为localVariable1的局部变量。 此局部变量是对Integer对象的对象引用。该方法将localVariable1引用设置为指向新的Integer实例。 localVariable1引用将存储在执行methodTwo()的每个线程的一个副本中。 实例化的两个Integer对象将存储在堆上但由于该方法每次执行该方法时都会创建一个新的Integer对象因此执行此方法的两个线程将创建单独的Integer实例。 在methodTwo()中创建的Integer对象对应于上图中的Object 1和Object 5。
另请注意类型为long的MySharedObject类中的两个成员变量它们是基本类型。 由于这些变量是成员变量因此它们仍与对象一起存储在堆上。 只有局部变量存储在线程堆栈中。
2、JMM与硬件内存结构关系
2.1、硬件内存结构简介
现代硬件内存架构与内部Java内存模型略有不同。了解硬件内存架构也很重要以了解Java内存模型如何与其一起工作。 本节介绍了常见的硬件内存架构后面的部分将介绍Java内存模型如何与其配合使用。
这是现代计算机硬件架构的简化图
现代计算机通常有2个或更多CPU。其中一些CPU也可能有多个内核。关键是在具有2个或更多CPU的现代计算机上可以同时运行多个线程。每个CPU都能够在任何给定时间运行一个线程。这意味着如果您的Java应用程序是多线程的线程真的在可能同时运行。
每个CPU基本上都包含一组在CPU内存中的寄存器。CPU可以在这些寄存器上执行的操作比在主存储器中对变量执行的操作快得多。这是因为CPU可以比访问主存储器更快地访问这些寄存器。
每个CPU还可以具有CPU高速缓存存储器层。 事实上大多数现代CPU都有一些大小的缓存存储层。CPU可以比主存储器更快地访问其高速缓存存储器但通常不会像访问其内部寄存器那样快。 因此CPU高速缓存存储器介于内部寄存器和主存储器的速度之间。某些CPU可能有多个缓存层(级别1和级别2)但要了解Java内存模型如何与内存交互这一点并不重要。 重要的是要知道CPU可以有某种缓存存储层。
计算机还包含主存储区(RAM)。 所有CPU都可以访问主内存。主存储区通常比CPU的高速缓存存储器大得多。同时访问速度也就较慢。
通常当CPU需要访问主存储器时它会将部分主存储器读入其CPU缓存。 它甚至可以将部分缓存读入其内部寄存器然后对其执行操作。当CPU需要将结果写回主存储器时它会将值从其内部寄存器刷新到高速缓冲存储器并在某些时候将值刷新回主存储器。
各硬件的性能差异见这篇文章性能优化-每个程序员都应该知道的数字
2.2、JMM与硬件内存连接 - 引入
如前所述Java内存模型和硬件内存架构是不同的。硬件内存架构不区分线程堆栈和堆。在硬件上线程堆栈和堆都位于主存储器中。线程堆栈和堆的一部分有时可能存在于CPU高速缓存和内部CPU寄存器中。 这在图中说明 当对象和变量可以存储在计算机的各种不同存储区域中时可能会出现某些问题。 两个主要问题是
可见性问题Visibility of thread updates (writes) to shared variables.竞态条件Race conditions when reading, checking and writing shared variables.
以下各节将解释这两个问题。
2.3、JMM与硬件内存连接 - 对象共享后的可见性
如果两个或多个线程共享一个对象而没有正确使用volatile声明或同步则一个线程对共享对象的更新可能对其他线程不可见。
想象一下共享对象最初存储在主存储器中。然后在CPU上运行的线程将共享对象读入其CPU缓存中。它在那里对共享对象进行了更改。 只要CPU缓存尚未刷新回主内存共享对象的更改版本对于在其他CPU上运行的线程是不可见的。 这样每个线程最终都可能拥有自己的共享对象副本每个副本都位于不同的CPU缓存中。
下图描绘了该情况。在左CPU上运行的一个线程将共享对象复制到其CPU缓存中并将其count变量更改为2对于在右边的CPU上运行的其他线程此更改不可见因为计数更新尚未刷新回主内存中。
要解决此问题您可以使用Java的volatile关键字。volatile关键字可以确保直接从主内存读取给定变量并在更新时始终写回主内存。
volatile关键字详解可以参考这篇文章二面阿里竟然败在了 volatile 关键字上
2.4、JMM与硬件内存连接 - 竞态条件
如果两个或多个线程共享一个对象并且多个线程更新该共享对象中的变量则可能会出现竞态。
想象一下如果线程A将共享对象的变量计数读入其CPU缓存中。想象一下线程B也做同样的事情但是进入不同的CPU缓存。现在线程A将一个添加到count而线程B执行相同的操作。现在var1已经增加了两次每个CPU缓存一次。
如果这些增量是按先后顺序执行的则变量计数将增加两次并将原始值 2写回主存储器。
但是两个增量同时执行而没有适当的同步。 无论线程A和B中哪一个将其更新后的计数版本写回主存储器更新的值将仅比原始值高1尽管有两个增量。
该图说明了如上所述的竞争条件问题的发生
要解决此问题您可以使用Java synchronized块。 同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分。 同步块还保证在同步块内访问的所有变量都将从主存储器中读入当线程退出同步块时所有更新的变量将再次刷新回主存储器无论变量是不是声明为volatile
3、参考文章
Java Memory Model 英文Java Memory Model 中文版