一、什么是 JVM?

  Java Virtual Machine:Java 程序的运行环境(Java 二进制字节码的运行环境)。

优点:

  • 一次编写,到处运行;
  • 自动内存管理,垃圾回收功能;
  • 数组下标越界检查;
  • 多态

1、JVM,JRE,JDK

YH1MEGHGNCJLGBZJGW2BT.png

2、常见的 JVM

287UVQ0RW4FRNN2I7I7.png

3、JVM 内部结构

OKLQO7VYKVO8EZT5IX.png

二、程序计数器

  Program Counter Register 程序计数器,用来存储 jvm 指令的地址,是一个寄存器。

  • 作用:记住下一条 jvm 指令的执行地址。
  • 特点:程序计数器是线程私有的,即每个线程都有,线程阻塞后其值需要保存。不会存在内存溢出。(内存溢出 OOM:指程序申请内存时,没有足够的内存供申请者使用;内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间)

1、工作原理

  Java 源码通过编译后生成二进制字节码(即 jvm 指令),这些二进制字节码无法直接让 CPU 运行,需要通过解释器解释成机器码后 CPU 才能执行。而程序计数器用来存储解释器下一次解释的 jvm 指令地址。

Java 编译和解释

  Java 是一种特殊的高级性语言,它既有解释性语言的特征,也有编译性语言的特征,因为它是经过先编译,后解释的过程。HelloWorld.java → HelloWolrd.class → 特定平台的机器代码。

  • 编译是将源程序翻译成二进制字节码,这种机器码是不可以直接执行的。
  • 解释程序不产生目标代码,它逐条地取出程序中的语句,边解释,边执行;解释器把字节码文件边解释成机器语言边交给 CPU 执行。

三、虚拟机栈

  Java Virtual Machine Stacks:Java 虚拟机栈。

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

1、使用 Debug 观察虚拟机栈

  使用 Idea 对下面的程序以 Debug 方式执行,观察虚拟机栈的情况。

public class Jvm {

    public static void main(String[] args) {
        a();
    }

    public static void a() {
        b(1, 2);
    }

    public static int b(int a, int b) {
        int c = a + b;
        return c;
    }
}

  Frames 反应虚拟机栈的情况,每调用一个方法都会在顶部添加一行(即 栈顶,每一行就是一个栈帧),当前方法执行完后,当前行会删除,然后执行下一行的方法。最顶部的方法就是当前线程的活动栈帧。

S7OBT7O3TCN9BBSOU.png

2、设置虚拟机栈的大小

  通过下面递归死循环,来判断默认情况下虚拟机栈能存储的栈帧数。

public class Jvm {

    static int count = 0;

    // 栈帧过多导致栈内存溢出 java.lang.StackOverflowError
    public static void main(String[] args) {
        try {
            a();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    public static void a() {
        count++;
        a();
    }
}

  上面的代码运行结果都是 2万 左右,修改虚拟机栈的大小后,再次执行,结果只是 2700 左右。(m,k,没有单位是 b)

TGRL9P8ICAEWTVHON.png

3、三个问题

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

  栈内存无非是一次次的方法调用,所产生的栈帧内存。而栈帧内存在每一次方法调用结束后都会弹出栈并释放内存,所以不需要垃圾回收来管理。

(2)栈内存分配越大越好吗?

  栈内存分配越大可以增加栈帧的容量,但是每个线程所需的内存也随之增大,物理内存不变的情况下,会影响线程数。

(3)方法内的局部变量是否线程安全?
  • 如果方法内,局部变量没有逃离方法的作用范围,它是线程安全的。
  • 如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全。
    // 安全
    public static void m1() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(1);
        System.out.println(stringBuilder.toString());
    }

    // 不安全
    public static void m2(StringBuilder stringBuilder) {
        stringBuilder.append(1);
        System.out.println(stringBuilder.toString());
    }

    // 不安全
    public static StringBuilder m3() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(1);
        return stringBuilder;
    }

4、线程运行诊断

CPU 占用过多

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

程序运行很长时间没有结果

  • jstack 进程id:看有没有发生死锁。

四、本地方法栈

  本地方法栈是 Java 虚拟机调用一些本地方法时,需要给本地方法提供的一些内存空间。本地方法(Native Method):不是由 java 代码编写的方法。

五、堆(Heap)

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

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

1、设置堆大小

  运行一下代码会出现堆内存溢出:java.lang.OutOfMemoryError: Java heap space。

public class Jvm {
  
    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);
        }
    }
}

  上面的代码运行结果是 25,修改堆的大小,再次执行,结果是 17。

NWV59ZTU2XBCD8PLX0I3.png

2、堆内存诊断

  演示堆内存使用情况的代码,在不同时间观察内存的使用情况。

public class Jvm {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1");
        Thread.sleep(30000);
        // 10 MB
        byte[] array = new byte[1024 * 1024 * 10];
        System.out.println("2");
        Thread.sleep(20000);
        array = null;
        System.gc();
        System.out.println("3");
        Thread.sleep(1000000L);
    }
}
(1)jps 工具

  查看当前系统中有哪些 java 进程以及该进程的进程号。

C:\software\IntelliJ IDEA Workspace\algorithm>jps
16160
1808 Jvm
19104 Jps
9440 Launcher
(2)jmap 工具

  查看堆内存占用情况,jmap -heap 进程id。

C:\software\IntelliJ IDEA Workspace\algorithm>jmap -heap 1808
Attaching to process ID 1808, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.211-b12

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2090860544 (1994.0MB)
   NewSize                  = 44040192 (42.0MB)
   MaxNewSize               = 696778752 (664.5MB)
   OldSize                  = 88080384 (84.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 33554432 (32.0MB)
   used     = 4698000 (4.4803619384765625MB)
   free     = 28856432 (27.519638061523438MB)
   14.001131057739258% used
From Space:
   capacity = 5242880 (5.0MB)
   used     = 0 (0.0MB)
   free     = 5242880 (5.0MB)
   0.0% used
To Space:
   capacity = 5242880 (5.0MB)
   used     = 0 (0.0MB)
   free     = 5242880 (5.0MB)
   0.0% used
(3)jconsole 工具

  图形界面的,多功能的监测工具,可以连续监测。

BOBEL8P0LS48PDKS.png

(4)jvisualvm 工具

  如果垃圾回收后,内存占用仍然很高。则使用 jvisualvm 的 堆Dump 功能查看堆的使用情况。

public class Jvm {

    public static void main(String[] args) throws InterruptedException {
        List<Studnet> studnets = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            studnets.add(new Studnet());
        }
        Thread.sleep(1000000000L);
    }
}

class Studnet {
    private byte[] big = new byte[1024 * 1024];
}

标题:JVM 的内部结构
作者:Yi-Xing
地址:http://47.94.239.232/articles/2021/01/21/1611226534659.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!