Java性能 -- JVM内存模型
JVM内存模型
堆
- 堆是JVM内存中最大的一块内存空间,被所有线程共享,几乎所有对象和数组都被分配到堆内存中
- 堆被划分为新生代和老年代,新生代又被划分为Eden区和Survivor区(From Survivor + To Survivor)
- 永久代
- 在Java 6中,永久代在非堆内存中
- 在Java 7中,永久代的静态变量和运行时常量池被合并到堆中
- 在Java 8中,永久代被元空间取代
程序计数器
- 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址
- Java是多线程语言,当执行的线程数量超过CPU数量时,线程之间会根据时间片轮询争夺CPU资源
- 当一个线程的时间片用完了,或者其他原因导致该线程的CPU资源被提前抢夺
- 那么退出的线程需要单独的程序计数器来记录下一条运行的指令
方法区
- 方法区 != 永久代
- HotSpot VM使用了永久代来实现方法区,但在其他VM(Oracle JRockit、IBM J9)不存在永久代一说
- 方法区只是JVM规范的一部分,在HotSpot VM中,使用了永久代来实现JVM规范的方法区
- 方法区主要用来存放已被虚拟机加载的类相关信息
- 类信息(类的版本、字段、方法、接口和父类等信息)、运行时常量池、字符串常量池
- JVM在执行某个类的时候,必须经过加载、连接(验证、准备、解析)、初始化
- 加载类时,JVM会先加载class文件,在class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池
- 常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用
- 字面量:字符串(
String a="b"
),基本类型的常量(final修饰) - 符号引用:类和方法的全限定名、字段的名称和描述符、方法的名称和描述符
- 当类加载到内存中后,JVM会将class文件常量池中的内容存放到运行时常量池中
- 在解析阶段,JVM会把符号引用替换为直接引用(对象的索引值)
- 加载类时,JVM会先加载class文件,在class文件除了有类的版本、字段、方法、接口等描述信息外,还有常量池
- 运行时常量池是全局共享的,多个类共用一个运行时常量池
- class文件中的常量池多个相同的字符串在运行时常量池只会存在一份
- 方法区和堆类似,都是一个共享内存区,所以方法区是线程共享的
- 假设两个线程都试图访问方法区中同一个类信息,而这个类还没有装入JVM
- 那么此时只允许一个线程去加载该类,另一个线程必须等待
- HotSpot VM
- 在Java 7中,已经将永久代的静态变量和运行时常量池转移到堆中,其余部分则存储在JVM的非堆内存中
- 在Java 8中,已经用元空间代替永久代来实现方法区,并且元空间的存储位置是本地内存
- 之前永久代中类的元数据存储在元空间,永久代的静态变量和运行时常量池与Java 7一样,转移到堆中
- 移除永久代,使用元空间的好处
- 移除永久代是为了融合HotSpot VM和JRockit VM
- 永久代内存经常不够用或者发生内存溢出(java.lang.OutOfMemoryError: PermGen)
- 为永久代分配多大的空间很难确定,依赖很多因素
- JVM的内存模型只是一个规范,方法区也是一个规范,一个逻辑分区,并不是一个物理分区
虚拟机栈
- Java虚拟机栈是线程私有的内存空间,和Java线程一起创建
- 当创建一个线程时,会在虚拟机栈中申请一个线程栈
- 用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回
- 每一个方法的调用都伴随着栈帧的入栈操作,每一个方法的返回都伴随着栈帧的出栈操作
本地方法栈
- 本地方法栈跟Java虚拟机栈的功能类似
- Java虚拟机栈用来管理Java函数的调用,而本地方法栈用来管理本地方法(C语言实现)的调用
JVM运行原理
1 | public class JVMCase { |
- JVM向操作系统申请内存
- JVM第一步是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表
- 然后把内存段的起始地址和终止地址分配给JVM,最后JVM进行内部分配
- JVM获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小
- class文件加载、验证、准备和解析
- 其中准备阶段会为类的静态成员(字段和方法)分配内存,初始化为系统初始值
- 初始化
- JVM首先会执行构造器
<clinit>
方法,编译器会在.java文件被编译成.class文件,收集所有类的初始化代码 - 初始化代码:静态变量赋值语句,静态代码块、静态方法
- JVM首先会执行构造器
- 执行方法
- 启动main线程,执行main方法,执行第一行代码
- 在堆内存中会创建一个Student对象,对象引用student存放在栈中
- 再创建一个JVMCase对象,并调用sayHello非静态方法
- sayHello方法属于对象jvmCase,此时sayHello方法入栈,并调用栈中Student引用调用堆中的Studentd对象
- 调用静态方法print
- print方法属于JVMCase类,从静态方法中获取后放入到栈中,通过Student引用调用堆中的Studentd对象
准备阶段
初始化阶段
创建一个Student对象
创建一个JVMCase对象,并调用sayHello非静态方法和print静态方法
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.