JVM 内存结构?

JVM 内存结构

这里的JVM内存结构,是指Runtime Data Areas(运行时数据区)。包含:

  • 方法区(Method Area)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method)
  • 堆(Heap)
  • 程序计数器(Program Counter Register)

各部分是否私有

通过上图可以知道:

  1. 线程私有的部分:程序计数器本地方法栈虚拟机栈
  2. 线程间共享的部分:方法区

程序计数器

注意:这里的 程序计数寄存器物理中的****CPU的寄存器 是不一样的。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器 。 它是线程私有的

工作细节:通过改变这个计数器的值来选取下一条需要执行的字节码指令 ,++程序控制流,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成++。

虚拟机栈

虚拟机栈(Java Stack) ,也叫Java栈 ,就是我们平常说的线程私有的

除了Native 方法(它通过本地方法栈实现),++其他的 Java 方法调用都是通过栈来实现的++(也需要和其他运行时数据区域比如程序计数器配合)。

每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息

  • 局部变量表 :存放局部变量的,以**局部变量槽(Slot)**来表示。局部变量表的大小是确定的,在编译期间完成分配的。

  • 操作数栈 :主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果 。另外,计算过程中产生的临时变量也会放在操作数栈中

  • 动态连接 :用于 一个方法需要调用其他方法的场景。ava 源文件被编译成字节码文件时,变量、方法的引用都是 符号引用(保存在Class 文件的常量池里)。动态链接 就是在方法调用其他方法时,将 符号引用转换为 直接引用的

问:垃圾回收是否涉及栈内存?

答:不会,因为栈的运行结束后会按从顶至底的顺序移除栈对应的线程的方法,所以不需要垃圾回收机制来处理长久不用的垃圾。

问:栈内存分配越大越好吗?

答:不是的,我们的物理内存是有限的,如果栈内存分配过大,会导致我们能运行的线程数变少。

可以通过-Xss size(分配的大小)来指定栈内存的大小。

问:方法内的 局部变量 是线程安全的吗?

分析:如果它是线程私有的,那肯定是线程安全的。否则就是线程县城不安全的。

答:分情况:

  1. 如果这个方法中的局部变量不是static修饰。那就是线程安全的。
  2. ①如果这个方法中的局部变量是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 survivorto 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不同版本汇总,主要关注字符串常量池位置变迁

  1. Jdk1.6及之前: 有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池
  2. Jdk1.7:有永久代,但已经逐步"去永久代",字符串常量池从永久代里的运行时常量池分离到堆里
  3. 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中创建字符串时的区别(以下面的代码示例):

要知道:

  1. 直接使用双引号声明出来的String对象会直接存储在字符串常量池中。如:String a = "jack";
  2. 使用new创建的String对象,则会在 字符串常量池中创建字符串,并在堆中创建String对象。
java 复制代码
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)是特指新生代、老年代。所以:

  1. String s = new String("1");:会在 字符串常量池 中创建字符串"1",然后在JAVA 堆中创建String对象obj(存储的是 字符串常量池"1"的引用),然后栈中的s指向Java heap中的 obj
  2. String s2 = "1";是栈中的s2直接去 字符串常量池 中查找是否有"1",有则直接指向。无则在字符串常量池中创建,然后再指向。
jdk1.7 中输出 false、true

jdk1.7中,字符串常量池 从永久代 移动到正常的 Java 堆(中的 年轻代、老年代)中了。

相关推荐
学到头秃的suhian1 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN8 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海13 小时前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗14 小时前
JVM整理
jvm
echoyu.14 小时前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考1 天前
JVM中内存管理的策略
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z1 天前
【JVM】详解 线程与协程
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm