Java内存区域
Java内存区域
Java虚拟机在运行时,会把内存空间分为若干个区域:程序计数器、虚拟机栈、本地方法栈、堆内存、方法区。
JDK1.8(含)之后:
1、程序计数器
程序计数器是用来指示执行哪条指令的,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以程序计数器是Java虚拟机中唯一不会发生内存溢出的区域。
2、虚拟机栈
虚拟机栈也是每个线程私有的一块内存空间,它描述的是方法的内存模型。
虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。
一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。
3、本地方法栈
本地方法栈与虚拟机栈的作用和原理非常相似,区别是虚拟机栈为执行Java方法服务的,而本地方法栈是为执行本地方法(Native Method)服务的。
在JVM规范中,并没有强制规定本地方法栈的具体实现方法以及数据结构,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
4、堆内存
堆内存主要用于存放对象和数组,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。
在垃圾收集的层面上来看,由于现在的收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为 Eden、From Survivor、To Survivor。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa==bb);// true
这里的字符串是字符串字面量。在声明一个字符串字面量时,如果字符串常量池中能够找到该字符串字面量,则直接返回该引用。如果找不到的话,则在常量池中创建该字符串字面量的对象并返回其引用。
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
为什么要将字符串常量池移动到堆中
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
5、方法区
方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。
在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。
在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中。
JDK1.6 到 JDK1.8 方法区的变化:
去永久代的原因有:
- 字符串存在永久代中,容易出现性能问题和内存溢出
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的常量池表(Constant Pool Table)。常量池表会在类加载后存放到方法区的运行时常量池中。
字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。
运行时常量池原本是方法区的一部分,会受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError
错误。
- JDK1.7 之前,运行时常量池包含的字符串常量池和静态变量存放在方法区, 此时 HotSpot 虚拟机对方法区的实现为永久代。
- JDK1.7 字符串常量池和静态变量被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 HotSpot 中的永久代 。
- JDK1.8 HotSpot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池和静态变量还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
6、元空间
在jdk1.8 中,替代方法区的一块空间叫做 “ 元空间 ”,它并不在虚拟机中,而是使用本地内存。
元空间的大小仅受本地内存限制,但可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定元空间的大小。
7、直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
Java对象访问方式
一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。
Object objRef = new Object()为例:
- Object objRef 表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据
- new Object()作为实例对象数据存储在堆中
- 堆中还记录了能够查询到此Object对象的类型数据(接口、方法、field、对象类型等)的地址,实际的数据则存储在方法区中
在Java虚拟机规范中,只规定了指向对象的引用,对于通过reference类型引用访问具体对象的方式并未做规定,不过目前主流的实现方式主要有两种:
通过句柄访问
通过句柄访问的实现方式中,JVM堆中会划分单独一块内存区域作为句柄池,句柄池中存储了对象实例数据(在堆中)和对象类型数据(在方法区中)的指针。
通过直接指针访问
通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。
句柄访问和直接访问的特点
- 句柄访问:reference存放的是句柄地址(比较稳定),在对象移动时(垃圾回收),只会改变句柄中实例数据的地址,而reference无需改变
- 直接访问:节省了一次指针开销访问速度比较快。Sun HotSpot虚拟机采用的是直接访问