1. 什么是 JVM?

1.1 定义

JVM,即Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)

1.2 益处

  1. 一次编写,到处运行的基石
  2. 主动内存治理,垃圾回收性能
  3. 数据下标越界查看
  4. 多态

1.3 JVM、JRE、JDK 比拟

1.4 常见的 JVM

JVM 只是套标准,基于这套标准的实现常见如下几种:

2. 内存构造

2.1 整体架构

JVM 内存构造次要蕴含:程序计数器虚拟机栈本地办法栈办法区,其中前三者是线程公有的,后两者是线程共享的。整体架构如下图:

2.2 程序计数器

2.2.1 作用

用于保留 JVM 中下一条要执行的指令的地址

2.2.2 特点

  • 线程公有

    • CPU会为每个线程调配工夫片,当以后线程的工夫片应用完当前,CPU就会去执行另一个线程的代码。程序计数器是每个线程所公有的,当另一个线程的工夫片用完,又返回来执行以后线程的代码时,通过程序计数器能够晓得应该执行哪一条指令
  • 不会存在内存溢出

    • 因为永远只存储一个指令地址,JVM标准中惟一一个不存在OutOfMemoryError的区域

2.3 虚拟机栈

2.3.1 定义

每个线程运行须要的内存空间,称为虚拟机栈。每个栈由多个栈帧组成,对应着每次调用办法时所占用的内存。每个线程只能有一个流动栈帧,对应着以后正在执行的办法

应用 IDE 调试演示,从控制台能够看到办法进入虚拟机栈后的样子,如下:

2.3.2 特点

  • 线程公有的
  • 存在栈内存溢出

2.3.3 问题辨析

  • 垃圾回收是否波及栈内存?

    • 不须要。因为虚拟机栈中是由一个个栈帧组成的,在办法执行结束后,对应的栈帧就会被弹出栈,内存会主动开释。所以无需通过垃圾回收机制去回收栈内存
  • 栈内存的调配越大越好吗?

    • 不是。因为物理内存是肯定的,栈内存越大,能够反对更多的递归调用,然而可执行的线程数就会越少
  • 办法内的局部变量是否是线程平安的?

    • 如果办法内局部变量没有逃离办法的作用范畴,则是线程平安
    • 如果局部变量援用了对象,并逃离了办法的作用范畴,则须要思考线程平安问题
    • 代码示例:
          //是线程平安的,因为局部变量sb只在m1()办法内应用      public static void m1() {          StringBuilder sb = new StringBuilder();          sb.append(1);          sb.append(2);          sb.append(3);          System.out.println(sb.toString());      }          //不是线程平安的,因为局部变量sb是作为参数传过来的,m2()执行的同时可能会被其余线程批改      public static void m2(StringBuilder sb) {          sb.append(1);          sb.append(2);          sb.append(3);          System.out.println(sb.toString());      }          //不是线程平安的,因为局部变量sb是作为返回值,可能会被多个其余线程拿到并同时批改      public static StringBuilder m3() {          StringBuilder sb = new StringBuilder();          sb.append(1);          sb.append(2);          sb.append(3);          return sb;      }

2.3.4 栈内存溢出

Java.lang.stackOverflowError 栈内存溢出

产生起因

  • 虚拟机栈中,栈帧过多(比方有限递归)
  • 每个栈帧所占用内存过大

2.3.5 线程运行诊断

  • CPU 占用过高。Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时须要定位占用CPU过高的线程

    1. top 命令,查看哪个过程占用CPU过高
    2. 拿到占用CPU过高的过程ID后,再通过 ps 查看哪个线程占用 CPU 过高。" ps H -eo pid, tid, %cpu | grep 查到的占用CPU过高的过程ID"
    3. 通过 "jstack 过程id" 命令查看过程中线程ID(nid),与上一步查出的线程ID(tid)来比照定位有问题的线程以及源码行号。留神jstack查找出的线程ID(nid)是16进制的须要转换。
  • 程序运行很长时间没有后果 。个别可能是产生了线程死锁,这时须要找到死锁的线程

    • 找到以后运行的java程序的过程ID(也能够应用 jps 命令)
    • 通过 "jstack 过程id" 命令查看过程中是否有死锁线程
    • 演示

      class A{}class B{}public class Demo004 {    static A a = new A();    static B b = new B();    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();    }}

2.4 本地办法栈

一些带有native关键字的办法就是须要JAVA去调用本地的C或者C++办法,因为JAVA有时候没法间接和操作系统底层交互,所以须要用到本地办法。

本地办法栈也是 线程公有的

2.5 堆

2.5.1 定义

通过new关键字创立的对象都会被放在堆内存

2.5.2 特点

  • 所有线程共享。堆内存中的对象都须要思考线程平安问题
  • 有垃圾回收机制
  • 存在堆内存溢出

2.5.3 堆内存溢出

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

2.5.4 堆内存诊断工具

  • jps工具:查看以后零碎中有哪些java过程
  • jmap工具:java内存映像工具。如查看堆内存占用状况 ,jmap -heap pid
  • jconsole工具:图形界面的,Java监督与治理控制台
  • jvisualvm工具:图形界面的,多合一故障解决工具

2.6 办法区

2.6.1 定义

办法区用于存储每个类的构造,比方运行时常量池、字段和办法数据,以及办法和构造函数的代码,包含在类和实例初始化以及接口初始化中应用的非凡办法。办法区域能够是固定大小,也能够依据计算的须要进行扩大,如果不须要更大的办法区域,则能够膨胀。

2.6.2 组成

办法区是一个抽象概念,JDK1.8以前办法区由永恒代实现,JDK1.8当前办法区由元空间实现

2.6.3 办法区内存溢出

  • JDK1.8之前会导致永恒代内存溢出

    • 设置永恒代最大内存参数:-XX:MaxPerSize=<size>
    • 异样信息:java.lang.OutOfMemoryError: PerGen space
  • JDK1.8之后会导致元空间内存溢出

    • 设置元空间最大内存参数:-XX:MaxMetaspaceSize=<size>
    • 异样信息:java.lang.OutOfMemoryError: Metaspace
    • 演示

      /** * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 */public class Demo006 extends ClassLoader { // 继承ClassLoader,能够用来加载类的二进制字节码        public static void main(String[] args) {        int j = 0;        try {            Demo006 test = new Demo006();            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);        }    }}```![image](/img/bVbZOEr)
尽管咱们本人编写的程序没有大量应用动静加载类,但如果咱们在应用内部一些框架时,可能大量动静加载类,就可能会导致元空间内存溢出。如 spring、mybatis 的动静代理

2.6.4 常量池

  • 常量池:Class文件中除了类的版本、字段、办法、接口等形容信息,还有常量池,用于寄存编译期生成的各种字面量和符号援用。就是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等信息。
  • 运行时常量池:常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把外面的符号地址翻译为实在地址
较于Class文件常量池,运行时常量池具备动态性,在运行期间也能够将新的常量放入常量池中,而不是肯定要在编译时确定的常量能力放入。最次要的使用便是String类的intern()办法

常量池与JVM字节码演示

public class Demo007 {    public static void main(String[] args) {        System.out.println("hello world");    }}

通过 "javap -v <class文件>" 反编译,查看二进制字节码,其中蕴含了类根本信息常量池类办法定义(蕴含虚拟机指令)
进入Demo007.class 所在目录执行,javap -v Demo007.class,输入后果如下,

Classfile /E:/codes/java_demos/out/production/jvm/indi/taicw/jvm/structure/methodarea/constantpool/Demo007.class  Last modified 2020-10-7; size 622 bytes  MD5 checksum 1356d96bb349e8a95e8c5df1cb36040c  Compiled from "Demo007.java"public class indi.taicw.jvm.structure.methodarea.constantpool.Demo007  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant 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            // indi/taicw/jvm/structure/methodarea/constantpool/Demo007   #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               Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007;  #14 = Utf8               main  #15 = Utf8               ([Ljava/lang/String;)V  #16 = Utf8               args  #17 = Utf8               [Ljava/lang/String;  #18 = Utf8               SourceFile  #19 = Utf8               Demo007.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               indi/taicw/jvm/structure/methodarea/constantpool/Demo007  #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 indi.taicw.jvm.structure.methodarea.constantpool.Demo007();    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 6: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007;  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 8: 0        line 9: 8      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       9     0  args   [Ljava/lang/String;}SourceFile: "Demo007.java"

2.6.5 字符串常量池 StringTable

字符串常量池 是常量池中专门用来寄存常量字符串的一块区域,应用 StringTable 进行存储,底层数据结构是一个哈希表。

StringTable 地位

  • JDK1.6版本中,字符串常量池是在永恒代
  • JDK1.7版本及当前版本,JVM 曾经将字符串常量池从办法区中移了进去,在 Java 堆(Heap)中开拓了一块区域寄存字符串常量池

StringTable 个性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象并放入字符串常量池
  • 利用字符串常量池的机制,来防止反复创立字符串对象
  • 字符串变量拼接的原理是 StringBuilder
  • 字符串常量拼接的原理是编译期优化
  • 能够应用 String#intern() 办法,被动将以后字符串退出到字符串常量池中,并返回常量池中该字符串对象的援用

    • JDK1.7及当前版本中调用intern()办法将这个字符串对象尝试放入串池,如果串池有则不会放入,如果没有则放入串池,并把串池中的对象援用返回
    • JDK1.6 版本中调用intern()办法将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,并把串池中的对象援用返回,而之前那个字符串对象还是存在的

字符串拼接与字符串常量池相干问题

public class Demo008 {    public static void main(String[] args) {        String s1 = "a";    //字符串 "a" 放入串池,StringTable ["a"]        String s2 = "b";    //字符串 "b" 放入串池,StringTable ["a", "b"]        String s3 = "ab";   //字符串 "ab" 放入串池,StringTable ["a", "b", "ab"]        //因为s1、s2是变量,只有运行期间能力确定具体值,所以底层实现是 new StringBuilder().append("a").append("b").toString(),返回一个新的"ab"字符串对象        String s4 = s1 + s2;        // javac 在编译期间的优化,后果曾经在编译期确定为 "ab",所以等价于 s5 = "ab",此时常量池已存在不必再创立新的"ab"对象        String s5 = "a" + "b";        // s4调用intern()办法尝试把"ab"放入串池中,此时串池已存在"ab", 并返回串池中的"ab"对象援用赋值给s6        String s6 = s4.intern();        //false,起因:s3 是串池中"ab"字符串对象的援用,s4是堆中"ab"对象的援用        System.out.println(s3 == s4);        //true,起因:s3、s5 都是串池中"ab"字符串对象的援用        System.out.println(s3 == s5);        //true,起因:s3、s6 都是串池中"ab"字符串对象的援用        System.out.println(s3 == s6);        String x2 = new String("c") + new String("d");        x2.intern(); //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd"]        String x1 = "cd"; //间接把串池中的"cd"对象援用赋值给 x1                //JDK1.7及之后版本,后果为true, 起因:x2调用intern()办法尝试把"cd"放入串池,此时串池不存在"cd"放入胜利,此时串池中的"cd"对象与堆中的是同一个        //JDK1.6版本,后果为false, 起因:x2调用intern()办法尝试把"cd"放入串池,此时串池不存在"cd"字符串,会复制一个新的"cd"对象放入串池中,此时串池中的"cd"对象与堆中的不是同一个        System.out.println(x1 == x2);        String x4 = new String("e") + new String("f");        String x3 = "ef"; //把字符串"cd"放入串池,StringTable ["a", "b", "ab", "cd", "ef"]        x4.intern();        //后果为false, 起因:x4调用intern()办法尝试把"ef"放入串池,此时串池已存在"cd"放入失败,此时x3是串池中"ef"对象的援用,而x4是堆中的"ef"对象援用        System.out.println(x3 == x4);    }}

2.6.6 StringTable 垃圾回收

因为在jdk1.7版本当前,字符串常量池是放在堆中,如果堆空间有余,字符串常量池也会进行垃圾回收。如下演示代码,设置最大堆内存为10m 并打印 gc 信息

/** * 演示 StringTable 垃圾回收 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc */public class Demo009 {    public static void main(String[] args) {        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);        }    }}

2.6.7 StringTable 性能调优

  • StringTable 的数据结构是哈希表,所以能够适当减少哈希表桶的大小,来缩小字符长放入StringTable所需的工夫

    • 设置StringTable桶大小参数:-XX:StringTableSize=<桶个数>
  • 对于有些反复的字符串能够思考放入常量池,能够缩小内存占用

2.7 间接内存

2.7.1 定义

间接内存并不是JVM内存区域的一部分,不受JVM内存回收治理常见于NIO操作用于数据缓存区调配回收老本高但读写性能高

java程序资源文件读取过程中,未应用间接内存和应用间接内存区别如下:

  • 未应用间接内存

    • 须要从用户态向内核态申请资源,内核态会创立零碎缓冲区,用户态会创立一个java 缓冲区byte[],而后数据从零碎缓存区复制到java缓存区
  • 应用间接内存

    • 须要从用户态向内核态申请资源,即内核态会创立一块间接内存direct memory,这块direct memory内存能够在用户态、内核态应用。间接内存是操作系统和Java代码都能够拜访的一块区域,无需将数据从零碎内存复制到Java堆内存,从而进步了效率

应用一般内存形式与应用间接内存形式读取大文件效率比拟,演示代码如下:

import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;/** * 演示 一般内存与间接内存读取文件效率 */public class Demo011 {    static final String FROM = "C:\\Users\\taichangwei\\Downloads\\CentOS-6.10-x86_64-bin-DVD2.iso"; //一个2.03G的文件    static final String TO = "E:\\a.iso";    static final int _1Mb = 1024 * 1024;    public static void main(String[] args) {        io();        directBuffer();    }    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);    }}/*执行三次别离用时io 用时:4944.8368directBuffer 用时:1927.7912io 用时:4989.7547directBuffer 用时:2287.7028io 用时:5022.2618directBuffer 用时:2453.5307 */

2.7.2 间接内存溢出

间接内存尽管不受JVM内存治理,然而物理内存是无限的,所以间接内存还是会存在内存溢出,java.lang.OutOfMemoryError: Direct buffer memory

演示间接内存溢出,代码如下:

import java.nio.ByteBuffer;import java.util.ArrayList;import java.util.List;/** * 演示 间接内存溢出 */public class Demo012 {    static int _100Mb = 1024 * 1024 * 100;    public static void main(String[] args) {        List<ByteBuffer> list = new ArrayList<>();        int i = 0;        try {            while (true) {                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);                list.add(byteBuffer);                i++;            }        } finally {            System.out.println(i);        }    }}

循环了 36 次,应用了3.6G的内存导致内存溢出了

2.7.3 间接内存的调配及回收原理

  • 底层应用了UnSafe对象实现间接内存的调配及回收,并且回收须要被动调用freeMemory()办法

    • 间接内存调配及回收的底层原理演示,代码如下:

      import sun.misc.Unsafe;import java.io.IOException;import java.lang.reflect.Field;public class Demo014 {    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);        //暂定期待。此时查看工作管理器(windows零碎),会发现有一个占用1G内存的java程序        System.in.read();        // 开释内存        unsafe.freeMemory(base);        //暂定期待。此时查看工作管理器(windows零碎),会发现方才占用1G内存的java程序内存开释了        System.in.read();    }    public static Unsafe getUnsafe() {        try {            //Unsafe 构造方法是私有化的,只能通过反射取得            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.allocateDirect(int capacity)" 来申请间接内存就能够了。ByteBuffer的实现类外部应用了Cleaner(虚援用)来检测ByteBuffer一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean()办法调用freeMemory()来开释内存

    • allocateDirect(int capacity) 办法外部实现:

          public static ByteBuffer allocateDirect(int capacity) {        return new DirectByteBuffer(capacity);    }

      DirectByteBuffer(int cap) 办法外部实现

          DirectByteBuffer(int cap) {                    super(-1, 0, cap, cap);        boolean pa = VM.isDirectMemoryPageAligned();        int ps = Bits.pageSize();        long size = Math.max(1L, (long)cap + (pa ? ps : 0));        Bits.reserveMemory(size, cap);        long base = 0;        try {            base = unsafe.allocateMemory(size);        } catch (OutOfMemoryError x) {            Bits.unreserveMemory(size, cap);            throw x;        }        unsafe.setMemory(base, size, (byte) 0);        if (pa && (base % ps != 0)) {            // Round up to page boundary            address = base + ps - (base & (ps - 1));        } else {            address = base;        }        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));        att = null;    }

      这里调用了一个Cleaner的create()办法,且后盾线程还会对虚援用的对象监测,如果虚援用的理论对象(这里指的是DirectByteBuffer对象)被回收当前,就会调用Cleaner的clean()办法,来革除间接内存中占用的内存

          public void clean() {        if (remove(this)) {            try {                this.thunk.run();            } catch (final Throwable var2) {                AccessController.doPrivileged(new PrivilegedAction<Void>() {                    public Void run() {                        if (System.err != null) {                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();                        }                        System.exit(1);                        return null;                    }                });            }        }    }

      其中 "this.thunk" 是上一步传入的 Deallocator 对象,它的run办法外部实现如下:

          public void run() {        if (address == 0) {            // Paranoia            return;        }        unsafe.freeMemory(address);        address = 0;        Bits.unreserveMemory(size, capacity);    }