JVM内存结构

这里写目录标题

  • 引言
    • 常见的JVM
    • 学习路线
  • jvm内存结构
    • 1. 程序计数器
      • 1.1 定义
      • 1.2 作用
    • 2. 虚拟机栈
    • 2.1 定义
    • 2.2 栈内存溢出
    • 2.3 线程运行诊断
      • 案例一,cpu占用过高
      • 案例二, 迟迟不到结果
    • 3. 本地方法栈
    • 4. 堆
      • 4.1 定义
      • 4.2 堆内存溢出
      • 4.3 堆内存诊断
    • 5. 方法区
      • 5.1 定义
      • 5.2 组成
      • 5.3 方法区内存溢出
      • 5.4 运行时常量池
      • 5.5 StringTable
      • 5.5 StringTable 位置
      • 5.6 StringTable垃圾回收
      • 5.7 StringTable 性能调优
    • 6.直接内存
      • 6-1 定义
      • 6.2 分配和回收原理

引言

JVM内存结构_第1张图片

常见的JVM

JVM内存结构_第2张图片

学习路线

JVM内存结构_第3张图片

jvm内存结构

1. 程序计数器

1.1 定义

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

可以看做是当前线程所执行的字节码的行号指示器,字节码解释器的工作时就是通过改变这个计数器的值来选取下一条执行的字节码指令

特点

  • 线程私有的(因为每个线程执行的程序是不一样的)
  • 不会内存溢出

1.2 作用

JVM内存结构_第4张图片

程序计数实际上是由寄存器来实现的

2. 虚拟机栈

线程私有,且生命周期线程相同,每个方法运行时java虚拟机都会同步创建一个栈帧用于存储局部变量表,也就是栈帧是每个方法运行时需要的内存,比如方法的变,返回值等

JVM内存结构_第5张图片

2.1 定义

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

栈的默认大小

JVM内存结构_第6张图片

问题

  1. 垃圾回收是否涉及栈内存?
    不涉及,因为每次方法运行结束栈帧会自动弹出

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

    不会,栈内存越多那么,线程数目会越少

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

    • 局部变量是局部私有的,但是如果static会有线程安全问ti

    • 如果局部变量引用了对象,并逃离了该方法的作用范围也是不安全的

两个线程都会修改同一个StringBuilder对象

JVM内存结构_第7张图片

这里返回了说明其他线程也会拿到该对象进行修改,也不安全

JVM内存结构_第8张图片

2.2 栈内存溢出

  • 帧过多导致栈溢出(递归)
  • 栈帧过大

2.3 线程运行诊断

案例一,cpu占用过高

定位

  • 用top定位哪个进程对cpu占用过高

  • ps H -eo pid,tid,%cpu | grep 进程id

​ 可以查看进程内的线程的cpu占用

  • jstack 进程id
    • 可以根据线程id找到有问题的线程,进一步找到问题代码的行号

案例二, 迟迟不到结果

找到死锁

JVM内存结构_第9张图片

3. 本地方法栈

JVM内存结构_第10张图片

就是c/c++ 编写的方法和系统底层打交道

4. 堆

4.1 定义

Heap 堆

  • 通过new 关键字,创建的对象都会使用堆内存

特点

  • 线程共享,需要考虑线程安全问题
  • 有垃圾回收机制

4.2 堆内存溢出

JVM内存结构_第11张图片

java.lang.OutofMemoryError :java heap space 堆内存溢出

这就是对象一直不断被使用导致的,他不断自增

Xmx 可以控制堆空间的大小

所以排查堆内存溢出的问题,可以把堆内存设置小一点去测试(不然在内存大的机器中一开始不会暴露出问题)

4.3 堆内存诊断

  1. jps工具

    • 查看当前系统中有哪些java进程

    JVM内存结构_第12张图片

  2. jmap工具

    • 查看堆内存占用情况 jmap - heap 进程id
  3. jconsole工具

    • 图形界面的,多功能的监测工具,可以进行连续监测

案例

  • 垃圾回收后,内存占用仍很高

可视化工具jvisualvm

JVM内存结构_第13张图片

其中对Dump

JVM内存结构_第14张图片

JVM内存结构_第15张图片

JVM内存结构_第16张图片

5. 方法区

5.1 定义

是所有虚拟机线程共享的区域,它存储了

lt stores per-class structures such as the run-time constant pool, field andmethod data, and the code for methods and constructors, including the specialmethods (S2.9) used in class and instance initialization and interface initialization.

他逻辑上是我们堆的组成部分

但是具体实现上,不同jvm厂商实现不同

5.2 组成

JVM内存结构_第17张图片

  • 永久代用的堆内存 之前的版本
  • 元空间用的本地内存

5.3 方法区内存溢出

public class main1 extends ClassLoader {//可以用来加载类的二进制字节码

    public static void main(String[] args) {
        int j = 0;
        try {
            main1 test = new main1();
            for (int i = 0; i < 10000; i++,j++) {
                //ClassWriter 作用是生产类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                //版本号,public,类名,包名,父类,接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回 byte[]
                byte[] code = cw.toByteArray();
                //执行类的加载
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

JVM内存结构_第18张图片

这里设置元空间内存

出现了元空间内存溢出

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at com.itcast.itheima.xpp.main1.main(main1.java:26)
4865

Process finished with exit code 1

老版本是永久代溢出

JVM内存结构_第19张图片

总结

  • 1.8以前会导致永久代内存溢出java.lang.OutOfMemoryError: PermGen space
  • 1.8以后会导致元空间内存溢出java.lang.OutOfMemoryError: Metaspace

场景

  • spring
  • mybatis

JVM内存结构_第20张图片

他们会在运行期间动态的生成类的字节码完成类的加载

5.4 运行时常量池

反编译字节码文件

javap -v class文件

JVM内存结构_第21张图片

类的基本信息

  • 类文件

  • 基本时间

  • 签名

  • 版本

  • 访问修饰符

JVM内存结构_第22张图片

类的常量池

JVM内存结构_第23张图片

默认构造方法

JVM内存结构_第24张图片

方法(包含虚拟机的指令)

获取一个静态变量(System.out)

idc:加载一个参数

invoke:虚方法调用

根据#2查常量池里的表

image-20231005000022434

然后#2引用了一个成员变量

image-20231005000146156

又接着引用了#29#30

image-20231005000251723

JVM内存结构_第25张图片

类型是 java/io ,变量是out

所以常量池就是

  • 常量池就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

5.5 StringTable

StringTable 特性

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

常量池中的信息,都会加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有变为java字符串对象

等到执行到具体引用它的代码上

JVM内存结构_第26张图片

ldc #2就会找到a这个符号,然后把他变成字符串对象,然后把他作为key在StringTable[]中去找,看有没有取值相同的key,如果没有找到话,就会把这个a这个字符串对象放进去也就是放入串池中

JVM内存结构_第27张图片

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

使用拼接字符串变量对象创建字符串的过程

public class HelloWorld {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4=s1+s2;//new StringBuilder().append("a").append("2").toString()  new String("ab")
        System.out.println(s3==s4);//false
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
    }

反编译后的结果

	 Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        29: return

通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

字符串拼接的操作

  • 创建一个StringBuilder的对象
  • 调用StringBuilder的init 方法
  • 把s1加载过来
  • 调用append方法
  • 拿到s2 参数,调用append方法
  • 调用toString方法

在Java中,字符串常量池和堆是不同的内存区域,而字符串的创建方式决定了它们在哪个区域分配内存。

  1. 字符串常量池: 字符串常量池是专门用于存储字符串常量的区域。当你使用字符串字面量(比如 "abc")创建字符串时,Java首先会检查字符串常量池中是否已经存在相同内容的字符串。如果存在,它将返回对现有字符串的引用,而不是创建新的字符串对象。这样做有助于节省内存,因为相同的字符串在常量池中只存储一次。

    在你的代码中,String s1 = "a";String s2 = "b";String s3 = "ab"; 这三个字符串都是通过字符串字面量创建的,因此它们都存储在字符串常量池中。

  2. 堆: 堆是用于存储动态分配对象的内存区域。当你使用 new 关键字创建字符串对象时,该对象会在堆中分配内存。在你的代码中,String s4 = s1 + s2; 这行代码使用了字符串拼接操作,它会创建一个新的字符串对象,其内容是 s1 和 s2 的拼接结果。因为这是在运行时动态生成的,所以会在堆中分配内存。

因此,s1s2s3 是字符串常量池中的字符串,而 s4 是通过字符串拼接在堆中动态创建的字符串对象。这也是为什么 s3 == s4 的结果可能是 false,因为它们分别指向不同的内存位置。

使用字符串常量拼接创建字符串

public class HelloWorld {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4=s1+s2;//new StringBuilder().a|ppend("a").append("2").toString()  new String("ab")
        String s5="a"+"b";
        System.out.println(s5==s3);//true
    }
}


反编译的结果

 	  Code:
      stack=2, locals=6, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        //ab3初始化时直接从串池中获取字符串
        29: ldc           #4                  // String ab
        31: astore        5
        33: return

从常量池中直接取出来的ab符号

这是javac在编译期的优化,它认为ab都是常量都是确定的,那结果已经编译期间确定为ab

而上面的拼接是变量的拼接,还会变,那么只能动态的拼接使用StringBuilder的方式

intern方法 1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

public class HelloWorld {

    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");

        String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,这两种情况都会把串池中的对象返回
        System.out.println(s2 == x);//true
        System.out.println(s == x);//false
    }
}


intern方法 1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份(又创建了一个对象),再放入到串池中
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

package com.itcast.itheima.xpp;

public class main {
    public static void main(String[] args) {

        String s1="a";
        String s2="b";
        String s3="a"+"b";
        String s4=s1+s2;
        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");
        String x1="cd";
        x2.intern();
        System.out.println(x1==x2);//false


        String x4=new String("e")+new String("f");
        x4.intern();
        String x3="ef";
        System.out.println(x3==x4);//true

    }
}

JVM内存结构_第28张图片

5.5 StringTable 位置

JVM内存结构_第29张图片

  • JDK1.6 时,StringTable是属于常量池的一部分。
  • JDK1.8 以后,StringTable是放在中的。

5.6 StringTable垃圾回收

-Xmx18m -XX:+PrintStringTableStatistics -XX: +PrintGCDetails -verbose:gc

摄者虚拟机参数,分别是设置虚拟机堆内存的最大值,打印字符串表的统计信息,打印垃圾回收的详细信息

JVM内存结构_第30张图片

桶的数量, 总占用空间等信息

类名啊,方法名啊也是以字符串常量存储

JVM内存结构_第31张图片

JVM内存结构_第32张图片添加了一万多个对象,但是只有7千多个,因为内存不足触发垃圾回收

5.7 StringTable 性能调优

底层是一个hash表,性能和大小相关

太小的话就会容易出现hash冲突

因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=xxxx
//最低为1009


  • 考虑是否将字符串对象入池

  • 可以通过intern方法减少重复入池,保证相同的地址在StringTable中只存储一份

6.直接内存

6-1 定义

  • 常见于NIO操作,用于数据缓冲区

  • 分配回收成本较高,但读写性能高

  • 不受JVM 内存回收影响

JVM内存结构_第33张图片

这里系统缓冲区复制到java缓冲区造成了不必要的复制

public class Demo1_9 {
    static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

JVM内存结构_第34张图片

这里是使用了directbuffer 之后的效果

操作系统画出来一片缓冲区,这片缓冲区java代码可以直接访问,这样少了一次缓冲区的复制操作

直接内存溢出是Direct buffer memory

6.2 分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配和回收,并且回收需要主动调用freeMemory方法

  • ByteBuffer的实现类内部,使用Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过

    Cleaner的clean方法调用freeMemory来释放直接内存


/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 显式的
     */
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

直接内存的释放不能通过垃圾回收只能通过底层的unsafe对象的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);
        }
    }
}

JVM内存结构_第35张图片

当bytebuffer被回收时就会触发虚引用对象cleaner 中的create方法,然后会调用回调任务对象Delocator就会调用freeMemory方法

有时候我们会-XX:+DisableExplicitGC显式的 禁用显示的垃圾回收,因为这个很损耗性能,这样也会间接的让我们无法手动直接调用垃圾回收拉释放直接内存

这时候我们可以使用usafe对象来释放直接内存

(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);
    }
}

}




[外链图片转存中...(img-gIZ18FLN-1696576900268)]

当bytebuffer被回收时就会触发虚引用对象cleaner 中的create方法,然后会调用回调任务对象Delocator就会调用freeMemory方法



有时候我们会`-XX:+DisableExplicitGC显式的` 禁用显示的垃圾回收,因为这个很损耗性能,这样也会间接的让我们无法手动直接调用垃圾回收拉释放直接内存

这时候我们可以使用`usafe`对象来释放直接内存

你可能感兴趣的:(jvm,jvm,java,开发语言)