JVM 内存结构 -Java8

JVM 内存结构 -Java8

参考《Java 虚拟机规范》

总览图:
1100
注意蓝色部分为单个线程私有,深黄色部分为所有线程共享
很多开发人员会把 Java 内存分为堆内存(Heap)和栈内存(Stack),这种划分的流行只能说明大多数开发人员最关注、与对象内存分配关系最密切的内存区域是这两块。实际上 Java 内存区域的划分远比这要复杂。

JVM 内存与本地内存的区别

Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,我们称它为本地内存,这两种内存有一定的区别:
JVM 内存

java 运行时数据区域(上图中的所有部分)

java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 java 进程产生和消失,根据《Java 虚拟机规范》的规定,运行时数据区分为以下几个区域:

程序计数器(Program Counter Register)

程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过他来实现跳转、循环、恢复线程等功能。每个线程都有独立的程序计数器,用来在线程切换后能恢复到正确的执行位置,各条线程之间的计数器互不影响,独立存储。所以它是一个“线程私有”的内存区域。还记得线程的 wait 和 notify 吗,就是通过这个实现的。
在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的,这就需要有一个标志来记住每个线程执行到了哪里,这里便需要到了程序计数器。
所以,程序计数器是线程私有的,每个线程都已自己的程序计数器。

虚拟机栈(JVM Stacks)

800
虚拟机栈是线程私有的,随线程生灭。虚拟机栈描述的是线程中的方法的内存模型:
每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame)。每个栈帧的包含如下的内容:

虚拟机栈可能会抛出两种异常:

本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈的作用是相似的,都会抛出 OutOfMemoryError 和 StackOverFlowError,都是线程私有的,主要的区别在于:

Java 堆(Java Heap)

java 堆是 JVM 内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于 java 虚拟机的发展,堆中也多了许多东西,现在主要有:

方法区 (Method Area)

方法区绝对是网上所有关于 java 内存结构文章争论的焦点,因为方法区的实现在 java8 做了一次大革新,现在我们来讨论一下:
方法区是所有线程共享的内存,在 java8 以前是放在 JVM 内存中的,由永久代实现,受 JVM 内存大小参数的限制,在 java8 中移除了永久代的内容,方法区由元空间 (Meta Space) 实现,并直接放到了本地内存中,不受 JVM 参数的限制(当然,如果物理内存被占满了,方法区也会报 OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了 Java 堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

直接内存

直接内存位于本地内存,不属于 JVM 内存,但是也会在物理内存耗尽的时候报 OOM,所以也讲一下。
在 jdk1.4 中加入了 NIO(New Input/Output)类,引入了一种基于通道(channel)与缓冲区(buffer)的新 IO 方式,它可以使用 native 函数直接分配堆外内存,然后通过存储在 java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高 IO 性能,避免了在 java 堆和 native 堆来回复制数据。
关于 NIO,请看 Java的NIO

变量的存放位置

类变量/静态变量

类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁,在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中。

成员变量

成员变量是定义在类中,但是没有 static 修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分,由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中。

局部变量

局部变量是定义在类的方法中的变量,在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

final 修饰的常量

final 关键字并不影响在内存中的位置,具体位置请参考上一问题。

常见的几个常量池的关系

类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中。(注意类常量池和类变量不是同一个概念,类常量池存放的是编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放,看 5 小节
在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;(汇总起来,供所有线程使用)
对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。

什么是字面量?什么是符号引用?

字面量

字面量就是在编译时对于数据的一种表示:

int a=1;//这个 1 便是字面量
String b="iloveu";//iloveu 便是字面量

符号引用

由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以 java 代码在编译过程中是无法构建引用的,如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。所以类信息里通常包含着对其它类的符号应用。
例子:我在 com.demo.Solution 类中引用了 com.test.Quest,那么我会把 com.test.Quest 作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。