JVM 是一个完整的计算机模型,所以自然就需要有对应的内存模型,这个模型被称为 “Java 内存模型
”,对应的英文是“Java Memory Model
”,简称 JMM
。
Java 内存模型规定了 JVM 应该如何使用计算机内存(RAM)。 广义来讲, Java 内存模型分为两个部分:
- JVM 内存结构
- JMM 与线程规范
其中,JVM 内存结构是底层实现,也是我们理解和认识 JMM 的基础。 大家熟知的堆内存、栈内存等运行时数据区的划分就可以归为 JVM 内存结构。
JVM内存结构
jvm内部使用的Java内存模型,逻辑上分为线程栈和堆内存。
线程栈
每个运行的线程都有自己的线程栈,线程栈包含了当前正在执行方法链/调用链上所有方法的状态信息。所有又称为方法栈或者调用栈。
- 每个线程都只能访问自己的线程栈。
- 每个线程都不能访问(看不见)其他线程的局部变量。
- 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
- 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
- 堆内存中包含了Java代码中创建的所有对象,不管是那个线程创建的。其中也涵盖了包装类型(例如Byte、Interger、Long等)
- 不管是创建一个对象并将其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都会保存到堆内存中。
- 如果是原生数据类型的局部变量,那么他的内容就全部保留在线程栈上。
- 如果是对象引用,栈中的局部变量保存这对象的引用地址,实际的对象内容保存在堆中。
- 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值还是对象引用。
- 类的静态变量和类定义一样都保存在堆中。
总结:原始数据类型和对象引用地址在栈上;对象、对象成员与类定义、静态变量在堆上。
堆内存
堆内存又称为"共享堆",堆中所有对象可以被所有线程访问,只要线程能找到(拿到对象的引用实例)
- 如果一个线程可以访问某个对象,那就能访问该对象的成员变量
- 如果两个线程同时调用某个对象的同一方法,则他们都可以访问到这个对象的成员变量,但是每个线程的局部变量副本是独立的。
总结:各个线程自己使用的局部变量都在自己的栈上,可以共享堆上的对象,不同线程访问同一个对象实例的基础类型的成员变量时,会给每个线程一个变量的副本。
栈内存结构
Java虚拟机栈是线程私有的内存空间,他和线程一起创建。当一个线程创建时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态连接方法和返回地址等信息,并参与方法的调用和返回。每一个方法额调用都伴随着栈帧的入栈操作,方法的返回则为出栈操作。
每启动一个线程,JVM 就会在栈空间栈分配对应的线程栈, 比如 1MB 的空间(-Xss1m
)。
线程栈也叫做 Java 方法栈。 如果使用了 JNI 方法,则会分配一个单独的本地方法栈(Native Stack)。
线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B 调用 C……每执行到一个方法,就会创建对应的栈帧(Frame)。
堆结构
堆是JVM内存中最大的一块内存空间,内存被所有线程所共享,几乎所有的对象和数组都被分配到堆内存中。
在逻辑上将堆分为堆(Heap)和非堆(Non-Heap)两个部分。我们编写的Java代码基本上只用Heap这部分,发生内存分配和回收的主要部分。
堆中又包括新生代和老年代,新生代又进一步划分为Eden和Survivor区,Survivor有由From和To Survivor组成。To和From Survivor总有一个是空的。
1.7版本中永久代的静态变量和运行时常量池被合并到堆中。到了1.8中,永久代被元空间取代了。
非堆用来存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。方法区(所有线程共享)属于非堆内存。
所以一般我们将xmx的内存设置为容器60%-75%,除了堆还有非堆、jvm自身还需要内存空间。
方法区(Method Area)
用来存放已经被虚拟机加载的类的相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括类的版本、字段、方法、接口和父类信息。
JVM在执行某个类的执行,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。在加载类的时候,JVM会先加载class文件,而在class文件中除了又类的版本、方法和接口等描述信息之外,还有一项信息是常量池,用于存放编译期间生成额各种字面量和符号引用。
字面量包括字符串(String a=“b”)、基本类型的常量(final修饰的变量),符号引用则包括类和方法的全限定名。
当类加载到内存中后,JVM就会将class文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)
程序计数器(Program Counter Register)
程序计数器是一块很小的内存空间,主要记录各个线程的执行的字节码的地址。例如,分支、循环、跳转、异常、线程恢复等都依赖计数器。