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. 在1.8以前是和Jvm共享空间(PermGen永久代),在1.8以后和操作系统共享
    1. 1.8以前常量池也是放在方法区的,1.8以后常量池放到了heap

内存溢出

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
  • 1.8以后移动到堆中,回收效率提升 mirrcr-gc

垃圾回收

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. 类诞生和成长的地方,甚至死亡

老年区

  1. FULL GC
  2. 这个区域常驻内存的。用来存放IDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些运行环境
    1. Jdk1.6之前:永久代,常量池是在方法区;这个区域常驻内存的.用来存放IDK自身携带的类对象.接口元数据,存储的是JAVA运行时的一些环境
    2. Jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
    3. Jdk1.8之后:无永久代,常量池在元空间

永久区\元空间

  1. 逻辑上存在,实际不存在

堆内存调优

  1. -Xms1024m -Xmx4024m -XX:+PrintGCDetails
  2. JProfile && MAT
    1. 导出 dump file 参数:-XX:+HeapDumpOnOutOfMemoryError
  3. jstat: jstat(JVM statistics Monitoring) 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  4. jmap: jmap(VM Memory Map)命令用于生成 heap dump 文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError 参数来让虚拟机出现 OOM 的时候·自动生成 dump 文件。jmap 不仅能生成 dump 文件,还阔以查询 finalize 执行队列、Java 堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
  5. jhat: jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析 jmap 生成的 dump,jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的 dump 文件复制到本地或其他机器上进行分析。
  6. jstack: jstack 用于生成 java 虚拟机当前时刻的线程快照。jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。如果 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。

JMM

Java Memory Model

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

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

原子性

JMM保证除long 和double 外的基本数据类型的读写操作是原子性的,另外synchronized也可以提供原子性保证。sychronized的原子性是通过Java的两个字节码指令 monitorenter 和 monitorexit 来保证的。

可见性

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

有序性

对有序性的保证,主要通过volatie和一系列happers-before原则。volatie的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。
happens-before原则包括一系列规则,如:

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

对象头

  • 普通对象
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:取常量值(运行时常量池)