JVM 内存结构
共享区域和私有区域
相关虚拟机 VM 参数项
StringTable 相关配置项
参数项 | 描述 |
---|---|
-XX:+PrintStringTableStatistics |
输出 StringTable 的统计信息 |
-XX:StringTableSize=<counts> |
设置 StringTable 中桶的数量 桶越多,哈希冲突的概率越低 |
参数项 | 描述 |
---|---|
-client |
选择client VM。 |
-server |
选择server VM。 |
-version |
输出JVM版本。 |
-showversion |
输出JVM版本并继续执行。 |
-help |
输出帮助信息并退出。 |
-X |
显示非标准选项的帮助信息。 |
-Xms<size> |
设置初始堆大小。 |
-Xmx<size> |
设置最大堆大小。 |
-Xss<size> |
设置线程栈大小。 在默认情况下, Linux 和 MacOS 的栈大小为 1MB,Windows 中会根据虚拟内存的大小来确定 |
-XX:+UseSerialGC |
使用串行垃圾回收器。 |
-XX:+UseParallelGC |
使用并行垃圾回收器。 |
-XX:+UseConcMarkSweepGC |
使用并发标记-清除垃圾回收器。 |
-XX:+UseG1GC |
使用G1垃圾回收器。 |
-XX:NewSize=<size> |
设置新生代初始大小。 |
-XX:MaxNewSize=<size> |
设置新生代最大大小。 |
-XX:SurvivorRatio=<ratio> |
设置Eden区和Survivor区的比例。 |
-XX:MaxTenuringThreshold=<threshold> |
设置对象进入老年代的阈值。 |
-XX:+TraceClassLoading |
输出类加载信息。 |
-XX:+TraceClassUnloading |
输出类卸载信息。 |
-XX:+DisableExplicitGC |
禁止调用System.gc()。 |
-XX:+PrintCompilation |
输出JIT编译器编译方法的信息。 |
-XX:+PrintInlining |
输出方法内联信息。 |
-XX:CompileThreshold=<threshold> |
设置方法触发JIT编译的调用次数阈值。 |
-XX:+PrintGC |
输出 GC 信息 |
程序计数器(PC)
作用
记录下一条 JVM 指令的执行地址。
特点
- 程序计数器是线程私有的,即每个线程都有自己的程序计数器。例如发生线程切换的时候,可以通过PC来实现类似断点续传的效果
- 不存在内存溢出问题

虚拟机栈
栈:线程运行时需要的内存空间
栈帧 Frame
每个方法运行时需要的内存空间,一个方法对应一个栈帧。
方法中出现的局部变量(非成员变量和 static 静态变量)保存栈帧的局部变量表中。
活动栈帧即为线程当前正在执行的那个方法,也就是栈顶
问题讨论
问:栈内存是否需要进行垃圾回收机制管理?
答:不需要,栈所占用的内存本质上就是一个个栈帧所占用的内存。栈帧会随着方法的结束自动被弹出栈,这部分内存会被自动回收复用,不需要GC来进行管理。GC主要针对堆内存。
问:栈内存分配越大越好吗?
答:并不是,栈内存分配的太大可能会造成能够分配的线程数量减少。
问:方法内的局部变量是否是线程安全的?
答:需要判断该局部变量是否逃离了方法的作用范围。因为线程共享的数据才会产生线程安全问题,例如,成员变量和类变量。但是局部变量是栈帧中定义的,而栈又是线程私有的,因此局部变量是线程私有的。但是私有的局部变量如果是引用类型的变量,那么可能修改所指向的共享数据(引用对象)。
// 形参逃离方法作用域
public String append(StringBuilder builder) {
builder.append("a");
builder.append("b");
return builder.toString();
}
// 返回值逃离方法作用域
public StringBuilder getBuilder() {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append("b");
return builder;
}
注:
- 成员变量、类变量都不会出现在栈帧的局部变量表 LocalVariableTable 中
- 区分引用变量和引用对象,引用变量是局部变量,本身不会逃离方法作用域,但是引用对象可能会逃离方法的作用域
栈内存溢出(Stack Overflow)
栈帧过多
没有退出条件的递归调用
循环引用问题
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@Data
@RequiredArgsConstructor
class Person {
@NonNull
private String name;
private Cat cat;
}
@Data
// 这是解决Lombok输出时候的循环依赖
@ToString(exclude = "owner")
class Cat {
@NonNull
private String name;
// 这是解决序列化时候的循环依赖
@JsonIgnore
private Person owner;
}
public class StackOverflowV1 {
public static void main(String[] args) {
Person person = new Person("person");
Cat cat = new Cat("cat");
person.setCat(cat);
cat.setOwner(person);
System.out.println(person);
}
}
线程运行诊断
案例一:CPU占用过多
使用 nohup 来运行 Java 程序,nohup 的作用是让 Java 代码在后台运行
先使用
top
来定位哪个进程对CPU的占用过高再使用
ps H -eo pid,tid,%cpu | grep <指定过滤的进程id>
来定位哪个线程对CPU的占用过高使用
jstack <进程id>
使用jstack输出的tid是十六进制表示, 而ps输出的是tid是十进制表示, 因此需要进行一定的转换.
案例二:迟迟没有输出程序运行结果(发生死锁)
同样使用 jstack 进行诊断,查看后面的输出信息
本地方法栈
native
修饰的方法占用的内存空间存放在本地方法栈中
Java 代码有时候不能直接和操作系统底层交互,需要通过调用 C 或 C++ 实现的方法,例如 Object 类中的 clone()
方法
堆
- 通过 new 关键字创建的对象都会使用堆空间
- 堆中的变量是线程共享的,堆中对象都需要考虑线程安全问题
- 有垃圾回收机制
堆内存溢出(OutOfMemory)
堆内存诊断
- jps 工具: 查看当前系统中有哪些java进程
- jmap 工具: 查看堆内存占用情况
- jconsole 工具: 图形化界面, 多功能监测工具, 可以连续监测
- jvisualvm
方法区
方法区用来存放类的元数据 Class 对象,内存溢出问题没有演示出来。
运行时常量池
字符串常量池 StringTable
StringTable性能调优
- -XX:StringTableSize=桶的个数
- 将字符串对象存放到字符串常量池中
直接内存(Direct Memory)
使用 ByteBuffer.allocateDirect()
来分配直接内存
文件数据从磁盘 -> 系统内存 -> JVM 虚拟机内存(堆栈) -> 程序使用
JVM 不能够直接使用系统内存中的数据,因此存在需要将系统内存中的数据复制到 JVM 的内存中
直接内存则是 JVM 可以直接从中读取数据的系统内存,直接内存的分配、回收成本比较高,但是读写性能也高
磁盘->直接内存->程序使用
直接内存并不由 JVM 的垃圾回收机制处理,而是由 unsafe 对象的 freeMemory 方法来释放,只有当 unsafe 对象被 gc 处理,才会去释放由其创建的直接内存。
调用 gc 方法会回收掉 unsafe 对象,因此,调用 gc 方法也会回收直接内存,不会存在内存泄漏问题。
Unsafe 对象的 allocateMemory() 分配直接内存,freeMemory() 释放直接内存。直接内存由 JAVA 中的 Unsafe 对象进行管理。
Cleaner 虚引用对象关联的 ByteBuffer 被回收 => Cleaner.clean() => Unsafe 对象的 freeMemory
System.gc() 显式地执行一次 Full GC,使用 VM 参数 -XX:DisableExplicitGC
可以使得这行代码失效。但是这个参数也会造成直接内存不能够及时被回收,