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 堆(中的 年轻代、老年代)中了。

相关推荐
王佑辉5 小时前
【jvm】为什么java是半编译半解释型语言
jvm
G丶AEOM5 小时前
JVM标量替换
java·jvm
王佑辉5 小时前
【jvm】解释器
jvm
蜗牛沐雨11 小时前
Go语言中的sync.Pool详解:高效对象复用
java·jvm·golang
xq51486312 小时前
内存分配与回收策略
java·开发语言·jvm
Azure++1 天前
JVM相关知识
jvm
G丶AEOM1 天前
JVM逃逸分析机制
java·jvm
无聊写博客1 天前
JDK、JRE、JVM的区别
java·开发语言·jvm
极客先躯1 天前
高级java每日一道面试题-2024年11月22日-JVM篇-说说堆和栈的区别?
java·jvm··
王佑辉1 天前
【jvm】new对象的过程
jvm