章一 JVM基础【基于JDK8及以上】
本文是JVM系列的开篇,从JVM内存模型切入,系统梳理JVM相关的核心知识点。关于JVM的初级内容,如JVM简介与发展历史、架构概览、Java 字节码解析以及类加载机制等,
我们将在后续的详细介绍中逐步展开。本系列将从内存管理的角度深入解读JVM的工作原理,并逐步涵盖垃圾回收机制、性能调优、多线程与并发模型等内容。
<!-- TOC -->
jvm内存模型
jvm内存模型主要分为:方法区(元空间),堆内存、程序计数器、栈内存、本地方法栈、直接内存
各区域对比说明
区域 | 主要存储内容 | 内存分配位置 |
---|---|---|
程序计数器 | 当前线程执行的字节码指令地址 | jvm内部 |
方法区(元空间) | 方法元数据、jit编译后代码等 | 本地内存 |
堆 | 对象实例、静态变量、运行时常量池 | jvm堆内存 |
栈 | 每个线程的局部变量、操作数栈 | jvm栈内存 |
- 程序计数器:每个线程一个独立的程序计数器,用于存储当前线程执行的字节码地址,如果是native方法,计数器为空
- 方法区:方法区中存储类元数据(类名、修饰符、字段、方法描述等)、运行时常量池、方法字节码等数据
- 堆内存:应用程序创建的所有对象实例以及JIT编译过程生成的编译代码,是jvm中最大的一块内存区域,使用分代模型
栈内存:内存线程独享,基本类型的变量以及对象应用存在栈内存中
- 虚拟机栈:每个线程执行方法时的栈帧,每个栈帧包括局部变量表、操作数栈、动态链接、方法返回地址,方法的调用和结束就是栈帧入栈和出栈的过程
- 本地方法栈:为本地方法服务的栈帧,即通过JNI调用的c/c++代码
堆内存分配(经典分代收集器)
在不同的Gc中存在各自的内存分配规则,将在后续详细阐述
内存分配区域,主要分为新生代和老年代
新生代
- eden区:绝大多数对象优先分配到eden区
- survivor:从eden区晋升的对象复制到survivor区(分为to和from)
- 老年代:存放生命周期较长或较大的对象,通常是从survivor晋升的对象
- 堆外内存:某些场景下可以通过DirectByteBuffer或其他工具分配堆外内存
内存分配规则
- 优先分配到eden区,当eden区内存不足,触发Minor Gc,将eden区对象转移到survivor区,survivor区内存不足,对象移动到老年代
大对象,即需要连续内容空间的对象(大数组或大量字段的对象),直接分配到老年代
- 通过 -XX:PretenureSizeThreshold设置大对象的阈值,大于该值的对象直接分配到老年代
- 适用于G1之外的垃圾收集器,例如GMS
对象晋升
对象在survivor区经历一定次数的GC后,会晋升到老年代。标记次数可由参数 -XX:MatTenuringThreshold设置,默认值为15
- 每经过一次Minor Gc,年龄+1
大量分配
- 当新生代内存不足,jvm将对象直接分配到老年代,此时会增加老年代内存使用率,易触发Full GC
空间分配担保
- 在新生代发生Minor Gc前,jvm会检查老年代是否有足够空间,如果空间不足会触发Full Gc
- 在新生代发生Minor Gc前,jvm会检查老年代是否有足够空间,如果空间不足会触发Full Gc
- 相关JVM参数
参数 | 作用 |
---|---|
-Xms/-Xmx | 设置堆内存的初始大小和最大值 |
-XX:NewRatio | 设置新生代和老年代比例 |
-XX:SurvivorRation | 设置eden区和Survivor区比例 |
-XX:PretenureSizeThreshold | 设置大对象阈值 |
-XX:MaxTenuringThreshold | 设置对象晋升年龄阈值 |
-XX:+UseTLAB/-XX:TLABSize | 控制TLAB是否启用以及大小 |
类加载
jvm类加载机制是java虚拟机动态加载类的过程,主要包括类加载、连接、初始化三个阶段
加载
- 通过类的全限定名获取类的二进制字节,将字节流代表的静态存储结构转化成方法区的运行时数据结构
类加载器
- 启动类加载器:BootStrap ClassLoader,加载java.*包
- 扩展类加载器:Extension ClassLoader,加载lib/ext目录或java.ext.dirs系统属性指定目录中的类
- 应用程序加载器:Application ClassLoader,加载classPath中的类
- 自定义加载器:用户继承ClassLoader实现
连接
- 验证:确保字节码文件格式正确,保证jvm安全性
- 准备:为类的静态变量分配内存,并初始化为默认值
- 解析:将类、方法、字段的符号引用替换为直接引用,例如常量池中的符号引用解析为具体的内存地址
初始化
- 初始化阶段对类的静态变量和静态代码快执行显示初始化
- 执行类的 <clinit>方法,该方法有编译器生成,包含所有静态变量赋值和静态代码块
双亲委派机制
- 加载类时,先委派给父类加载器进行加载,当无父类加载器时,才由当前加载器尝试加载
- 该机制可以保证java的核心类库不会被自定义加载器替代,确保jvm的安全性和稳定性
惰性加载
- 类加载时按需进行,在使用时才加载
- 类的静态成员,只有在首次访问时才会触发加载
初始化过程
静态变量初始化,jvm在准备阶段为类的所有静态变量分配内存,并初始化默认值,在初始化阶段,会根据代码中指定的值重新赋值
public class MyClass { static int x = 10; // 静态变量赋值 }
准备阶段,x=0,初始化阶段,x=10
静态代码块只会在类加载时执行一次
public class MyClass { static { System.out.println("Static Block 1"); } static int x = 10; static { System.out.println("Static Block 2"); } }
输出结果:1、Static Block 1 2、初始化x=10 3、输出Static Block 2
父类初始化优先于子类
class Parent { static { System.out.println("Parent initialized"); } } class Child extends Parent { static { System.out.println("Child initialized"); } } public class Test { public static void main(String[] args) { Child child = new Child(); } }
输出:
Parent initialized
Child initialized- clinit方法:jvm会为类生成特殊的< clinit >方法,包含静态变量的复制操作和静态代码块的内容
逃逸分析
逃逸分析:逃逸分析是jvm的一种优化技术,用于确定一个对象的作用范围,判断该对象是否逃逸出某个特性的范围,如方法或线程,基于逃逸分析的结果,jvm可以对代码进行多种优化,比如栈上分配、标量替换、同步省略等
- 方法逃逸:如果一个对象被一个方法外的其他方法引用,那么该对象即发生逃逸
public Object createObject() {
return new Object(); // 对象返回给调用方,发生方法逃逸
}
- 线程逃逸
public void threadEscape() {
new Thread(() -> {
System.out.println(this); // 对象在另一个线程中被引用,发生线程逃逸
}).start();
}
逃逸分析作用
- 栈上分配:如果一个对象没有发生逃逸,那么对象可以直接分配在栈上,而不是堆上,可以避免垃圾回收的开销
- 锁消除:如果一个加锁的对象没有发生逃逸分析,即不会出现锁竞争情况,jvm可以安全的删除锁操作
- 标量替换:如果对象没有发生逃逸,并且它的内容可以被拆解为多个字段,则可以通过标量替换避免对象创建
如果启用逃逸分析,jvm中逃逸分析默认开启,相关jvm参数如下
- -XX:+DoEscapeAnalysis:开启逃逸分析
- -XX:+PrintEscapeAnalysis:打印逃逸分析的结果
- -XX:+EliminateLocks:启用锁消除
- -XX:+EliminateAllocations:启用分配优先
逃逸分析风险:当未发生逃逸分析的对象过大时,可能更容易触发栈溢出
如下代码,未发生逃逸分析,且需要分配较大内存,极易发生栈溢出
public class StackOverflowExample { public void recursiveMethod() { // 创建一个非常大的对象,但它没有逃逸出方法范围 int[] largeArray = new int[1024 * 1024]; // ~4MB的栈空间 recursiveMethod(); // 递归调用 } public static void main(String[] args) { StackOverflowExample example = new StackOverflowExample(); example.recursiveMethod(); } }
避免栈溢出
- 避免在方法中分配过大的局部对象,如果方法中必须分配大对象,需要设置方法返回值
- 递归方法中,设置结束条件,并且需要避免递归深度,最好不要超过5层
- 通过jvm参数设置合理栈大小,例如:-Xss2m
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。