02-JVM内存结构

JVM 内存结构

共享区域和私有区域

线程共享和线程私有的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来实现类似断点续传的效果
  • 不存在内存溢出问题
程序计数器PC效果图

虚拟机栈

栈:线程运行时需要的内存空间

栈帧 Frame

每个方法运行时需要的内存空间,一个方法对应一个栈帧。

方法中出现的局部变量(非成员变量和 static 静态变量)保存栈帧的局部变量表中。

活动栈帧即为线程当前正在执行的那个方法,也就是栈顶

通过debugger来查看栈和栈帧

问题讨论

问:栈内存是否需要进行垃圾回收机制管理?

答:不需要,栈所占用的内存本质上就是一个个栈帧所占用的内存。栈帧会随着方法的结束自动被弹出栈,这部分内存会被自动回收复用,不需要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;
}

注:

  1. 成员变量、类变量都不会出现在栈帧的局部变量表 LocalVariableTable 中
  2. 区分引用变量和引用对象,引用变量是局部变量,本身不会逃离方法作用域,但是引用对象可能会逃离方法的作用域

栈内存溢出(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 代码在后台运行

  1. 先使用top来定位哪个进程对CPU的占用过高

  2. 再使用ps H -eo pid,tid,%cpu | grep <指定过滤的进程id>来定位哪个线程对CPU的占用过高

  3. 使用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 可以使得这行代码失效。但是这个参数也会造成直接内存不能够及时被回收,


   转载规则


《02-JVM内存结构》 熊水斌 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
AQS队列同步器 AQS队列同步器
Node 节点class Node { // static final 修饰的常量 static final Node SHARED = new Node(); static final Node EXCLUSIVE
2023-03-20
下一篇 
线程安全的集合 线程安全的集合
JUC中的线程安全的集合 所谓的线程安全的集合,指的是集合中的每一个方法是原子操作,例如 get()、put()等。但这并不表示使用了线程安全的集合就不会造成线程安全问题,正如事务不是简单地由一系列原子操作堆叠就可以实现的一样,只有正确
2023-03-16
  目录