JVM 深入理解 - 内存区域
Java 学习目录
上一章 JVM 基础入门 - 内存区域
深入理解运行时数据区
我们从一个简单的类去理解运行时数据区。
/**
* VM参数
* -Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops
*/
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 静态变量
public static void main(String[] args)throws Exception {
Teacher T1 = new Teacher();
T1.setName("归不归");
T1.setSexType(MAN_TYPE);
T1.setAge(36);
for(int i =0 ;i<15 ;i++){
System.gc();//主动触发GC 垃圾回收 15次--- T1存活
}
Teacher T2 = new Teacher();
T2.setName("吴勉");
T2.setSexType(MAN_TYPE);
T2.setAge(18); // 这男人脸酸 还是 让他岁数小点吧。
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
}
class Teacher{
String name;
String sexType;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
这个类从启动到运行结束是什么样的过程,这里就随着大家一起走一遍。
- JVM 使用我们设置的参数去向系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存的起始地址和结束地址分配给 JVM ,接下来 JVM 虚拟化内存。
- JVM 获取到内存后会根据参数分配堆、栈、方法区的内存大小。
-Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m - 类加载
主要是把类放入方法区,然后获取类的常量池,将类的常量池放入方法区的静态常量池。 - 执行方法和创建对象:
启动 main 线程 (还要一对线程,什么 gc finally 等等),执行 main 方法 ,开始执行第一行代码。此时会在堆中创建一个 Teacher 对象,对象的引用 T1 就存放在java 虚拟机栈中的局部变量表中。
后续在创建的 Teacher 对象也是刚刚的逻辑。
JVM 运行内存的整体流程
- JVM 启动 → 申请内存 → 行运行时数据区初始化 → 类加载到方法区 → 最后执行方法。
- 方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。
- 同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。
从底层深入理解运行时数据区
堆空间分代划分
堆空间的划分
- 堆
- 新生代
- Eden
- Survivor
- From survivor
- To Survivor
- 老年代
堆被划分为新生代和老年代(Tenured),新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
- 新生代
GC 概念
- GC- Garbage Collection 垃圾回收,在 JVM 中是自动化的垃圾回收机制,在 JVM 中 GC 的主要工作区域是堆空间。
- 一般无需关注。
- 也可以手动发起。System.gc() 主动发起。
- GC又分为 minor GC 和 Full GC (也称为 Major GC )
- Minor GC触发条件:
当Eden区满时,触发Minor GC。 - Full GC触发条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过Minor GC后进入老年代的平均大小,大于老年代的可用内存。
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
垃圾回收器
既然说到这里了 GC 还和垃圾回收器有很大的关系。我们常用的垃圾回收器有以下几种。
- 经典垃圾回收器
- Serial
- Parallel Scavenge
- CMS
- G1
- 低延迟垃圾回收器
- Shenandoah
- Epsilon
- ZGC
先知道就好,之后会有文章专门介绍垃圾回收器。
JHSDB 工具
JHSDB 是一款基于服务性代理实现的进程外调试工具。服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的,主要基于 Java 语言实现的 API 集合。
开启 HSDB 工具
- JDK1.8
启动 JHSDB 的时候必须将 sawindbg.dll(一般会在 JDK 的目录下)复制到对应目录的 jre 下(注意在 win 上安装了 JDK1.8 后往往同级目录下有一个 jre 的目录)
一定要做这步哦不然就这样。
然后在 jdk/lib 目录下启动cmd 然后执行下面命令
java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
- JDK1.9 及以后的开启方式
打开命令行进入 JDK 的 bin 目录下,输入命令 jhsdb hsdb 来启动它。
那么现在就看看用 JHSDB 看看我们刚刚的代码在 JVM 是什么样的。
- 先启动我们刚刚的程序。
- 在命令行中运行 **“jsp” ** 命令。
- 然后就可以看见我们程序的进程号。
- HSDB → file → Attach → 输入 29764。就会出现下面的图片。
查看堆内存分配
Tools → Heap parameters
在这里就能够看见对内年龄分代了。
-
Gen 0
- eden [0x0000000013200000 <-> 0x0000000013a00000)
- from [0x0000000013a00000 <-> 0x0000000013b00000)
- to [0x0000000013b00000 <-> 0x0000000013c00000)
-
Gen 1 [ 0x0000000013c00000 <-> 0x0000000015000000 )
-
下面我们在找一下,我们两个老师类都在那里存放。
Tools → Object Histogram 输入类的包名。
然后双击这一行。
然后就可以看见了有两个对象。然后点击 inspect 按钮 。就可以看见内存地址了。
那么我们现在就知道吴勉的内存地址是 13200000,归不归的内存地址是13d6d728 那他们对应在那个区域呢?。
- Gen 0
- eden [0x0000000013200000 <-> 0x0000000013a00000)
- from [0x0000000013a00000 <-> 0x0000000013b00000)
- to [0x0000000013b00000 <-> 0x0000000013c00000)
- Gen 1 [ 0x0000000013c00000 <-> 0x0000000015000000 )
我们对一下内存地址就可以知道 T2 在 eden 区, T1 在老年代。
但是为什么呢? 仔细看一下下面的代码。
/**
* VM参数
* -Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops
*/
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 静态变量
public static void main(String[] args)throws Exception {
Teacher T1 = new Teacher();
T1.setName("归不归");
T1.setSexType(MAN_TYPE);
T1.setAge(36);
for(int i =0 ;i<15 ;i++){
System.gc();//主动触发GC 垃圾回收 15次--- T1存活
}
Teacher T2 = new Teacher();
T2.setName("吴勉");
T2.setSexType(MAN_TYPE);
T2.setAge(18); // 这男人脸酸 还是 让他岁数小点吧。
Thread.sleep(Integer.MAX_VALUE);//线程休眠
}
}
你也发现原因了吧!
- T1 归不归在实例化对象后我们主动触发了 15 次的系统GC 。
而当一个对象存活过15次 GC 的时候就要从年轻代转移到老年代了。这也是为什么 T1 在老年代 ,T2 在新生代。 - 下面说一下详细的细节
T1 的回收细节总共分为三个阶段
GC 次数 | 移动前存放位置 | 移动后存放位置 |
---|---|---|
1 | eden | from |
2 | from | to |
3 | to | from |
4 | from | to |
… | to | from |
14 | from | to |
15 | to | tenured |
而 T2 没有经历过一次 GC 所以仍然存放在 eden 区域。
栈信息
查看方式:先选中 1 号 main 线程,然后再点击那个 2 号长方形的物体。
从上图中可以看见 hotspot 把虚拟机栈和本地方法栈的实现合二为一了。
方法区
常量池
Class 常量池(静态常量池)
在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种 字面量 和 符号引用。
- 字面量:
定义:给基本类型变量赋值的方式就叫做字面量或者字面值。
比如:String a=“b” ,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。 - 符号引用:
定义:Java 在编译的时候每一个 Java 类都会被变编译成一个 class 文件,但由于编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替。
运行时常量池
定义:运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
- 直接引用
定义:直接引用是 JVM 在加载类的时候,能过获得所有被引用类的实际地址,这个时候就替换运行时常量池中的符号引用为实际地址,作为直接引用。
符号引用与直接引用的转换
// A.java 类 这个是我们的代码
package cctv.just.dance
public A class {
private C b; // 这里使 B 类的引用
}
public C class {
}
在编译后会转为符号引用(我是这样理解的。)
JVM 加载类的时候会在运行时常量池中 存入 **cctv.just.dance.B **
当加载 B.class 的时候会 把 运行时常量池的 符号引用 **cctv.just.dance.B ** 转为直接引用。
字符串常量池
这个东西比较难理解需要单开一章进行讲解。JVM 深入理解 - 字符串常量池
深入理解内存区域总结:
以下几个方面总结:
- 功能
- 虚拟机栈:以栈帧的方式存储方法的执行过程中,存储 8 大基础类型,与引用类型的指针,和方法出口,和执行引擎的操作区。
- 堆:用来存储虚拟机栈中所有的指向对象。
- 存储内容
- 虚拟机栈:
- 局部变量表:八大基础类型 + 对象引用
- 返回地址(方法执行结束后的返回地址)
- 操作数栈(JVM 执行引擎的操作区)
- 动态链接
- 堆
- 各种对象
- 字符串常量池中指向的内存地址,的实际内容存放处。
- 虚拟机栈:
- 线程独享还是共享
- 虚拟机栈:线程独享
- 堆:线程共享
- 空间大小
- 栈空间的大小要远远小于堆空间。
虚拟机内存优化技术
- 栈的优化技术 —— 栈帧之间数据的共享
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠。(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时
就可以直接公用一部分数据,无需进行额外的参数复制传递了。
使用 JHSDB 工具可以证实上面图片的说法。
验证代码如下
/**
* VM参数
* JVM对栈帧空间的优化
*
**/
public class JVMStack {
public int work(int x) throws Exception{
int z =(x+5)*10;//局部变量表有, 32位
Thread.sleep(Integer.MAX_VALUE);
return z;
}
public static void main(String[] args)throws Exception {
JVMStack jvmStack = new JVMStack();
jvmStack.work(10);//10 放入main栈帧 10 -> 操作数栈
}
}