JVM 内存结构
这里的JVM内存结构,是指Runtime Data Areas(运行时数据区)。包含:
- 方法区(Method Area)
- 虚拟机栈(VM Stack)
- 本地方法栈(Native Method)
- 堆(Heap)
- 程序计数器(Program Counter Register)
各部分是否私有
通过上图可以知道:
- 线程私有的部分:程序计数器、本地方法栈、虚拟机栈;
- 线程间共享的部分:堆、方法区;
程序计数器
注意:这里的 程序计数寄存器 与 物理中的****CPU的寄存器 是不一样的。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。 它是线程私有的。
工作细节:通过改变这个计数器的值来选取下一条需要执行的字节码指令,程序控制流,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
虚拟机栈
虚拟机栈(Java Stack),也叫Java栈,就是我们平常说的栈。线程私有的。
除了Native 方法(它通过本地方法栈实现),其他的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
局部变量表:存放局部变量的,以**局部变量槽(Slot)**来表示。局部变量表的大小是确定的,在编译期间完成分配的。
操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态连接:用于 一个方法需要调用其他方法的场景。ava 源文件被编译成字节码文件时,变量、方法的引用都是 符号引用(保存在Class 文件的常量池里)。动态链接 就是在方法调用其他方法时,将 符号引用转换为 直接引用的。
问:垃圾回收是否涉及栈内存?
答:不会,因为栈的运行结束后会按从顶至底的顺序移除栈对应的线程的方法,所以不需要垃圾回收机制来处理长久不用的垃圾。
问:栈内存分配越大越好吗?
答:不是的,我们的物理内存是有限的,如果栈内存分配过大,会导致我们能运行的线程数变少。
可以通过-Xss size(分配的大小)
来指定栈内存的大小。
问:方法内的 局部变量 是线程安全的吗?
分析:如果它是线程私有的,那肯定是线程安全的。否则就是线程县城不安全的。
答:分情况:
- 如果这个方法中的局部变量不是
static
修饰。那就是线程安全的。 - ①如果这个方法中的局部变量是
static
修饰。那就是线程不安全的。②如果传入的参数是一个可变对象,并且这个对象在多个线程中被共享和修改,那就可能出现线程安全问题。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
一些带有
native
关键字的方法就是需要JAVA去调用本地的C或者C++方法。
堆
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
但也不绝对,从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java 堆区域可细分为:
- JDK7 及之前分为:新生代内存(Young Generation)、老生代(Old Generation)、永久代(Permanent Generation);
- JDK8 及之后:新生代内存(Young Generation)、老生代(Old Generation);Metaspace(元空间) (元空间:使用的是本地内存,它只是替代了 永久代 的作用,但**
它不属于 堆内存,而是存放于本地内存
**)
1、新生代内存(Young Generation):
- 作用:用于存放新创建的对象。大部分对象通常具有短暂的生命周期,因此在新生代中频繁进行垃圾回收,可以高效地回收大量短生命周期的对象,减少内存占用。
- GC回收:新对象会先被分配到Eden区。当Eden区满了之后触发Minor GC(新生代垃圾回收)。在回收过程中,存活下来的对象会被移到其中一个Survivor区;当Survivor区也满了,对象会进一步晋升到老年代。
- 组成:由
年轻区(Eden区)
、两个Survivor区
(from survivor
和to survivor
)。默认情况下,新生代的Eden区和Survivor区的空间大小比例是8:2
,可以通过-XX:SurvivorRatio
参数调整。
2、老年代:存放经过多次垃圾回收仍然存活的对象,或者是大对象(某些JVM可以直接在老年代分配大对象)。老年代中的对象被认为是长时间存活的,因此回收的频率较低。
大对象:通常指的是需要占用较多连续内存空间的Java对象实例
3、永久代、元空间:(二者作用一样,只是元空间 替代 永久代)
- 作用:它是具体实现的方法区:用来存放类的元数据信息,包括类的结构信息、常量池、静态变量、即时编译后的代码等。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
主要考虑的是,永久代使用的是JVM内存,因此,当存放的数据太多时,很容易导致永久代内存溢出(PermGen OutOfMemoryError)。
而元空间使用本地内存而不是 JVM 堆内存,理论上只受限于操作系统的内存大小。这大大减少了因为类信息、常量等存储导致内存溢出的风险。
方法区
方法区(Method Area)是线程共享的。它是Java 虚拟机规范的一个逻辑部分,别名 非堆(Non-Heap),目的是与 Java的 堆(Heap)区分开。在不同的虚拟机实现上,方法区的实现是不同的。
方法区主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码、运行时常量池。虚拟机启动时就会创建方法区。
JDK中方法区的不同实现
以HotSpot虚拟机来讲,在JDK不同版本汇总,主要关注字符串常量池位置变迁:
- Jdk1.6及之前: 有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池;
- Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里;
- Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里。
问:为什么要将字符串常量池移动到堆中?
答:主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
String 类的 intern()
方法
参考:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
调用字符串对象的intern
方法,会将该字符串对象尝试放入到字符串常量池中:
- 如果字符串常量池中没有该字符串对象,会将该字符串对象复制一份,再放入到字符串常量池中;
- 如果有该字符串对象,则放入失败;
知道了intern()
方法的作用,下面来看看String
中创建字符串时的区别(以下面的代码示例):
要知道:
- 直接使用双引号声明出来的
String
对象会直接存储在字符串常量池中。如:String a = "jack";
- 使用
new
创建的String对象,则会在 字符串常量池中创建字符串,并在堆中创建String对象。
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
JDK1.6 中输出 false、false
因为,JDK1.6中,字符串常量池 位于 堆的永久代区域(我们知道 JVM 堆中的 永久代 是是具体实现的方法区),而上图中的 堆(Heap)是特指新生代、老年代。所以:
String s = new String("1");
:会在 字符串常量池 中创建字符串"1”
,然后在JAVA 堆中创建String
对象obj(存储的是 字符串常量池“1”
的引用),然后栈中的s
指向Java heap中的obj
。String s2 = "1";
是栈中的s2
直接去 字符串常量池 中查找是否有“1”
,有则直接指向。无则在字符串常量池中创建,然后再指向。
jdk1.7 中输出 false、true
jdk1.7中,字符串常量池 从永久代 移动到正常的 Java 堆(中的 年轻代、老年代)中了。