JVM

概念

什么是JVM?

JVM(Java Virtual Machine)即 Java虚拟机,可以简化理解为单供Java程序运行的虚拟机

JRE (Java Runtime Environment)即 Java 运行时环境,包含JVM及Java类库等运行必须的组件

JDK(Java Development Kit)即 Java 开发组件,包含了JRE及开发工具(javac、java、jar、javadoc)、调试工具(jdb)等

主流JVM

hotspot、JRockit、J9VM

类加载子系统

加载

类的元信息(类名、访问修饰符、父类、接口、字段表、方法表等)被读入到方法区

堆重生成代表该类的Class

双亲委派机制

应用类加载器在收到加载请求时会递归向上委派至启动类加载器,启动类加载器找不到则向下委派至扩展类加载器,再找不到则委派给应用类加载器(三层加载模型)

有效避免了自定义类覆盖系统类的问题(即完全限定类名相同)

package xyz.ssydx;

public class User {
    public String name;

    public static void main(String[] args) {
        User user = new User();
        Class<? extends User> aClass = user.getClass();
        // 获取应用级的加载器
        ClassLoader classLoader = aClass.getClassLoader();
        // 获取扩展级的加载器
        ClassLoader parent = classLoader.getParent();
        // 获取启动级(顶级、根)的加载器
        ClassLoader loader = parent.getParent();
        System.out.println("App: " + classLoader);
        System.out.println("Ext: " + parent);
        System.out.println("Boot: " + loader);
        
        /*
        控制台输出如下:
        App: sun.misc.Launcher$AppClassLoader@18b4aac2
        Ext: sun.misc.Launcher$ExtClassLoader@1b6d3586
        Boot: null
        */
    }
}

链接

验证

对class文件进行合规性和安全性验证:文件格式验证, 元数据验证,字节码验证,符号引用验证

准备

为静态变量分配内存

为静态字段分配初始值(不是自己设置的值,而是系统默认值)

为编译时常量直接赋值(自己设置的值)

解析

把符号引用(即变量名)转为直接引用(即地址)

初始化

​ 静态字段赋值(自己设置的值,如果有)

​ 静态代码块执行

实例化

​ 分配内存

​ 实例变量分配初始值(初始化零值,不是自己设置的值,而是系统默认值)

​ 设置对象头

​ 调用父类构造器

​ 实例字段赋值(自己设置的值)

​ 调用当前类构造器

运行时数据区

线程私有区

本地方法栈

它为 native 方法(用 C/C++ 等语言实现的方法) 提供支持。每个线程拥有独立的本地方法栈,生命周期与线程一致。 Java 中通过 native 关键字声明本地方法本地方法栈由栈帧组成(同Java栈)

程序计数器

程序计数器是一块较小的内存空间,它记录当前线程所执行的字节码指令的地址(即行号),类似于操作系统中的指令寄存器。

对于 Java 方法,程序计数器保存的是当前线程正在执行的 Java 字节码指令的地址。 对于 native 方法(如用 C/C++ 实现的方法),程序计数器的值为 undefined。 程序计数器是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的区域。

特点 描述
线程私有 每个线程独立拥有一个程序计数器
生命周期 与线程一致,线程启动时创建,线程结束时销毁
不会发生 OOM 不会抛出 OutOfMemoryError
很小的内存空间 通常只占用几十字节
字节码指令地址 存储当前线程正在执行的 Java 虚拟机字节码指令的地址
native 方法中无效 如果线程执行的是 native 方法,程序计数器的值为 undefined

栈(Java栈、Java虚拟机栈)

FIFO(先进先出)

线程启动时,虚拟机为其分配栈空间,每次方法调用都会在栈中压入栈帧(即每个栈帧对应一个方法的执行),方法执行完毕后对应的栈帧弹出

当线程正常执行完毕后其栈空间是清空的(所有栈帧都弹出),即栈空间的声明周期和其所属的线程同步,因此栈无需进行垃圾回收

栈帧

组成部分 描述
局部变量表(Local Variables) 存储方法中的局部变量(包括参数)、this引用等,以槽(slot)为单位
操作数栈(Operand Stack) 用于字节码指令执行时进行计算的临时存储空间
动态链接(Dynamic Linking) 指向运行时常量池的引用,支持方法调用时的符号引用解析
返回地址(Return Address) 方法执行完成后,程序计数器要跳转的位置
附加信息(可选) 如调试信息、异常处理表等(取决于具体 JVM 实现)

栈溢出

public class SOFTest {
    public static void main(String[] args) {
        new SOFTest().testA();
    }
    // 循环调用
    public void testA() {
        testB();
    }
    public void testB() {
        testA();
    }
}

线程共享区

方法区/元空间/永久代

不参与垃圾回收

内容 描述
类型信息 类或接口的结构信息
运行时常量池 存放类文件中的常量池信息
字段数据 静态变量及其初始值
方法代码 实例初始化方法、类构造器及普通方法的字节码指令
即时编译器编译后的代码缓存 热点方法编译成的本地机器代码

常量池

存放基本类型常量,字符串常量,符号引用等

整个堆的初始内存大小约为系统内存的1/64,最大可扩展内存约为系统内存的1/4

堆用于存储对象实例和数组

新生代和老生代的内存比约为1:2

package xyz.ssydx;

public class Memeory {
    public static void main(String[] args) {
        // 当前JVM获得的内存大小, 如果实际运行时不足会自动扩展至最大内存
        long totalMemory = Runtime.getRuntime().totalMemory();
        // JVM可以扩展的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory();
        // 空闲内存
        long freeMemory = Runtime.getRuntime().freeMemory();
        // 已使用的内存大小
        long usedMemory = totalMemory - freeMemory;
        System.out.println("totalMemory = " + totalMemory / 1024 / 1024);
        System.out.println("maxMemory = " + maxMemory / 1024 / 1024);
        System.out.println("freeMemory = " + freeMemory / 1024 / 1024);
        System.out.println("usedMemory = " + usedMemory / 1024 / 1024);
        
        /*
        控制台输出如下(示例):
        totalMemory = 241
        maxMemory = 3580
        freeMemory = 237
        usedMemory = 3
        */
    }
}

新生代

伊甸区:幸存区0:幸存区1=8:1:1

伊甸区:存储新生对象

幸存区:存储至少经过一次垃圾回收还未被回收的对象,幸存区分为两个区,两者每经过一次垃圾回收就会互换一次

注意:99%的对象都不会进入老年代

老年代

存储经过多次垃圾回收仍未被回收的对象,默认为15次

堆溢出

public class OOMTest {
    public static void main(String[] args) {
        // 不停新建更大的字符串
//        String str = "ssydx" + "123456";
//        while (true) {
//            str += str;
//        }
        // 不停扩大当前字符串
        StringBuilder stringBuilder = new StringBuilder();
        while (true) {
            stringBuilder.append("ssydx");
        }
    }
}

执行引擎

解释器

将字节码解释为机器码进行执行

编译器JIT

用于将热点字节码编译为机器码,便于直接执行,从而略过解释阶段,提高速度

这也是将Java称为半编译半解释语言的原因

垃圾回收器GC

分类

串行垃圾回收器(Serial Garbage Collector)

并行垃圾回收器(Parallel Garbage Collector)

CMS(Concurrent Mark Sweep)垃圾回收器

G1(Garbage First)垃圾回收器

四种垃圾回收器虽然依次进行了优化,但执行垃圾回收时仍不可避免的产生STW(stop the world,即中止主线程)

MonitorGC

轻GC,使用复制算法,仅处理新生代区域

FullGC

重GC,使用标记清理或标记压缩算法,处理整个堆

标记算法

引用计数法

对每个对象实例创建对应的计数器,每多一个引用则计数器加一,反之减一。计数器为0说明可回收

优点是实现简单且回收及时,缺点是无法处理循环引用(致命)且存在大量小对象时性能不佳(计数器对内存的占用占比升高且频繁创建销毁)

可达性分析

暂不做展开讨论

垃圾回收算法

标记复制

每执行一次都会把伊甸区和幸存区其一的幸存对象复制到另一幸存区 如果放不下或达到老年标准就会直接放到老年区

特点是内存利用率低,避免内存碎片化,执行效率高

标记清理

把未标记存活的对象直接清理掉

标记阶段:判断对象是否存活

复制阶段:将存活对象从伊甸区和幸存区from复制到幸存区to

交换阶段:交换幸存区的from和to角色

特点是内存利用率高,造成内存碎片化,执行效率中

标记压缩

把标记存活的对象移动到内存的一端,另一端全部清理

特点是内存利用率高,避免内存碎片化,执行效率低

对比

算法 内存碎片问题 回收执行效率 内存利用效率 适用场景
复制算法(Copying) 新生代(对象存活率低)
标记-清除(Mark-Sweep) 不在乎碎片的老年代
标记-压缩(Mark-Compact) 需要连续空间的老年代

实际通常采用分代回收机制

新生代通常存活率很低,适合复制算法,老生代则适合标记清理或标记压缩算法

本地方法接口

JNI(Java Native Interface) 是 Java 和本地代码之间的桥梁。当 Java 调用 native 方法时,JVM 会使用本地方法栈来执行这些方法,并通过 JNI 与本地代码交互。

虚拟机常见参数选项

参数类别 参数选项 描述
堆内存设置 -Xms<size> 设置JVM启动时的初始堆大小。例如,-Xms512m表示初始堆大小为512MB。
-Xmx<size> 设置JVM允许的最大堆大小。例如,-Xmx2g表示最大堆大小为2GB。
-Xmn<size> 设置年轻代(Young Generation)的大小。
-XX:NewRatio=<ratio> 设置年轻代与老年代的比例。例如,-XX:NewRatio=2表示年轻代占整个堆的1/3。
垃圾回收相关 -XX:+UseSerialGC 使用串行垃圾收集器。
-XX:+UseParallelGC 使用并行垃圾收集器(适用于多核处理器)。
-XX:+UseConcMarkSweepGC 启用CMS(Concurrent Mark Sweep)垃圾收集器,适合需要低延迟的应用。
-XX:+UseG1GC 使用G1(Garbage First)垃圾收集器,旨在提供可预测的暂停时间和高吞吐量。
-XX:MaxGCPauseMillis=<millis> 设置垃圾收集期间的目标最大暂停时间(仅对G1有效)。
性能调优 -XX:+AggressiveOpts 开启JVM的一些激进优化选项。
-XX:+UseStringDeduplication 在G1垃圾收集器中启用字符串去重以节省内存。
-XX:+DisableExplicitGC 禁用由应用程序显式调用System.gc()触发的垃圾收集。
调试与日志 -XX:+PrintGCDetails 打印详细的垃圾收集信息到控制台。
-XX:+PrintGCDateStamps 在打印GC日志时添加日期戳记。
-Xloggc:<file> 将GC日志输出到指定文件。
-XX:+HeapDumpOnOutOfMemoryError 当发生OutOfMemoryError时生成堆转储文件。
-XX:HeapDumpPath=<path> 指定堆转储文件的存储路径。
其他 -server-client 选择JVM运行模式,-server适用于长时间运行的服务端应用,而-client适用于桌面应用或快速启动场景。
性能调优 -XX:+PrintCommandLineFlags 打印实际使用的JVM参数
-XX:InitialCodeCacheSize / -XX:ReservedCodeCacheSize 设置JIT编译缓存大小
调试日志 -XX:+TraceClassLoading / -XX:+TraceClassUnloading 查看类加载/卸载过程
-XX:+PrintReferenceGC 显示软/弱/虚引用回收情况
GC 日志增强 -Xlog:gc*:time:file=logs/gc.log:time JDK9+ 新格式日志输出

借助-XX:+HeapDumpOnOutOfMemoryError可以输出堆溢出错误文件,通过Jprofiler、MAT等可以进行错误分析

JMM

Java 内存模型,本质是缓存一致性协议,解决线程共享的数据一致性

所有共享变量都存储在主内存中,每个线程在运行时都会从主内存拷贝所需的共享变量的副本到自己的工作内存(对共享变量的修改都是对副本的修改),假如一个线程修改了共享变量的副本但尚未写回主内存,此处其他线程再从主内存拷贝该变量就会造成数据不一致

共享变量指的是实例字段、静态字段、构成数组对象的元素等,不包括局部变量和方法参数(它们属于线程私有栈空间)

共享变量的操作流程

lock:作用于主内存中的变量,将他标记为一个线程独享变量

unlock:作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定

read:从主内存读取变量

load:将读取的变量放入工作内存(拷贝为变量副本)

use:使用变量副本

assgin:给变量副本赋值

store:将变量副本的值传回主内存

write:将变量副本的值写入主内存的变量中

上述步骤不是原子的,也不是顺序固定的,可能被重排序,除非用 volatile 或 synchronized 显式保证

volatile关键字可以保证共享变量的可见性,即对每次读取的都是主内存中的最新变量,并且变量副本的修改后会立即写回主内存,但无法保证原子性,例如:i++还是数据不一致,因为其本质是复合操作,不具备原子性;而flag=true是单步操作,不会造成数据不一致

synchronized关键字既可以保证共享变量的可见性,也可以保证其原子性

synchronized本质是对同步块或方法加互斥锁(一种悲观锁)

happends-before

happens-before 并不是时间上的先后顺序(即 A 不一定物理上先于 B 执行),而是指 逻辑上的因果顺序 —— A 的结果对 B 是可见的。

1. 程序顺序规则(Program Order Rule)

在一个线程内,按照代码顺序,前面的操作 happen-before 后面的操作。

int a = 1;        // 操作A
int b = a + 2;    // 操作B

操作 A happen-before 操作 B。

2. 锁定规则(Monitor Lock Rule)

一个线程对一个对象解锁(unlock)的操作 happen-before 另一个线程对该对象加锁(lock)的操作。

java深色版本

synchronized (obj) {
    // ...
}

线程A释放锁的操作 happen-before 线程B获取该锁的操作。

3. volatile变量规则(Volatile Variable Rule)

对一个 volatile 变量的写操作 happen-before 后续对该变量的读操作。

volatile boolean flag = false;

// 线程A
flag = true;

// 线程B
if (flag) { ... }

线程A的写 happen-before 线程B的读。

4. 线程启动规则(Thread Start Rule)

主线程调用 thread.start() 启动子线程 happen-before 子线程开始执行 run() 方法。

Thread t = new Thread(() -> {
    // run方法内容
});
t.start(); // 主线程的start() happen-before 子线程的run()

5. 线程终止规则(Thread Join Rule)

子线程中所有操作 happen-before 主线程从 join() 返回。

Thread t = new Thread(...);
t.start();
t.join(); // join返回后,主线程可以看到t线程中所有操作

6. 中断规则(Interrupt Rule)

一个线程调用 interrupt() 发生在被中断线程检测到中断(如抛出异常或调用 isInterrupted())之前。

t.interrupt(); // 主线程中断t
if (t.isInterrupted()) { ... } // t线程检查中断

主线程的 interrupt happen-before t线程检测到中断。

7. 终结器规则(Finalizer Rule)

一个对象的构造函数完成 happen-before 它的 finalize() 方法被执行。

8. 传递性规则(Transitivity)

如果 A happen-before B,B happen-before C,那么 A happen-before C。

简而言之

更早的代码先于更晚的代码执行

解锁先于加锁,volatile变量的写先于读,主线程先于子线程,中断先于检测

先于规则可传递

子线程插队后的所有操作对之后的线程可见?

#java##jvm#
计算机编程合集 文章被收录于专栏

本专栏包含Java、MySQL、JavaWeb、Spring、Redis、Docker等等,作为个人学习记录及知识总结,将长期进行更新! 如果浏览、点赞、收藏、订阅过少,考虑停更或删除了(😄有点打击积极性)

全部评论
mark
点赞 回复 分享
发布于 今天 14:20 北京

相关推荐

林北很感性:最慌的一次是误删测试环境数据,手抖着找运维大哥抢救。结果他说:“我入职时删过生产库,你这只是青铜局。” 瞬间释然 —— 只要别碰红线,大部分错误都是 成长必经路
投递菜鸟集团等公司6个岗位 担心入职之后被发现很菜怎么办
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务