JVM--内存结构

JVM内存结构

一、JVM介绍

1.1JDK、JRE、JVM

Java development kit

Java runtime environment

Java virtual machine 跨平台

1.2JVM:跨语言的平台

  • Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。

  • JVM平台的各种语言可以共享Java虛拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。

  • Java技术的核心就是Java虚拟机(JVM, Java Virtual Machine),因为所有的Java程序都运行在Java虛拟机内部。

  • 作用

    Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。

  • 特点

    • 一次编译,到处运行
  • 自动内存管理

    • 自动垃圾回收功能
  • 数组下标越界检查

jvm是运行在操作系统之上的,它与硬件没有直接的交互。

1.3JVM的整体结构

二、内存结构

2.1程序计数器

定义

Program Counter Register 程序计数器(寄存器)

  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的
    • 不会存在内存溢出

作用

2.2虚拟机栈

定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

1.垃圾回收是否涉及栈内存?

答:不会。栈内存在方法调用结束后自动清除。

2.栈内存分配越大越好吗?

答:不是。因为栈内存变大会影响线程数量,栈越大线程越少。

3.方法内的局部变量是否线程安全?

  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的

  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出

  • 栈帧过多导致栈内存溢出

  • 栈帧过大导致栈内存溢出

通过设置-Xss来配置栈内存大小。

栈溢出案例
java 复制代码
public class stack {
    public static void main(String[] args) throws JsonProcessingException {
        Dept dept = new Dept("开发部");
        Emp emp1 = new Emp("张三", dept);
        Emp emp2 = new Emp("李四", dept);
        dept.addEmp(emp1);
        dept.addEmp(emp2);
        dept.setEmps(Arrays.asList(emp1, emp2));

        ObjectMapper objectMapper = new ObjectMapper();
        System.out.println(objectMapper.writeValueAsString(dept));
    }
}
class Emp {
    private String name;
    private Dept dept;

    public Emp(String name, Dept dept) {
        this.name = name;
        this.dept = dept;
    }
}
class Dept {
    private String name;
    private List<Emp> emps;

    public Dept(String name) {
        this.name = name;
        this.emps = new ArrayList<>();
    }

    public void addEmp(Emp emp) {
        emps.add(emp);
    }

    public void set(String name) {
        this.name = name;
    }
    public void setEmps(List<Emp> emps) {
        this.emps = emps;
    }
}

上述代码会出现栈溢出错误:

原因是:

java 复制代码
 dept.setEmps(Arrays.asList(emp1, emp2));

emp中含有dept会出现:一直无限往里套娃!

复制代码
{name:'研发部',emps;[{name:'张三',dept:{name:'',emps:[{}]}}]}

为了避免出现这样的问题,需要在emp的dept字段加上@JsonIgnore

线程运行诊断

案例1:cpu占用过多(Linux环境下)

定位:

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

2.3本地方法栈

为本地方法提供内存空间。

2.4堆

定义

Heap 堆

  • 通过 new 关键字,创建对象和数组都会使用堆内存
  • 特点
    • 它是线程共享的,堆中对象都需要考虑线程安全的问题
    • 有垃圾回收机制

堆的组成

  • 新生代(Young Generation):新生代分为Eden Space和Survivor Space。在Eden Space中, 大多数新创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
  • 老年代(Old Generation/Tenured Generation):存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。
  • 元空间(Metaspace):从Java 8开始,永久代(Permanent Generation)被元空间取代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。
  • 大对象区(Large Object Space / Humongous Objects):在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。

堆内存溢出

使用-Xmx8m进行堆空间大小修改

堆内存诊断

  1. 案例
    垃圾回收后,内存占用仍然很高,使用jvisualvm进行可视化的查看。

2.5方法区

方法区中方法执行过程

当程序中通过对象或类直接调用某个方法时,主要包括以下几个步骤:

  • 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)。
  • 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
  • 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。

组成

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

  • 类信息:包括类的结构信息、类的访问修饰符、父类与接口等信息。
  • 常量池:存储类和接口中的常量,包括字面值常量、符号引用,以及运行时常量池。
  • 静态变量:存储类的静态变量,这些变量在类初始化的时候被赋值。
  • 方法字节码:存储类的方法字节码,即编译后的代码。
  • 符号引用:存储类和方法的符号引用,是一种直接引用不同于直接引用的引用类型。
  • 运行时常量池:存储着在类文件中的常量池数据,在类加载后在方法区生成该运行时常量池。
  • 常量池缓存:用于提升类加载的效率,将常用的常量缓存起来方便使用。

方法区内存溢出

jdk 1.8前导致永久代内存溢出:这里不在演示

jdk 1.8之后导致原空间内存溢出

场景:

  • spring
  • mybatis

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息


  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

StringTable(字符串常量池)

java 复制代码
// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
	String s1 = "a"; // 一开始存在常量池,用到才会创建对象,懒惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
	//s3 != s4
    String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab   上边s4是两个变量拼接,可以变,所以不确定
	System.out.println(s3 == s5);
}

分析:使用 javap -v 类名.class查看常量池文件



StringBuilder中的toString的方法:相当于调用了new String()方法。

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern() 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
java 复制代码
//  ["ab", "a", "b"]
public static void main(String[] args) {

    String x = "ab";
    String s = new String("a") + new String("b");

    // 堆  new String("a")   new String("b") new String("ab")
    String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

    System.out.println( s2 == x); //true 串池中的"ab"
    System.out.println( s == x ); //false 堆中的对象 跟 串池中的并不相等
}
java 复制代码
//StringTable面试题

public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab  StringTable{"a","b","ab"};
        String s4 = s1 + s2;   // new String("ab") 放在了堆中
        String s5 = "ab";      // 串中有,不会新建了
        String s6 = s4.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,
        // 会把串池中的对象返回

// 问
        System.out.println(s3 == s4); // false 因为堆中的对象跟串中的对象不相等
        System.out.println(s3 == s5); // true 都是串池中的对象
        System.out.println(s3 == s6); // true 都是串池中的对象

        String x2 = new String("c") + new String("d"); // new String("cd") 放到堆中
        x2.intern(); // 尝试放入串池,如果串池中有,则不会放入,如果没有,则放入串池,返回串池中的对象
        String x1 = "cd"; // 串中有,不会新建了

// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2); // true
    }

StringTable位置

在jdk1.6时候,位于常量池中,常量池位于永久代中,内存空间不足导致永久代空间不足。

在jdk1.8以后,位于堆中。

Java 复制代码
/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Demo1_6 {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(j).intern());//放到StringTable中
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

堆空间不足。

StringTable垃圾回收机制

java 复制代码
/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

StringTable性能调优

StringTable的底层相当于HashTable。可以调整桶的大小来改变StringTable的存储速度。

java 复制代码
/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }
    }
}

默认桶大小时:

考虑字符串是否存在,使用intern()

java 复制代码
/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
       System.in.read();
    }
}

2.6直接内存

定义

Direct Memory(直接内存)

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理
直接内存溢出
java 复制代码
/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}
直接内存释放原理
  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
java 复制代码
/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

ByteBuffer底层源码:

java 复制代码
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    //调用了 setMemory方法存储
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    //cleaner 为虚引用,当他指向的对象被垃圾回收后,它也回收
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
Java 复制代码
//Deallocator中的run方法
public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    //回收数据
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}
java 复制代码
/*
	关闭显示的垃圾回收
 * -XX:+DisableExplicitGC 显式的
 */
相关推荐
cui_ruicheng3 分钟前
Linux线程(四):线程池、日志系统与单例模式
linux·开发语言·单例模式
伊甸35 分钟前
Neo4j 常用语法速查(Cypher)
java·数据库·neo4j
小程故事多_807 分钟前
深度解析Claude Code,AI编码助手的底层架构与工作原理
java·人工智能·架构·智能体
文祐10 分钟前
三维数组在内存中的分布
开发语言·内存
通往曙光的路上11 分钟前
JUCJUCJUC
java·前端·数据库
kaikaile199512 分钟前
基于 MATLAB 的3D 蒙特卡洛光子传输模拟
开发语言·matlab·3d
吴声子夜歌16 分钟前
Java——ArrayList
java·arraylist
旷世奇才李先生17 分钟前
Java 内置HttpClient 深度实战与性能优化全指南
java
我是唐青枫18 分钟前
C#.NET YARP 认证授权实战:在网关层统一接入 JWT
开发语言·c#·.net
故事和你9120 分钟前
洛谷-【数据结构2-2】线段树2
开发语言·数据结构·算法·动态规划·图论