【JVM系列】JVM 的内存模型

2022/05/10 JVM 共 2403 字,约 7 分钟

JVM 算是⾯试中的⾼频问题了,通常情况下总会有⼈问到:请你讲解下 JVM 的内存模型,JVM 的性能调优做过吗?

本文我们先来讲下JVM的内存模型。

内存模型

在 Java 中,JVM 内存模型主要分为堆、程序计数器、⽅法区、虚拟机栈和本地⽅法栈。

其中堆和方法区是线程共享的,存在线程安全问题,而程序计数器、虚拟机栈和本地方法栈是线程隔离的。

image-20220516104636844

堆是 JVM 内存中最⼤的⼀块内存空间,该内存被所有线程共享,⼏乎所有对象和数组都被分配到了堆内存中。堆被划分为新⽣代和⽼年代,新 ⽣代⼜被进⼀步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。

Java8版本,堆内存包括新生代、老年代和元空间。

image-20220516105648590

程序计数器

程序计数器是⼀块很⼩的内存空间,主要⽤来记录各个线程执⾏的字节码的地址,例如,分⽀、循环、跳转、异常、线程恢复等都依赖于计数 器。

由于 Java 是多线程语⾔,当执⾏的线程数量超过 CPU 核数时,线程之间会根据时间⽚轮询争夺 CPU 资源。如果⼀个线程的时间⽚⽤完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的⼀个程序计数器,来记录下⼀条运⾏的指令。

方法区

⽅法区主要是⽤来存放已被虚拟机加载的类相关信息,包括类信息、运⾏时常量池、字符串常量池。类信息⼜包括了类的版本、字段、⽅法、接⼝和⽗类等信息。

⽅法区与堆空间类似,也是⼀个共享内存区,所以⽅法区是线程共享的。

在 HotSpot 虚拟机、Java7 版本中已经将永久代的静态变量和运⾏时常量池转移到了堆中,其余部分则存储在 JVM 的⾮堆内存中,⽽ Java8 版本已经将⽅法区中实现的永久代去掉了,并⽤元空间(class metadata)代替了之前的永久代,并且元空间的存储位置是本地内存。之前永久 代的类的元数据存储在了元空间,永久代的静态变量(class static variables)以及运⾏时常量池(runtime constant pool)则跟 Java7 ⼀样, 转移到了堆中。

虚拟机栈

Java 虚拟机栈是线程私有的内存空间,它和 Java 线程⼀起创建。当创建⼀个线程时,会在虚拟机栈中申请⼀个线程栈,⽤来保存⽅法的局部变 量、操作数栈、动态链接⽅法和返回地址等信息,并参与⽅法的调⽤和返回。每⼀个⽅法的调⽤都伴随着栈帧的⼊栈操作,⽅法的返回则是栈帧 的出栈操作。

本地⽅法栈

本地⽅法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈⽤于管理 Java 函数的调⽤,⽽本地⽅法栈则⽤于管理本地⽅法的调⽤。但本地⽅法并 不是⽤ Java 实现的,⽽是由 C 语⾔实现的。

JVM的运行原理

我们通过⼀个案例来了解下代码和对象是如何分配存储的,Java 代码⼜是 如何在 JVM 中运⾏的。

public class JVMCase { 
	// 常量 
  public final static String MAN_SEX_TYPE = "man"; 
  // 静态变量 
  public static String WOMAN_SEX_TYPE = "woman"; 
  
  public static void main(String[] args) {
    Student stu = new Student();
    stu.setName("nick"); 
    stu.setSexType(MAN_SEX_TYPE); 
    stu.setAge(20); 
    
    JVMCase jvmcase = new JVMCase();
    // 调⽤静态⽅法 
    print(stu); 
    // 调⽤⾮静态⽅法 
    jvmcase.sayHello(stu); 
  }
  
  // 常规静态⽅法 
  public static void print(Student stu) { 
    System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge()); 
  }
  
  // ⾮静态⽅法 
  public void sayHello(Student stu) { 
    System.out.println(stu.getName() + "say: hello"); 
  } 
}

class Student{ 
  String name; 
  String sexType;
  int age;
  // ...省略get和set
}

当我们通过 Java 运⾏以上代码时,JVM 的整个处理过程如下:

1.JVM 向操作系统申请内存,JVM 第⼀步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存⼤⼩找到具体的内存分配表, 然后把内存段的起始地址和终⽌地址分配给 JVM,接下来 JVM 就进⾏内部分配。

2.JVM 获得内存空间后,会根据配置参数分配堆、栈以及⽅法区的内存⼤⼩。

3.class ⽂件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值。

image-20220516163138509

4.完成上⼀个步骤后,将会进⾏最后⼀个初始化阶段。在这个阶段中,JVM ⾸先会执⾏构造器 ⽅法,编译器会在.java ⽂件被编译 成.class ⽂件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态⽅法,收集在⼀起成为 () ⽅法。

image-20220516163244071

5.执⾏⽅法。启动 main 线程,执⾏ main ⽅法,开始执⾏第⼀⾏代码。此时堆内存中会创建⼀个 student 对象,对象引⽤ student 就存放在栈 中。

image-20220516163328502

6.此时再次创建⼀个 JVMCase 对象,调⽤ sayHello ⾮静态⽅法,sayHello ⽅法属于对象 JVMCase,此时 sayHello ⽅法⼊栈,并通过栈中的 student 引⽤调⽤堆中的 Student 对象;之后,调⽤静态⽅法 print,print 静态⽅法属于 JVMCase 类,是从静态⽅法中获取,之后放⼊到栈 中,也是通过 student 引⽤调⽤堆中的 student 对象。

image-20220516163417932

了解完实际代码在 JVM 中分配的内存空间以及运⾏原理,相信你会更加清楚内存模型中各个区域的职责分⼯。

最后

本文我们分析了JVM最基础的内存模型设计,了解其各个分区的作⽤及实现原理。

文档信息

搜索

    Table of Contents