JVM-内存结构

总结

在这里插入图片描述

  • 方法区:类(class)
  • 堆:根据类所创建的实例,也就是对象
  • 而对象在调用方法的时候,就会用到虚拟机栈,程序计数器,本地方法栈
  • 方法执行时,是由执行引擎中的解释器来进行逐行执行
  • 方法中的热点代码,也就是被频繁执行的代码,会由即时编译器进行编译,可理解为优化后的执行
  • GC(垃圾回收):可以对堆中不再引用的对象进行垃圾回收。
  • 本地方法接口:调用操作系统底层所提供的一些方法。

程序计数器

定义

  • Program Counter Register 程序计数器(寄存器)
  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的。假设有多个线程,每一个线程都有着自己的程序计数器。
    • 不会存在内存溢出。
  • 在物理上通过寄存器来实现。

作用

  • 在指令的执行过程中,记住下一条jvm指令的执行地址。例如执行了0指令,程序计数器会记住指令3的执行地址。
//Java源代码所编译的二进制字节码(JVM指令)    java源代码
0: getstatic #20 			// PrintStream out = System.out;
3: astore_1 				// --
4: aload_1 					// out.println(1);
5: iconst_1 				// --
6: invokevirtual #26 		// --
9: aload_1 					// out.println(2);
10: iconst_2 				// --
11: invokevirtual #26 		// --
14: aload_1 				// out.println(3);
15: iconst_3 				// --
16: invokevirtual #26 		// --
19: aload_1 				// out.println(4);
20: iconst_4 				// --
21: invokevirtual #26 		// --
24: aload_1 				// out.println(5);
25: iconst_5 				// --
26: invokevirtual #26 		// --
29: return
  • 指令也不能被CPU执行,而需要通过解释器变成机器码,才能被CPU所理解。

虚拟机栈

定义

  • Java Virtual Machine Stacks (Java 虚拟机栈):线程运行时需要的内存空间。
  • 栈帧:每个方法运行时需要的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
  • 一个栈中可能存在多个栈帧。如图,当方法1调用方法2时。
    在这里插入图片描述
  • 演示:如图所示,左下角即为当前虚拟机栈的状况。
    在这里插入图片描述
  • 问题辨析
    1. 垃圾回收是否涉及栈内存? 不涉及
    2. 栈内存分配越大越好吗? 不是。因为物理内存有限,每个线程需要1M栈内存空间,栈内存分配的越大,线程就会变少。
    3. 方法内的局部变量是否线程安全?
      • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
      • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。但是,如果是基本类型变量也可以保证线程安全。

栈内存溢出

  • 栈帧过多导致栈内存溢出。在递归调用中,结束条件未设置正确,可能会导致此结果
  • 栈帧过大导致栈内存溢出。

线程运行诊断

  • 案例1:CPU占用过多
    • 用top定位哪个进程对cpu的占用过高
    • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
    • jstack 进程id
      • 可以根据线程id找到在查询结果中有问题的线程,进一步定位到问题代码的源码行号
ps H -eo pid,tid,%cpu | grep 进程编号 
  • 案例2:程序运行很长时间没有结果.
    • 也可通过上述的方式查找到相应的信息。发现线程死锁问题。
public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }

本地方法栈

  • 本地方法(与操作系统底层交互)使用的内存,就为本地方法栈。

定义

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

堆内存溢出演示

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
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);
        }
    }
}

堆内存诊断

  • jps 工具

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

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

    • 图形界面的,多功能的监测工具,可以连续监测
  • 注意事项:如下图所示,jsp需要切换到jbin目录下执行才生效
    在这里插入图片描述

  • 案例:垃圾回收后,内存占用仍然很高

    • jvisualvm

方法区

定义

  • 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
  • 注意StringTable的更改。在内存中的位置不同。
    在这里插入图片描述

方法区内存溢出

  • 测试类。需要设置虚拟机变量,设置最大的元空间为8m,因为1.8之后默认使用的本地内存,很难实现方法区内存溢出。
/**
 * 演示元空间内存溢出 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);
        }
    }
}

运行时常量池

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

StringTable

  • 在以下代码中,变化字符串对象后。会准备一片空间,如下的stringTable[ ]。当找不到时,就将字符串对象放入。
  • Java代码。注解表示对代码运行的解释,都源自于字节码文件。javap -v xx.class
// 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")
        //ldc #4  // String ab
		//s5这行代码,直接找#4,也就是string sb
        String s5 = "a" + "b";  //常量拼接 javac 在编译期间的优化,结果已经在编译期确定为ab
		
		System.out.println(s3 == s4);  //false,s4 时new 出来的一个新的字符串对象,在堆中,而s3在stringTable中。 
        System.out.println(s3 == s5);  
    }
}
  • constant pool
Constant pool:
   #1 = Methodref          #12.#36        // java/lang/Object."<init>":()V
   #2 = String             #37            // a
   #3 = String             #38            // b
   #4 = String             #39            // ab
   #5 = Class              #40            // java/lang/StringBuilder
   #6 = Methodref          #5.#36         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #5.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
   ·
   ·
   ·
  #37 = Utf8               a
  #38 = Utf8               b
  #39 = Utf8               ab
  • Code
 		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."<init>":()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/String;
        27: astore        4
        29: ldc           #4                  // String ab

StringTable特性

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

    //  ["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。x指向的对象在串池中,而s2实际也为串池中的ab
        System.out.println( s == x );   //false。s引用的对象是在堆中。
    }
}
  • 题目测试
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();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);  //false

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来实现的。每一个链称为桶。(图为未运行前的统计信息,运行循环100后1754变为1854)。当压入次数过多,运行循环100000后,会触发垃圾回收机制,1754不会变为100000+1754。
    在这里插入图片描述
  • 垃圾回收信息。GC
    在这里插入图片描述

StringTable性能调优

  • 主要是调整HashTable桶的个数
  • 调整: -XX:StringTableSize=桶个数。
  • 案例:考虑字符串是否入池。 有大量字符串操作,字符串可能存在重复的问题。将字符串入池,可减小堆内存的使用。
/**
 * 演示 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());  //入池后添加,对于内存的消耗明显的减小
                    address.add(line);  //不入池,直接添加
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

直接内存(Direct Memory)

定义

  • 常见于 NIO (即New I/O,最常用为ByteBuffer)操作时,用于数据缓冲区。
  • 属于操作系统内存,分配回收成本较高,但读写性能高。
  • 不受 JVM 内存回收管理。
/**
 * 演示 ByteBuffer 作用  	不能运行,只是做举例说明
 */
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);
    }
}

使用NIO的底层对比图。

  • 传统IO方式。
    在这里插入图片描述
  • NIO操作。划出一片直接内存,系统和java代码都可以直接调用。减少了传统IO的两次缓冲所费时间,少了一次缓冲区复制操作。
    在这里插入图片描述

直接内存的分配与释放

  • 代码实现分配与释放,是通过Unsafe对象来管理的,不是通过GC。在直接内存使用较多时,推荐使用该种方式进行手动的管理。
/**
 * 直接内存分配的底层原理: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();
    }
	
	//通过反射获取Unsafe
    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如何与Unsafe关联。
/**
 * 禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC  禁用显式的的垃圾回收
     */
    public static void main(String[] args) throws IOException {
    	//源码分析:构造器里直接就调用了Unsafe对象。
    	/* 回收机制
		 cleaner = Cleaner.create(this, new Deallocator(base, size, cap));   (当前对象,任务对象)
		 this:指当前对象,若当前对象被回收,则执行回调函数(任务对象)当中的run方法。
		 new Deallocator:回调函数。里面通过了unsafe对象对直接内存进行了释放。
		*/
        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 方法ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程(守护线程)通过 Cleaner 的 clean 方法调。
    • 用 freeMemory 来释放直接内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值