JVM1️⃣概述、运行时数据区
1、JVM
1.1、含义
Java 虚拟机(Java Virtual Machine)
-
含义:Java 程序(二进制字节码)的运行环境,位于 Java 程序和操作系统之间。
-
优点
- 一次编写,到处运行(跨系统跨平台)
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
-
常见 JVM:Oracle JVM 规范
1.2、概念对比
- JVM
- JRE = JVM + 基础类库
- JDK = JRE + 编译工具
- JavaSE 开发 = JDK + IDE
- JavaEE 开发 = JavaSE + 服务器
1.3、体系结构(❗)
-
运行时数据区
- 方法区:类
- 堆:实例
- 虚拟机栈:引用(本地方法栈存放 Native 方法的引用)
- 程序计数器:存储下一行指令地址
-
执行引擎
-
解释器:逐行执行代码;
-
即时编译器:优化热点代码;
-
垃圾回收
-
2、运行时数据区
JVM 在执行 Java 程序的过程中,会将它所管理的内存划分为若干个不同的数据区,也就是运行时数据区。
2.1、程序计数器
-
作用:若当前执行的是 JVM 的方法,程序计数器保存当前执行指令的地址;若当前执行的是 native 方法,程序计数器为空。
-
特点:线程私有,不存在内存溢出。
-
流程:Java 类经过编译,成为二进制字节码。之后进行以下步骤:
- 解释器读取一行 JVM 指令,此时程序计数器存储下一行指令地址;
- 解释器将 JVM 转为机器码,交给 CPU 执行;
- 解释器从程序计数器获得下一行指令地址,此时程序计数器再存储下一行指令地址;
- 重复以上,直到代码结束。
// 二进制字节码 // Java代码 0: getstatic #20 // PrintStream out = System.out; 3: astore_1 // -- 4: aload_1 // out.println(1); 5: iconst_1 // -- 6: invokevirtual #26 // -- 9: aload_1 // out.println(2); 10: iconst_2 // -- 11: invokevirtual #26 // -- 14: aload_1 // out.println(3); 15: iconst_3 // -- 16: invokevirtual #26 // -- 19: aload_1 // out.println(4); 20: iconst_4 // -- 21: invokevirtual #26 // -- 24: aload_1 // out.println(5); 25: iconst_5 // -- 26: invokevirtual #26 // -- 29: return
2.2、虚拟机栈、本地方法栈
- 栈:仅允许在线性表的一端进行插入和删除操作,后进先出(LIFO)
- 虚拟机栈:每个线程运行时所需的内存;
- 由多个栈帧(Frame)组成
- 栈帧:对应每个方法调用时所占内存。
- 存储局部变量表、操作数栈、动态链接、方法出口等信息
- 每个线程只能有一个活动栈帧,对应当前正在执行的方法。
- 本地方法栈:跟虚拟机栈的区别在于,本地方法栈是给 Native 方法使用的。
- 特点:与线程同时创建,线程私有,不存在内存溢出,不存在垃圾回收。
栈内存
栈内存分配并非越大越好。物理内存大小固定,栈内存分配越大,线程数目越少。
- JVM参数:
-Xss size
,有默认值- Linux、macOS、Oracle Solaris:1024 KB;
- Windows:取决于虚拟内存;
- 内存溢出:java.lang.StackoverFlowError
- 栈帧过大(理论上不会)
- 栈帧过多:举例如下
- 逻辑判断条件永远为 true;
- 无限递归:递归没有终止条件;
- 代码循环引用,如一对多关系的实体类转 Json。
线程安全问题
针对方法内的变量,判断依据为变量是否线程私有。
- 线程安全:方法内的变量没有逃离方法的作用范围。
- 线程不安全:方法内的变量逃离方法的作用范围。
- 如:方法外部定义的引用类型参数、作为方法返回值、方法参数等;
- 可能存在一个以上的线程,对该变量进行存取操作。(如数据库的脏读、幻读等)
线程运行诊断
案例1:程序占用过多 CPU 内存
案例2:程序发生死锁(运行长时间没结果)
- 通过命令定位程序
- top 命令:实时显示进程信息(Progress)
- ps 命令:抓取进程快照,查看线程id
- 工具:jstack 进程id
2.3、堆
- 功能:分配所有的类实例和数组。(通过 new 关键字创建的对象)
- 特点:JVM启动时创建,线程共享(线程安全问题),存在垃圾回收机制。
堆内存
-
JVM参数:
-Xmx size
-
内存溢出:java.lang.OutOfMemoryError: Java Heap space(OOM)
- 原因:在对象的生命周期内,gc 不会对该对象进行回收。
- 举例:向 ArrayList 中无限添加 String 对象。
-
注:当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。
内存诊断工具
- jps:当前系统中的 Java 进程
- jmap:堆内存占用情况,某一时刻的快照( jmap - heap 进程id )
- jconsole:可视化,连续监测
- jvisualvm:可视化(堆转储:堆 Dump 功能)
2.4、方法区
方法区是一个概念上的结构。
- 功能:存储类的结构
- 方法和构造函数的代码;
- 运行时常量池、字段和方法数据;
- 接口初始化、实例初始化、类中的特殊方法;
- 特点:JVM 启动时创建,线程共享(线程安全问题),存在垃圾回收机制。
- 方法区在逻辑上,是堆的一部分。
- 实现位置
- <= JDK 1.6:永久代(Permanent Generation),位于堆中
- JDK 1.7:有永久代,逐渐 “去永久代”,部分位于堆中;
- >= JDK 1.8:元空间(Meta Space),位于本地内存中。
方法区内存
注:当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。
< JDK 8 :永久代
- JVM参数:
-XX:MaxPermSize=size
- 内存溢出:java.lang.OutOfMemoryError: PermGen space(OOM)
>= JDK 8:元空间
- JVM参数:
-XX:MaxMetaspaceSize=size
- 内存溢出:java.lang.OutOfMemoryError: Metaspace(OOM)
运行时常量池
运行时常量池(run-time contant pool)是常量池(constant_pool)的运行时表示。
- 每个类文件或接口都有一个常量池表,包含字面量、方法和字段引用等信息;
- 在类或接口创建时(class文件被加载),构造运行时常量池;
- 引用地址
- 常量池:符号地址;
- 运行时常量池:真实地址;
- 所有对象引用在最初都是符号引用,在实际使用时才创建对象。
字节码文件
可通过 javap 反编译工具查看
- 包含 类基本信息、常量池、类方法定义
- JVM 在执行指令时,根据符号地址查询常量池表,找到引用的对象;
- 重复以上动作,直到所有方法执行结束。
实例:在 main 方法中,运行System.out.println("helloworld ")
。以下是反编译代码的一部分。
(简单演示一行 JVM 指令的执行过程)
串池:StringTable
串池
- 存储字面量方式创建的字符串对象,具有不重复的特性。
- 底层实现:HashTable
- 字符串对象作为 HashTable 的 Key;
- Key 具有唯一性,可以保证字符串对象的不重复性。
- 位置
- JDK 1.6 前:永久代
- JDK 1.7 开始:堆
字符串对象创建
常量池中的字符串对象,最初仅是符号引用,在实际使用时才创建对象。
- 字面量创建:
String str = "abc";
- 类编译后,字面量 "abc" 存储在常量池;
- 类加载后,字面量 "abc" 存储在运行时常量池;
- 执行代码对应的 JVM 指令时
ldc
:查询运行时常量池表,找到字面量 "abc";astore
:在本地变量表中为其创建一个 String 对象;- 检查 StringTable 中是否存在该对象:不存在则加入对象,存在则返回 StringTable 中的该对象。
- new 方式:
String str = new String("abc");
- 在堆中为字符串对象实例分配空间。
- intern 方法:主动将字符串对象放入串池
- 串池中已有该对象:返回该对象;
- 串池中没有该对象:
- JDK 1.6 前:将对象的副本放入串池,返回副本对象;
- JDK 1.7 开始:将对象放入串池,并返回该对象。
字符串拼接
-
变量拼接:
- 变量:编译时无法确定值,运行时才能确定值;
- 本质上使用 StringBuilder.append().toString()
-
常量拼接
- 常量:编译时已确定值;
- 编译期优化,常量在编译时期就连在一起。
练习
练习1
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
String s7 = new String("ab");
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s4 == s7);
// 答:fasle、true、true、false(堆中的不同地址)
分析
值 | 说明 | 位置 | |
---|---|---|---|
s1 | "a" | 常量,字面量方式 | 串池 |
s2 | "b" | 常量,字面量方式 | 串池 |
s3 | "ab" | 常量拼接,字面量方式(编译期优化) | 串池 |
s4 | new String("ab") | 变量拼接,使用 new StringBuilder() | 堆 |
s5 | "ab" | 常量,字面量方式 | 串池 |
s6 | "ab" | intern(),返回串池中的对象 | 串池 |
s7 | new String("ab") | new 方式创建 | 堆 |
练习2
String x1 = new String("c") + new String("d");
x1.intern();
String x2 = "cd";
// 问
System.out.println(x1 == x2);
分析
x1 | x2 | x1.intern() | 结果 | |
---|---|---|---|---|
JDK 1.8 | new String("cd") | "cd" | 串池中已有 cd,不将 x1 放入 | false |
JDK 1.6 | new String("cd") | "cd" | 串池中已有 cd,不将 x1 放入 | false |
练习3:(调换练习2 最后两行代码位置)
String x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();
// 问
System.out.println(x1 == x2);
分析
x1 | x1.intern() | x2 | 结果 | |
---|---|---|---|---|
JDK 1.8 | new String("cd") | 串池中没有 cd,将 x1 放入 | "cd" | true |
JDK 1.6 | new String("cd") | 串池中没有 cd,将 x1 的副本放入 | "cd" | false |
性能调优
-
设置桶(bucket)数目
- JVM参数:
-XX:StringTableSize=桶个数
- 桶数目越少,平均每个桶存放的元素越多,查找效率越低;
- JVM参数:
-
使用 intern():利用串池保证字符串对象的不重复性,避免重复占用内存。
2.5、直接内存
Direct Memory
- 属于操作系统内存,不受 JVM 内存回收管理;
- 常见于 NIO 操作时,用于数据缓冲区;
- 分配回收成本较高,但读写性能高。
- 内存溢出:java.lang.OutOfMemoryError: Direct buffer Memory(OOM)
- 注:当一个线程抛出OOM异常后,它所占据的内存资源会被全部释放掉,不会影响其他线程。
使用
Java 只能操作堆内存,无法直接操作系统内存,效率低。
Direct Memory 在系统内存中分配一块直接内存(allocateDirect)。
-
Java 可以直接操作直接内存;
-
省去从系统缓存区到 Java 缓冲区的复制操作;
分配和回收原理
- 主动调用 Unsafe 对象的 freeMemory() 方法,回收 Direct Memory 的分配。
- ByteBuffer 子类:DirectByteBuffer 类
- 使用 Cleaner(虚引用类型)监测 ByteBuffer 对象;(观察者模式)
- 当 ByteBuffer 对象被回收,创建 ReferenceHandler 线程
- 调用 Cleaner 的 clean() 方法;
- clean() 方法会调用 freeMemory() 来释放直接内存。
3、相关概念
3.1、一次编译,到处运行
Java 跨平台性:一次编译,到处运行
- Java 程序执行的过程:
- 编译:程序运行前,编译器将 Java 源文件(.java)编译成字节码文件(.class)
- 类加载:加载、链接(验证、准备、解析)、初始化
- 运行时:
- 解释器:直接对代码解释执行。
- JIT 编译器:编译成机器码执行。
Hint
- 字节码无法直接运行,需通过 JVM 解释执行,或编译成机器码运行。
- 字节码是统一,每台机器的机器码有所不同。
- JVM 基于 C/C++ 开发,不具有跨平台性,需要安装相应版本 JVM。
- 只需在不同平台安装 JVM,无需修改源码,就能进行以上操作。
3.2、类的唯一性确定
如何唯一确定 Java 类:类加载器 + 全限类名。