美文网首页
01 JVM内存结构组成

01 JVM内存结构组成

作者: 攻城老狮 | 来源:发表于2021-09-14 08:47 被阅读0次

1 JVM 概述图

image-20210906145327336.png

2 内存结构

2.1 程序计数器

2.1.1 概述

程序计数器(寄存器):是记录下一条jvm指令的执行地址行号,硬件通过寄存器实现。线程私有的; 不存在内存溢出,也是JVM规范中唯一没有OutOfMemoryError的区域。

2.1.2 示例

二进制字节码:JVM指令                   Java源码
# 程序计数器记录下一条JVM指令的地址行号
image-20210906190353691.png

2.2 虚拟机栈

2.2.1 栈定义

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

2.2.2 栈问题

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

    答案:栈内存不涉及垃圾回收

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

    答案:栈内存不是越大越好,如果设置过大,会影响可用线程的数量;比如-Xss1m、-Xss2m,在总内存不变的情况下,可用线程数量会减少

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

    答案:方法内的局部变量是线程安全(不会暴露给外部),因为方法内的局部变量各自在自已独立的内存中;如果是static int 就是线程共享的,就不是线程安全;主要看变量是否是线程共享、还是线程私有

    核心1:如果方法内局部变量没有逃离方法的作用范围,它是线程安全的

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

2.2.3 栈内存溢出

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

2.2.4 线程运行诊断

  1. CPU占用过高的诊断
public class TestDemo {
  public static void main(String[] args) {
    new Thread(()->{
      System.out.println("1...");
      while(true) {} //死循环,消耗CPU
    },"thread1").start();
  }
}

分析:用top定位哪个进程对cpu的占用过高;然后用ps命令进一步定位哪个线程引起的CPU占用过高;最后用jstack定位问题代码的源码行号

  1. 程序运行很长时间没有结果的诊断
public class TestDemo {
    static Object a = new Object();
    static Object b = new Object();
    public static void main(String[] args) throws InterruptedException {
       new Thread(()->{
           synchronized (a){
               try {
                   TimeUnit.SECONDS.sleep(2);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (b){
                   System.out.println("thread1...");
               }
           }
       },"thread1").start();

       TimeUnit.SECONDS.sleep(1);

       new Thread(()->{
           synchronized (b){
               synchronized (a){
                   System.out.println("thread2...");
               }
           }
       },"thread2").start();
    }
}

分析:通过jstack查看是否有死锁,得到如下结果证明,确实发生了死锁

Found one Java-level deadlock:
=============================
"thread2":
  waiting to lock monitor 0x00007fa9e0812a98 (object 0x000000076ab76e90, a java.lang.Object),
  which is held by "thread1"
"thread1":
  waiting to lock monitor 0x00007fa9e0812ca8 (object 0x000000076ab76ea0, a java.lang.Object),
  which is held by "thread2"

2.3 本地方法栈

本地方法栈中保存的是调用本地方法给的内存空间(不是由Java代码编写的,是由C/C++编写的方法),这样的方法很多,native方法,例如在Object类中的 clone() , hashCode() 等。

2.4 堆

2.4.1 概述

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

2.4.2 堆内存溢出

生产环境建议:如果内存比较大,内存溢出不会那么快的暴露;这时,我们可以将堆内存调小,让内存溢出尽早暴露

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class TestDemo {
    public static void main(String[] args)  {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String str = "hello";
            while (true){
                list.add(str);
                str += str;
                i++;
            }
        } catch (Throwable e){
            System.out.println(i);
            e.printStackTrace();
        }
    }
}

2.4.3 堆内存诊断工具

  • jps工具:查看当前系统中有哪些java进程
  • jmap工具:查看堆内存占用情况 jmap -heap pid
  • jstack 工具:线程监控
  • jconsole工具:图形界面的,多功能的检测工具,可以连续监测
  • jvisualvm工具:图形界面的,多功能的检测工具,可以连续监测;还有dump
  1. jconsole工具演示对堆内存的监控情况
//堆内存演示程序
public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}
image-20210906202704997.png
  1. Jvistualvm工具演示查看当前占用内存多的对象具体情况
//演示查看对象个数 堆转储 dump 
public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
        }
        Thread.sleep(1000000000L);
    }
}

class Student{
    private byte[] big = new byte[1024*1024];
}
image-20210906203503159.png

2.5 元空间/方法区

2.5.1 概述

方法区是线程共享的,在JVM启动时创建,在逻辑上属于堆的一部分,方法区也可能会内存溢出

image-20210906204708931.png

2.5.2 方法区内存溢出

1.8 以前会导致永久代内存溢出

  • 永久代内存溢出 java.lang.OutOfMemoryError: PerGen space
  • -XX:MaxPerSize=8m

1.8 之后会导致元空间内存溢出(系统内存)

  • 元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
  • -XX:MaxMetaspaceSize=8m
//元空间内存溢出
//-XX:MaxMetaspaceSize=10m -XX:-UseCompressedClassPointers
public class TestDemo extends ClassLoader {
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        try {
            TestDemo testDemo = new TestDemo();
            for (int i = 0; i < 100000; i++,j++) {
                ClassWriter cw = new ClassWriter(0);
                cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
                byte[] code = cw.toByteArray();
                testDemo.defineClass("Class"+i,code,0,code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

注:场景(动态加载类),如果框架使用不合理也会导致方法区内存溢出

  • spring
  • mybatis

2.6 常量池/运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
public class TestDemo {
    public static void main(String[] args)  {
        System.out.println("Hello world");
    }
}
# 反编译class文件
$ javap -v TestDemo.class
Classfile /Users/yorick/Documents/study/code/test-tmp/target/classes/com/yqj/TestDemo.class
  Last modified 2021-9-6; size 543 bytes
  MD5 checksum 383cf26c42b79cb732cb00a79b0dfead
  Compiled from "TestDemo.java"
public class com.yqj.TestDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/yqj/TestDemo
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/yqj/TestDemo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               TestDemo.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/yqj/TestDemo
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.yqj.TestDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/yqj/TestDemo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "TestDemo.java"

2.6 StringTable串池

2.6.1 常量池与串池的关系

常量池中的信息,都会被加载到运行时常量池中。但此时只是常量池中的符号,还没有变成java对象,当调用JVM命令ldc后,变为Java字符串对象,并加入到串池 StringTable 中

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
 
public class T03_StringTable {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 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")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
 
        System.out.println(s3 == s4); //false
        System.out.println(s3 == s5); //true
    }
}

2.6.2 StringTable字符串延迟加载

image-20210906213953137.png

由此可以看到,当运行到某行时,某行的符号才会转换为Java字符串对象,并加入到串池中。当串池中存在该字符串对象时,不会再创建对象,而是沿用StringTable串池中的对象。

2.6.3 StringTable的特性

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

2.6.4 StringTable位置

  • JDK1.6版本,字符串常量池是在永久代中;
  • JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
  • JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
//证明1.8版本时,字符串常量池是在堆中的
// -Xmx8m
public class TestDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        try {
            for (i = 0; i < 200000; i++) {
                list.add(String.valueOf(i).intern());
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}
# 报错信息,可以发现是堆内存溢出,说明StringTable串池是开辟在堆中的
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:267)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
    at java.util.ArrayList.add(ArrayList.java:464)
    at com.yqj.TestDemo.main(TestDemo.java:14)
106710

2.6.5 StringTable的垃圾回收

jdk1.8后,串池放在堆中,如果堆空间不足,串池也会进行垃圾回收。

//-Xmx8m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public class TestDemo {
    public static void main(String[] args) {
        int i = 0;
        try {
            for (int j = 0; j < 20000; j++) { 
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}
# 运行结果
# 触发了垃圾回收
[GC (Allocation Failure) [PSYoungGen: 1536K->496K(2048K)] 1536K->512K(7680K), 0.0014248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
20000
Heap
 PSYoungGen      total 2048K, used 1491K [0x00000007bfd80000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1536K, 64% used [0x00000007bfd80000,0x00000007bfe78cf8,0x00000007bff00000)
  from space 512K, 96% used [0x00000007bff00000,0x00000007bff7c010,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 5632K, used 16K [0x00000007bf800000, 0x00000007bfd80000, 0x00000007bfd80000)
  object space 5632K, 0% used [0x00000007bf800000,0x00000007bf804000,0x00000007bfd80000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     12884 =    309216 bytes, avg  24.000
Number of literals      :     12884 =    495800 bytes, avg  38.482
Total footprint         :           =    965104 bytes
Average bucket size     :     0.644
Variance of bucket size :     0.645
Std. dev. of bucket size:     0.803
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
# 串池中的实例数量也没有达到创建的20000,说明部分实例对象被垃圾回收器回收
Number of entries       :     17105 =    410520 bytes, avg  24.000
Number of literals      :     17105 =    917296 bytes, avg  53.627
Total footprint         :           =   1807920 bytes
Average bucket size     :     0.285
Variance of bucket size :     0.387
Std. dev. of bucket size:     0.622
Maximum bucket size     :         4

2.6.6 StringTable性能调优

  • 将StringTable的桶个数加大可以有效提升StringTable的性能。原因是减少了哈希碰撞,使得查找速率提升。(-XX:StringTableSize=桶个数(默认60013))

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

2.7 直接内存 Direct Memory

2.7.1 概述

直接内存用于数据缓冲区,分配回收的成本比较高,但读写性能高,不受JVM内存回收管理。

  • 普通内存:需要从用户态向内核态申请资源,即用户态会创建一个java 缓冲区byte[],内核态会创建系统缓冲区。
  • 直接内存:需要从用户态向内核态申请资源,即内核态会创建一块直接内存direct memory,这块direct memory内存可以在用户态、内核态使用。
image-20210909205300148.png

2.7.2 直接内存和传统方式读取大文件

public class TestDemo {

    private static String FROM = "/Users/yorick/Downloads/auth-resource.jar";
    private static String TO = "/Users/yorick/Downloads/copy_auth-resource.jar";
    private static int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        io();
        directBuffer();
    }
        //采用传统的读取文件方式
    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream fis = new FileInputStream(FROM);
             FileOutputStream fos = new FileOutputStream(TO)) {
            byte[] buffer = new byte[_1MB];
            int len;
            while ((len = fis.read(buffer)) != -1){
                fos.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io time: " + (end - start));
    }
        //采用直接内存的读取文件方式
    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
                FileChannel to = new FileOutputStream(TO).getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(_1MB);
            int len;
            while ((len = from.read(buffer)) != -1) {
                buffer.flip();
                to.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer time: " + (end - start));
    }
}

2.7.3 DirectBuffer的分配和回收原理

  • 使用了UnSafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
public class TestDemo {

    private static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        //获取Unsafe对象
        Unsafe unsafe = getUnsafe();
        //分配内存,返回分配的内存地址
        long memory = unsafe.allocateMemory(_1Gb);
        //给分配的内存赋值为初始值0
        unsafe.setMemory(memory,_1Gb,(byte) 0);
        System.in.read();
        //释放内存
        unsafe.freeMemory(memory);
        System.in.read();
    }

    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe  = (Unsafe) field.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

相关文章

网友评论

      本文标题:01 JVM内存结构组成

      本文链接:https://www.haomeiwen.com/subject/frzqgltx.html