自学内容网 自学内容网

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来配置栈内存大小。

栈溢出案例
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;
    }
}

上述代码会出现栈溢出错误:
在这里插入图片描述

原因是:

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

堆的组成

img

  • 新生代(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(字符串常量池)

// 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 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
//  ["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 堆中的对象 跟 串池中的并不相等
}
//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以后,位于堆中。

/**
 * 演示 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垃圾回收机制

/**
 * 演示 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的存储速度。

/**
 * 演示串池大小对性能的影响
 * -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()

/**
 * 演示 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 内存回收管理

在这里插入图片描述

直接内存溢出
/**
 * 演示直接内存溢出
 */
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 来释放直接内存
/**
 * 直接内存分配的底层原理: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底层源码:

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;
}
//Deallocator中的run方法
public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    //回收数据
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}
/*
关闭显示的垃圾回收
 * -XX:+DisableExplicitGC 显式的
 */

原文地址:https://blog.csdn.net/m0_51275144/article/details/144437751

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!