了解 JVM 中的内存
如您所知,JVM 在其内部运行 Java 程序。像任何虚拟机一样,它有自己的内存组织系统。
内部存储器布局指示 Java 应用程序的工作方式。通过这种方式,可以识别应用程序和算法运行中的瓶颈。让我们看看它是如何工作的。
重要的!原来的Java模型不够好,所以在Java 1.5中进行了修改。这个版本一直沿用至今(Java 14+)。
线程栈
JVM内部使用的Java内存模型将内存分为线程栈和堆。我们来看下Java内存模型,逻辑上分为块:
JVM 中运行的所有线程都有自己的堆栈。反过来,堆栈保存有关线程调用了哪些方法的信息。我将其称为“调用堆栈”。一旦线程执行其代码,调用堆栈就会恢复。
线程的堆栈包含在线程堆栈上执行方法所需的所有局部变量。一个线程只能访问它自己的栈。局部变量对其他线程不可见,仅对创建它们的线程可见。在两个线程执行相同代码的情况下,它们都创建自己的局部变量。因此,每个线程都有自己的每个局部变量版本。
所有基本类型的局部变量(boolean、byte、short、char、int、long、float、double)都完全存储在线程堆栈中,对其他线程不可见。一个线程可以将原始变量的副本传递给另一个线程,但不能共享原始局部变量。
堆
堆包含在您的应用程序中创建的所有对象,无论哪个线程创建了该对象。这包括基本类型的包装器(例如,Byte、Integer、Long等)。不管对象是被创建并赋值给局部变量,还是被创建为另一个对象的成员变量,它都存储在堆上。
下图说明了调用堆栈和局部变量(它们存储在堆栈中)以及对象(它们存储在堆中):
在局部变量是原始类型的情况下,它存储在线程的堆栈中。
局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈中,但对象本身存储在堆中。
一个对象包含方法,这些方法包含局部变量。这些局部变量也存储在线程堆栈中,即使拥有该方法的对象存储在堆中。
对象的成员变量与对象本身一起存储在堆中。当成员变量是原始类型和对象引用时都是如此。
静态类变量也与类定义一起存储在堆中。
与对象的交互
所有引用该对象的线程都可以访问堆上的对象。如果一个线程可以访问一个对象,那么它就可以访问该对象的变量。如果两个线程同时调用同一个对象的方法,它们都可以访问该对象的成员变量,但每个线程都有自己的局部变量副本。
两个线程有一组局部变量。局部变量 2指向堆上的共享对象(对象 3). 每个线程都有自己的局部变量副本和自己的引用。它们的引用是局部变量,因此存储在线程堆栈中。但是,两个不同的引用指向堆上的同一个对象。
请注意,一般对象 3有链接到对象 2和对象 4作为成员变量(如箭头所示)。通过这些链接,两个线程可以访问对象 2和目的4.
该图还显示了一个局部变量(局部变量 1来自methodTwo)。它的每个副本都包含指向两个不同对象的不同引用(对象 1和对象 5) 而不是同一个。理论上,两个线程都可以访问对象 1, 所以对象 5如果他们引用了这两个对象。但是在上图中,每个线程只有对两个对象之一的引用。
与对象交互的示例
让我们看看如何在代码中演示这项工作:
public class MySomeRunnable implements Runnable() {
public void run() {
one();
}
public void one() {
int localOne = 1;
Shared localTwo = Shared.instance;
//... do something with local variables
two();
}
public void two() {
Integer localOne = 2;
//... do something with local variables
}
}
public class Shared {
// store an instance of our object in a variable
public static final Shared instance = new Shared();
// member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
}
run()方法调用one()方法,然后one()又调用two()。
one()方法声明了一个原始局部变量 (本地一个) 类型的int和一个局部变量 (本地二),这是对对象的引用。
每个执行one()方法的线程都会创建自己的副本本地一个和本地二在你的堆栈中。变量本地一个将彼此完全分开,在每个线程的堆栈上。一个线程看不到另一个线程对其副本所做的更改本地一个.
每个执行one()方法的线程也会创建自己的副本本地二. 但是,两个不同的副本本地二最终指向堆上的同一个对象。事实是本地二指向静态变量引用的对象实例. 静态变量只有一个副本,并且该副本存储在堆中。
所以两个副本本地二最终指向同一个共享实例。Shared实例也存储在堆上。它匹配对象 3在上图中。
请注意,Shared类还包含两个成员变量。成员变量本身与对象一起存储在堆上。两个成员变量指向另外两个对象整数. 这些整数对象对应于对象 2和对象 4在图上。
另请注意,two()方法创建了一个名为本地一个. 此局部变量是对Integer类型对象的引用。该方法设置链接本地一个指向一个新的Integer实例。该链接将存储在其副本中本地一个对于每个线程。堆上会存储两个Integer实例,由于该方法每次执行都会创建一个新的Integer对象,所以执行该方法的两个线程会创建单独的Integer实例。他们匹配对象 1和对象 5在上图中。
还要注意Shared类中Integer类型的两个成员变量,这是一种基本类型。因为这些变量是成员变量,所以还是和对象一起存放在堆上。只有局部变量存储在线程堆栈中。
GO TO FULL VERSION