Skip to content

JVM

JVM的位置

JVM的体系结构

类加载器

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象

  1. 类加载过程

类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)

1. 加载,加载分为三步:
    1. 通过类的全限定性类名获取该类的二进制流
    2. 将该二进制流的静态存储结构转为方法区的运行时数据结构
    3. 在堆中为该类生成一个 Class 对象
2. 验证:验证该 Class 文件中的字节流信息复合虚拟机的要求,不会威胁到 JVM 的**安全**
3. 准备:为 Class 对象的静态变量分配内存,初始化其初始值
4. 解析:该阶段主要完成符号引用转化成直接引用
5. 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程
  1. 有哪些类加载器
    1. 虚拟机自带的加载器
    2. 启动类(根)加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用
    3. 扩展类加载器(ExtensionClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类
    4. 应用程序加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的

双亲委派机制

当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类;为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。

  1. 为了保证类加载的安全性
  2. 如果重复定义了一个 jdk 的类,则会层层递进类加载器,加载 jdk 提供 class
    1. app --> exc --> bootstartup
  3. 打破双亲委派机制
    1. 自定义类加载器,继承 ClassLoader 类,重写 loadClass 方法和 findClass 方法
    2. Tomcat

沙箱安全机制

Native

  1. 调用 C / c++ 语言库 JNI
  2. JNI 扩展 Java,融合更多的语言方法

PC寄存器

  1. 程序计数器:Program Counter Register
  2. 每个线程都有一个程序计数器是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

方法区

  1. 它是一个概念
  2. 方法区是被所有线程共享,**所有字段和方法字节码,**以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
  3. 方法区用于存储类结构、运行时常量池、方法与字段信息等;实例对象通常分配在堆中。静态字段属于类结构的一部分,但其引用指向的对象仍可能位于堆中
    1. static
    2. final
    3. Class Template
    4. const pool
  4. HotSpot 在 JDK 7 及以前主要使用永久代实现方法区,JDK 8 起改用本地内存中的 Metaspace。运行时常量池属于方法区;字符串常量池从 JDK 7 起移到堆中,两者不要混淆

内存溢出

java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.FixedValue;

public class MethodAreaOOM {
    public static void main(String[] args) {
        try {
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                
                enhancer.setCallback((FixedValue) () -> "Hello Metaspace OOM!");
                enhancer.create();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    static class OOMObject {
        // 这里可以添加一些字段和方法
    }
}

异常

java
java.lang.OutOfMemoryError: Metaspace

调整参数

  • 1.8元空间
shell
-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
  • 1.8以前永久代
shell
-XX:MaxPermSize=8m

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
java
PS D:\prog\Java\LearnJvm\LearnJvm\target\classes\cn\lopr\day01> javap -v .\HelloWorld.class
Classfile /D:/prog/Java/LearnJvm/LearnJvm/target/classes/cn/lopr/day01/HelloWorld.class
  Last modified 2024-1-2; size 561 bytes       
  MD5 checksum 04c264dba9cabc631d3b209d6fe8c739
  Compiled from "HelloWorld.java"              
public class cn.lopr.day01.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/lopr/day01/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/lopr/day01/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

StringTable

特性

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

位置

  • 1.6以前是在永久代中,回收效率低 full-gc
  • JDK 7 起 StringTable 移到堆中,可以随普通堆对象一起回收

垃圾回收

java
-XX:+PrintStringTableStatistics # 查看StringTable统计数据
-XX:+PrintGCDetails -verbose:gc
shell
Heap
 PSYoungGen      total 153088K, used 10527K [0x0000000715b80000, 0x0000000720600000, 0x00000007c0000000)
  eden space 131584K, 8% used [0x0000000715b80000,0x00000007165c7c98,0x000000071dc00000)
  from space 21504K, 0% used [0x000000071f100000,0x000000071f100000,0x0000000720600000)
  to   space 21504K, 0% used [0x000000071dc00000,0x000000071dc00000,0x000000071f100000)
 ParOldGen       total 349696K, used 0K [0x00000005c1200000, 0x00000005d6780000, 0x0000000715b80000)
  object space 349696K, 0% used [0x00000005c1200000,0x00000005c1200000,0x00000005d6780000)
 Metaspace       used 3156K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13163 =    315912 bytes, avg  24.000
Number of literals      :     13163 =    565720 bytes, avg  42.978
Total footprint         :           =   1041720 bytes
Average bucket size     :     0.658
Variance of bucket size :     0.659
Std. dev. of bucket size:     0.812
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1740 =     41760 bytes, avg  24.000
Number of literals      :      1740 =    156440 bytes, avg  89.908
Total footprint         :           =    678304 bytes
Average bucket size     :     0.029
Variance of bucket size :     0.029
Std. dev. of bucket size:     0.171
Maximum bucket size     :         2

性能优化

java
-XX:StringTableSize=20000 # Sring Bucket 数量 如果过字符串多的话 可以调整高,散列性能

如果你的应用存在大量的字符串,且重复率高,可以入池减少内存使用

直接内存

Direct memory

java
 ByteBuffer.allocateDirect() # 直接在系统内存中申请一块空间用于读写,申请多了会内存溢出

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

    private static long gb = 1024 * 1024 * 1024;

    public static void main(String[] args) {
        Unsafe unsafe = getUnsafe();
        long base = unsafe.allocateMemory(gb);
        unsafe.setMemory(base, gb, (byte) 0);
        unsafe.freeMemory(base);
    }

    static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe result = null;
            result = (Unsafe) field.get(null);
            return result;
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}

分配和回收原理

  • 会存在内存溢出,直接内存回收不会清理这块内存,需要手动调用System.gc()
    • 在实现源码中,会在创建改对象时注册为虚引用对象,这样在系统gc时就可以释放这块内存了
  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBufer 对象被垃
  • 圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
java
    DirectByteBuffer(int cap) {                   // package-private

        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;
        }
        // 注册虚引用对象,在Dealloacator中注册了一个接口用于回收内存
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

	private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

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

    }
java
-XX:+DisableExplicitGC # 禁用显示的自动垃圾回收

Java Virtual Machine Stacks (Java 虚拟机栈)

  1. 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  2. 每个线程只能有一个活动栈帧(顶部栈帧),对应着当前正在执行的那个方法
  3. 每个栈帧会记录父帧和子帧
  4. 栈里面存的都是对**堆里面对象的引用,**实际对象都在堆里面
  5. 栈底是 main 方法,栈帧过多会抛出栈溢出 Error
    1. -xss 调整栈大小
  6. 因为栈帧每次执行完成,都会释放,所以不存在内存回收
    1. 八大数据类型
    2. 对象的引用
    3. 实例的方法

三种JVM

  1. HotSopt
  2. JRockit
  3. J9VM

引用类型

强引用

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

使用 WeakReference 类来创建弱引用。

虚引用

又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来创建虚引用。

  1. 一个JVM只有一个堆内存,堆内存的大小是可以调节的
    1. 最大默认 4分之一 -Xmx
    2. 初始默认 64分之一 -Xms
  2. 类,方法,常量,变量,保存我们所有引用类型对象
  3. OOM 堆内存溢出
    1. 尝试扩大堆内存
    2. 分析内存

新生区

  1. 会发生轻GC
  2. 类诞生和成长的地方,甚至死亡

老年代

老年代主要保存经过多次 Minor GC 后仍存活或无法放入新生代的对象。类元数据属于方法区的实现,不存放在老年代;JDK 8 起 HotSpot 使用 Metaspace 保存类元数据。

永久区\元空间

  1. 方法区是 JVM 规范定义的逻辑区域;永久代和 Metaspace 是 HotSpot 在不同版本中的具体实现。

堆内存调优

  1. -Xms1024m -Xmx4024m -XX:+PrintGCDetails
  2. JProfile && MAT
    1. 导出 dump file 参数:-XX:+HeapDumpOnOutOfMemoryError
  3. jstat: jstat(JVM statistics Monitoring) 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  4. jmap 可以生成 Heap Dump,也可使用 -XX:+HeapDumpOnOutOfMemoryError 在 OOM 时自动生成。线上优先评估 jcmd GC.heap_dump,因为附加和转储过程可能造成停顿。
  5. jhat 已在 JDK 9 中移除。Heap Dump 通常复制到隔离环境后使用 Eclipse MAT、VisualVM 等工具分析,避免在线上机器消耗大量 CPU 和内存。
  6. jstack: jstack 用于生成 java 虚拟机当前时刻的线程快照。jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。如果 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。

JMM

Java Memory Model

缓存一致性协议,用于定义数据读取规则

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

原子性

JMM 对大多数基本类型和引用的单次读写提供原子性;规范允许非 volatilelongdouble 被拆分读写。volatile long/double 的读写具有原子性。synchronized 通过监视器互斥保护临界区;同步代码块通常对应 monitorentermonitorexit,同步方法使用方法访问标志。

可见性

volatile强制变量的赋值会同步刷新回主内存中,强制变量的读取从主内存中加载,保证不同线程始终能看到该变量的最新值。这就保证了可见性。

有序性

对有序性的保证,主要通过 volatile、锁和一系列 happens-before 规则。volatile 会约束相关内存操作的重排序,但不是禁止所有指令重排。happens-before 包括程序次序、监视器锁、volatile、线程启动与终止等规则。

  • 程序顺序有原则,即一个线程内必须保证语义串行性。
  • 锁规则,即对同一个锁的解锁一定发生在再次加锁之前。

对象头

  • 普通对象
shell
|--------------------------------------------------------------|
|                      Object Header (64 bits)                 |
|------------------------------------|-------------------------|
|                Mark Word (32 bits) | Klass Word (32 bits)    |
|------------------------------------|-------------------------|
  • 数组对象
shell
|---------------------------------------------------------------------------------|
|                            Object Header (96 bits)                              |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits)              | Klass Word(32bits)    | array length(32bits)   |
|--------------------------------|-----------------------|------------------------|
  • 其中的 Mark Word 结构

32位

shell
|-------------------------------------------------------|--------------------|
|              Mark Word (32 bits)                      | State              |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0              | 01 |  Normal            |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1      | 01 | Biased             |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30                            | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30                    | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                                  | 11 | Marked for GC      |
|-------------------------------------------------------|--------------------|

64位

shell
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62                                         | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62                                 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                                               | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

字节码指令

  • astore_x:将数据存入LocalVariableTable,_x表示存入位置
  • ldc:取常量值(运行时常量池)