JVM内存模型

  1. 堆是JVM内存中最大的一块内存空间,被所有线程共享几乎所有对象和数组都被分配到堆内存中
  2. 堆被划分为新生代老年代,新生代又被划分为Eden区和Survivor区(From Survivor + To Survivor)
  3. 永久代
    • 在Java 6中,永久代在非堆内存中
    • 在Java 7中,永久代的静态变量运行时常量池合并到堆
    • 在Java 8中,永久代被元空间取代

程序计数器

  1. 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址
  2. Java是多线程语言,当执行的线程数量超过CPU数量时,线程之间会根据时间片轮询争夺CPU资源
    • 当一个线程的时间片用完了,或者其他原因导致该线程的CPU资源被提前抢夺
    • 那么退出的线程需要单独的程序计数器来记录下一条运行的指令

方法区

  1. 方法区 != 永久代
  2. HotSpot VM使用了永久代来实现方法区,但在其他VM(Oracle JRockitIBM J9)不存在永久代一说
  3. 方法区只是JVM规范的一部分,在HotSpot VM中,使用了永久代来实现JVM规范的方法区
  4. 方法区主要用来存放已被虚拟机加载的类相关信息
    • 类信息(类的版本、字段、方法、接口和父类等信息)、运行时常量池字符串常量池
  5. JVM在执行某个类的时候,必须经过加载连接(验证、准备、解析)、初始化
    • 加载类时,JVM会先加载class文件,在class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池
      • 常量池(Constant Pool Table),用于存放编译期生成的各种字面量符号引用
      • 字面量:字符串String a="b"),基本类型的常量(final修饰)
      • 符号引用:类和方法的全限定名字段的名称和描述符方法的名称和描述符
    • 当类加载到内存中后,JVM会将class文件常量池中的内容存放到运行时常量池
    • 在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)
  6. 运行时常量池全局共享的,多个类共用一个运行时常量池
    • class文件中的常量池多个相同的字符串在运行时常量池只会存在一份
  7. 方法区和堆类似,都是一个共享内存区,所以方法区是线程共享
    • 假设两个线程都试图访问方法区中同一个类信息,而这个类还没有装入JVM
    • 那么此时只允许一个线程去加载该类,另一个线程必须等待
  8. HotSpot VM
    • 在Java 7中,已经将永久代的静态变量运行时常量池转移到中,其余部分则存储在JVM的非堆内存中
    • 在Java 8中,已经用元空间代替永久代来实现方法区,并且元空间的存储位置是本地内存
      • 之前永久代中类的元数据存储在元空间,永久代的静态变量运行时常量池与Java 7一样,转移到
    • 移除永久代,使用元空间的好处
      • 移除永久代是为了融合HotSpot VM和JRockit VM
      • 永久代内存经常不够用或者发生内存溢出(java.lang.OutOfMemoryError: PermGen)
      • 为永久代分配多大的空间很难确定,依赖很多因素
  9. JVM的内存模型只是一个规范,方法区也是一个规范,一个逻辑分区,并不是一个物理分区

虚拟机栈

  1. Java虚拟机栈是线程私有的内存空间,和Java线程一起创建
  2. 当创建一个线程时,会在虚拟机栈中申请一个线程栈
    • 用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回
  3. 每一个方法的调用都伴随着栈帧的入栈操作,每一个方法的返回都伴随着栈帧的出栈操作

本地方法栈

  1. 本地方法栈跟Java虚拟机栈的功能类似
  2. Java虚拟机栈用来管理Java函数的调用,而本地方法栈用来管理本地方法(C语言实现)的调用

JVM运行原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class JVMCase {
// 常量
private final static String MAN_SEX_TYPE = "man";
// 静态变量
public static String WOMAN_SEX_TYPE = "woman";

public static void main(String[] args) {
Student student = new Student();
student.setName("nick");
student.setSexType(MAN_SEX_TYPE);
student.setAge(20);

JVMCase jvmCase = new JVMCase();
// 调用非静态方法
jvmCase.sayHello(student);
// 调用静态方法
print(student);
}

// 非静态方法
private void sayHello(Student student) {
System.out.println(student.getName() + " say: hello");
}

// 常规静态方法
private static void print(Student student) {
System.out.println(student);
}
}

@Data
class Student {
private String name;
private String sexType;
private int age;
}
  1. JVM向操作系统申请内存
    • JVM第一步是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表
    • 然后把内存段的起始地址终止地址分配给JVM,最后JVM进行内部分配
  2. JVM获得内存空间后,会根据配置参数分配以及方法区的内存大小
  3. class文件加载验证准备解析
    • 其中准备阶段会为类的静态成员(字段和方法)分配内存,初始化为系统初始值
  4. 初始化
    • JVM首先会执行构造器<clinit>方法,编译器会在.java文件被编译成.class文件,收集所有类的初始化代码
    • 初始化代码:静态变量赋值语句静态代码块静态方法
  5. 执行方法
    • 启动main线程,执行main方法,执行第一行代码
    • 内存中会创建一个Student对象对象引用student存放在
    • 再创建一个JVMCase对象,并调用sayHello非静态方法
      • sayHello方法属于对象jvmCase,此时sayHello方法入栈,并调用栈中Student引用调用堆中的Studentd对象
    • 调用静态方法print
      • print方法属于JVMCase类,从静态方法中获取后放入到栈中,通过Student引用调用堆中的Studentd对象

准备阶段

初始化阶段

创建一个Student对象

创建一个JVMCase对象,并调用sayHello非静态方法和print静态方法

参考资料

Java性能调优实战