• 【黑马程序员JVM学习笔记】02.内存结构


    1.程序计数器

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

    作用:
    记住下一条jvm指令的执行地址

    getstatic  #20    // PrintStream out = System.out;
    astore_1          // --
    aload_1           // out.print1n(1);
    iconst_1          // --
    invokevirtual #26 // --
    aload_1           // out.println(2);
    iconst_2          // --
    invokevirtual #26 // --
    aload_1           // out.println(3);
    iconst_3          // --
    invokevirtual #26 // --
    aload_1           // out.println(4);
    iconst_4          // --
    invokevirtual #26 // --
    aload_1           // out.println(5);
    iconst_5          // --
    invokevirtual #26 // --
    return
    

    特点:

    • 是线程私有的
    • 不会存在内存溢出

    2.虚拟机栈

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rynn34DS-1663580243335)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89d100aa-5690-4551-8f84-236b424fbb73/Untitled.png)]
    定义:
    Java Virtual Machine Stacks (Java虚拟机栈)

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

    问题辨析
    1.垃圾回收是否涉及栈内存?
    2.栈内存分配越大越好吗?
    3.方法内的局部变量是否线程安全?

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

    栈内存溢出

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

    /*演示栈内存溢出java.lang.StackOverflowError*/
    public class Demo1_1 {
    		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();
    		}
    }
    

    线程运行诊断

    案例1:cpu占用过多
    定位

    • 用top定位哪个进程对cpu的占用过高
    • ps H-eo pid,tid,%cpu|grep进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
    • jstack 进程id
      • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

    案例2:程序运行很长时间没有结果

    3.本地方法栈

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GU2Gn8MI-1663580243337)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf738ca6-dc52-4849-82d8-605f93ea7e5e/Untitled.png)]

    4.堆

    定义:
    Heap 堆

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

    特点:

    • 它是线程共享的,堆中对象都需要考虑线程安全的问题
    • 有垃圾回收机制

    堆内存溢出

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

    内存诊断

    1. jps 工具
    • 查看当前系统中有哪些java进程
    1. jmap 工具
    • 查看堆内存占用情况 jmap-heap进程id
    1. jconsole 工具
    • 图形界面的,多功能的监测工具,可以连续监测
    /*演示堆内存*/
    public class Demo1_3 {
    		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(30000);
    				array = null;
    				System.gc();
    				System.out.println("3...");
    				Thread.sleep(1000000L);
    		}
    }
    

    案例

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

    5.方法区

    定义

    https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html

    组成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hnqF3ZVW-1663580243338)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/92bf0c2e-44a4-4895-ad1c-512720e9e4c3/Untitled.png)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3tH4Uc3-1663580243339)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/203b1dd5-3c8b-442d-9f90-4c12b4cbbada/Untitled.png)]

    方法区内存溢出

    • 1.8以前会导致永久代内存溢出
    • 1.8之后会导致元空间内存溢出
    import jdk.internal.org.objectweb.asm.ClassWriter;
    import jdk.internal.org.objectweb.asm.Opcodes;
        /*
         *演示证元空间内存溢出
         *-XX:MaxMetaspaceSize=8m
         */
        public class Demo04 extends ClassLoader{//可以用来加载类的二进制字节码
        public static void main(String[] args) {
            int j = 0;
            try {
                Demo04 test = new Demo04();
                for (int i = 8; 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);
            }
        }
    }
    

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

    • 演示永久代内存溢出 java.lang.OutOfMemoryError:PermGen space
    • -XX:MaxPermSize=8m

    1.8之后会导致元空间内存溢出

    • 演示元空间内存溢出 java.1ang.OutOfMemoryError:Metaspace
    • -XX:MaxMetaspaceSize=8m

    场景

    • spring
    • mybatis

    运行时常量池

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

    StringTable

    jvm-StringTable详解_爱搞技术的吴同学的博客-CSDN博客_java stringtablesize

    先看几道面试题:

    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");//new String("cd")
    String x1 = "cd";  //"cd"
    x2.intern();
    // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
    System.out.println(x1 == x2);  //false
    

    StringTable叫做字符串常量池,用于存放字符串常量,这样当我们使用相同的字符串对象时,就可以直接从StringTable中获取而不用重新创建对象。

    字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

    // 在堆中创建字符串对象"ab"
    // 将字符串对象"ab"的引用保存在字符串常量池中
    String aa = "ab";
    // 直接返回字符串常量池中字符串对象”ab“的引用
    String bb = "ab";
    System.out.println(aa==bb);// true
    

    StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。

    StringTable特性

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

    StringTable位置
    在这里插入图片描述

    StringTable垃圾回收
    代码:

    /**
    演示 StringTable 垃圾回收
    -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    */
    public class Demo {
    	public static void main(String[] args) throws InterruptedException {
    		int i = 0;
    		try {
    		
    		} catch (Throwable e){
    		e.printStackTrace();
    		}finally{
    		System.out.println(i);
    	}
    }
    

    StringTable性能调优
    可参见博客:JVM专题(八)-StringTable调优

    6.直接内存

    定义

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

    直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    • 本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
    • 配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常

    直接内存(堆外内存)与堆内存比较

    1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
    2. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

    代码验证:

    package org.example;
    import java.nio.ByteBuffer;
    /**
     * 直接内存 与  堆内存的比较
     */
    public class ByteBufferCompare {
        public static void main(String[] args) {
            allocateCompare();   //分配比较
            operateCompare();    //读写比较
        }
        /**
         * 直接内存 和 堆内存的 分配空间比较
         * 
         * 结论: 在数据量提升时,直接内存相比非直接内的申请,有很严重的性能问题
         * 
         */
        public static void allocateCompare(){
            int time = 10000000;    //操作次数                           
            long st = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
                //ByteBuffer.allocate(int capacity)   分配一个新的字节缓冲区。
                ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接内存分配申请     
            }
            long et = System.currentTimeMillis();
    
            System.out.println("在进行"+time+"次分配操作时,堆内存 分配耗时:" + (et-st) +"ms" );
    
            long st_heap = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
                //ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。
                ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
            }
            long et_direct = System.currentTimeMillis();
    
            System.out.println("在进行"+time+"次分配操作时,直接内存 分配耗时:" + (et_direct-st_heap) +"ms" );
        }
        /**
         * 直接内存 和 堆内存的 读写性能比较
         * 
         * 结论:直接内存在直接的IO 操作上,在频繁的读写时 会有显著的性能提升
         * 
         */
        public static void operateCompare(){
            int time = 1000000000;
    
            ByteBuffer buffer = ByteBuffer.allocate(2*time);  
            long st = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
    
                //  putChar(char value) 用来写入 char 值的相对 put 方法
                buffer.putChar('a');
            }
            buffer.flip();
            for (int i = 0; i < time; i++) {
                buffer.getChar();
            }
            long et = System.currentTimeMillis();
    
            System.out.println("在进行"+time+"次读写操作时,非直接内存读写耗时:" + (et-st) +"ms");
    
            ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
            long st_direct = System.currentTimeMillis();
            for (int i = 0; i < time; i++) {
    
                //  putChar(char value) 用来写入 char 值的相对 put 方法
                buffer_d.putChar('a');
            }
            buffer_d.flip();
            for (int i = 0; i < time; i++) {
                buffer_d.getChar();
            }
            long et_direct = System.currentTimeMillis();
    
            System.out.println("在进行"+time+"次读写操作时,直接内存读写耗时:" + (et_direct - st_direct) +"ms");
        }
    }
    

    输出:
    在进行10000000次分配操作时,堆内存 分配耗时:12ms
    在进行10000000次分配操作时,直接内存 分配耗时:8233ms
    在进行1000000000次读写操作时,非直接内存读写耗时:4055ms
    在进行1000000000次读写操作时,直接内存读写耗时:745ms

    可以自己设置不同的time 值进行比较

    分析

    从数据流的角度,来看

    非直接内存作用链:
    本地IO –>直接内存–>非直接内存–>直接内存–>本地IO
    直接内存作用链:
    本地IO–>直接内存–>本地IO

    直接内存使用场景

    有很大的数据需要存储,它的生命周期很长
    适合频繁的IO操作,例如网络并发场景

    分配和回收原理

    • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
    • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
  • 相关阅读:
    入侵事件应急案例
    算法练习5——多数元素
    linux清理缓存垃圾命令和方法介绍
    下半年重要的10大美国写作比赛不要错过
    信息安全技术实验:用户数据备份
    国内常用源开发环境换源(flutter换源,python换源,Linux换源,npm换源)
    【ESP32_8266_WiFi (十一)】通过JSON实现物联网数据通讯
    Shell后门脚本
    使用IntelliJ Idea必备的插件!
    Vue中对于指令的介绍
  • 原文地址:https://blog.csdn.net/qq_55123599/article/details/126938452