JVM系列笔记--内存结构

运行时的数据区域

在这里插入图片描述

程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,作用是用来选取下一条需要执行的字节码指令
  • 各条线程之间计数器互不影响,独立存储,线程私有的内存区域
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空
  • 此区域不会有内存溢出(OutOfMemoryError

虚拟机栈

  • 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  • 垃圾回收不涉及栈内存

线程安全

  • 多个线程执行以下方法时,每个线程都会创建一个栈帧存储变量x,即该变量属于线程私有,不会产生线程安全问题
    // 多个线程同时执行此方法
        static void m1() {
          
          
            int x = 0;
            for (int i = 0; i < 5000; i++) {
          
          
                x++;
            }
            System.out.println(x);
        }
    
  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
    public static void m1() {
          
          //安全
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
    
        public static void m2(StringBuilder sb) {
          
          有入口,m2有可能被多个线程调用,不安全
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
    
        public static StringBuilder m3() {
          
          
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            return sb;//可以逃离方法的作用范围,不安全
        }
    

内存溢出

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
  • VM options处可调整Java虚拟机内存大小:-Xss256k
    /**
     * 演示栈内存溢出 java.lang.StackOverflowError
     * -Xss256k
     */
    public class Demo1_2 {
          
          //无限递归调用导致创建栈帧过多
        private static int count;
    
        public static void main(String[] args) {
          
          
            try {
          
          
                method1();
            } catch (Throwable e) {
          
          
                e.printStackTrace();
                System.out.println(count);
            }
        }
    
        private static void method1() {
          
          
            count++;
            method1();
        }
    }
    

本地方法栈

  • Java虚拟机可以通过本地方法接口调用本地方法
    在这里插入图片描述

  • 通过new创建对象会在堆分配内存
  • 堆中对象都是线程共享的,需要考虑线程安全问题
  • 有垃圾回收机制

内存溢出

public class Demo1_5 {
    
    

    public static void main(String[] args) {
    
    
        int i = 0;
        try {
    
    
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
    
    
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
    
    
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

------------------报错
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at cn.itcast.jvm.t1.heap.Demo1_5.main(Demo1_5.java:19)
24

方法区

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

内存溢出

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader {
    
     // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
    
    
        int j = 0;
        try {
    
    
            Demo1_8 test = new Demo1_8();
            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); // Class 对象
            }
        } finally {
    
    
            System.out.println(j);
        }
    }
}

---------------------报错:元空间导致的溢出
5411
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 cn.itcast.jvm.t1.metaspace.Demo1_8.main(Demo1_8.java:23)

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
  • 对最简单的HelloWorld.java编译成的class文件反编译,查看类的基本信息
    public class HelloWorld {
          
          
        public static void main(String[] args) {
          
          
            System.out.println("hello world");
        }
    }
    
  • 反编译命令:javap -v HelloWorld.class
    ------------类基本信息
    Classfile /D:/downloads/资料 解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
      Last modified 2020-11-2; size 567 bytes
      MD5 checksum 8efebdac91aa496515fa1c161184e354
      Compiled from "HelloWorld.java"
    public class cn.itcast.jvm.t5.HelloWorld
      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            // cn/itcast/jvm/t5/HelloWorld
       #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               Lcn/itcast/jvm/t5/HelloWorld;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               HelloWorld.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               cn/itcast/jvm/t5/HelloWorld
      #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 cn.itcast.jvm.t5.HelloWorld();//无参构造方法
        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 4: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;
    
      public static void main(java.lang.String[]);//main方法
        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 6: 0
            line 7: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "HelloWorld.java"
    

字符串常量池

  • 常量池中的字符串仅是符号,第一次用到时才变为对象,懒加载
    // StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
    public class Demo1_22 {
          
          
        // 常量池中的信息,都会被加载到运行时常量池中, 这时 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 == s5);
        }
    }
    
    在这里插入图片描述
  • 字符串常量池底层使用HashTable,来避免重复创建字符串
  • 字符串变量拼接的原理是 StringBuilder (1.8)
    public class Demo1_21 {
          
          
    
        public static void main(String[] args) {
          
          
            String s1 = "a";
            String s2 = "b";
            String s3 = "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
        }
    }
    

直接内存

在这里插入图片描述

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

分配回收原理

  • 使用了 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);
            }
        }
    }
    
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44863537/article/details/109480273
今日推荐