介绍
Java
虚拟机,是Java
程序运行的核心环境- 一次编译,到处运行
特点
- 跨平台运行
- 负责将字节码文件(
.class
)转换为机器码并执行
- 负责将字节码文件(
- 内存管理
- 自动分配和回收内存,避免开发者直接操作内存
- 垃圾回收(
GC
)- 自动回收不再使用的对象,防止内存x泄漏
- 安全隔离
- 通过沙箱机制限制程序对系统的直接访问
参数
参数名 | 描述 |
---|---|
-Xms<size> |
设置堆初始大小 |
-Xmx<size> |
设置堆最大大小 |
-Xmn<size> |
设置新生代大小 |
-XX:+Use<GC> |
设置使用的垃圾回收器 |
-XX:MetaspaceSize<size> |
设置元空间初始大小 |
-XX:MaxMetaspaceSize<size> |
设置元空间最大大小 |
-XX:MaxTenuringThreshoId=<size> |
设置Survivor 区对象晋升条件 |
-XX:NewRatio=<size> |
设置新生代与老年代内存比例 |
-XX:SurvivorRadio=<size> |
设置Eden 区与Survivor 区内存比例 |
架构
Class Loader
- 类加载器,将类文件通过类加载器加载到内存中,负责加载程序中的所有类
类加载
- Loading
- 类加载器根据类的全限定类名查找类文件,并将其加载到内存中,转换成一个
Class
对象
- 类加载器根据类的全限定类名查找类文件,并将其加载到内存中,转换成一个
- Verification
- 对加载的字节码进行验证,确保符合
JVM
的规范
- 对加载的字节码进行验证,确保符合
- Preparation
- 为类的静态变量分配内存,并赋予默认初始值
- Resolution
- 将常量池中的符号引用解析为(类,方法,字段等)解析为直接引用,解决类之间的依赖关系
- Initialization
- 执行类的静态初始化(静态字段的赋值,静态代码块的执行)`
类型
Bootstrap ClassLoader
- 加载核心类库
rt.jar
中的类,如java.lang
,java.util
- 由
JVM
提供,C
与C++
编写,属于非Java
类加载器 - 加载的类位于
JVM
的lib
目录下,如jre/lib/rt.jar
Extension ClassLoader
- 扩展类加载器,加载
JDK
扩展目录下的类,如jre/lib/ext
- 由
JVM
提供,继承自ClassLoader
- 加载的类位于
jre/lib/ext
目录或由java.ext.dirs
系统属性指定的目录
System ClassLoader
- 应用程序类加载器,加载应用程序类路径(
CLASSPATH
)下的类 - 最常用的类加载器,通常由应用程序的运行环境提供
Custom ClassLoader
- *自定义类加载器,开发者可以自定义类加载器,加载自定义的类文件或特定位置的类文件
- 继承
ClassLoader
类,实现findClass
方法 - 适用于Web容器,应用服务器,插件框架等需要动态加载类的场景
双亲委派模型
结构
- 当一个类加载器请求加载一个类时,他会先查询自己是否已经加载这个类
- 如果当前加载器未加载该类,它会将请求委托给父加载器去加载
- 父加载器继续向上委托,直到
BootStrap ClassLoader
- 如果父加载器无法加载该类,才由当前加载器加载
特点
- 防止重复加载,通过双亲委托,确保同一个类只加载一次
- 隔离性,不同类加载器之间可以隔离,避免不同的类加载器加载重名的类产生冲突
- 安全性,确保核心类库不会被第三方类加载器篡改
Runtime Data Areas
- 运行时数据区,
JVM
在执行程序时会使用的内存区域
Method Area
特点
- 方法区,存储类和方法相关的元数据
- JDK7及之前,方法区的实现是永久代
- JDK8开始,方法去的实现是元空间
组成
- 类元数据
- 每个类的结构信息(类名,父类名,实现的接口,字段,方法,构造函数等)
- 类加载时,
JVM
会将类的结构信息加载到方法区
- 常量池
- 每个类的字节码文件都包含一个常量池
- 存储类使用的所有常量,符号引用等
- 符号引用就是通过类的全限定名,字段名或方法名标识一个类,字段或方法,以字符串的形式存储在字节码文件中
- 静态变量
- JVM运行时常量池
JVM
为了提高性能而在运行时维护的常量池- 将类文件中的常量池加载到内存中,并在运行时可能会动态增加常量(字符串池)
管理
- 类卸载
- 当一个类不再被使用,
JVM
会卸载该类,并释放该类在方法区占用的内存
- 当一个类不再被使用,
- PermGen
JDK7
及以前,方法区的实现是通过永久代来管理的- 空间固定,使用堆内存,
JVM
启动时分配,空间不足可能导致OutOfMemoryError
- Metaspace
JDK8
引入的元空间,取代了永久代- 空间在本地内存中分配,大小不受
JVM
堆大小的限制,可以动态调整
堆(Heap)
- 存储所有
new
创建的Java
对象实例与数组
组成
- 新生代
- 新创建的对象存储在
Eden
区 Eden
区经过垃圾回收仍然存活的对象,存储在Survivor
区Survivor
区分为(S0)源区与目标区(S1),源区存储Eden
区存活的对象,目标区存储源区存活的对象
- 新创建的对象存储在
- 老年代
- 存储生命周期较长的对象
Survivor
区经过多次垃圾回收仍然存活的对象,存储在老年代- 当老年代内存不足时,
JVM
会触发Full GC
,清理老年代的无用对象,释放内存
- 永久代
- 已于JDK8移除
管理
- 新创建的对象首先分配到
Eden
区 Eden
填满后,垃圾回收器会进行一次Minor GC
,回收Eden
区与Survivor
区的垃圾对象Eden
区经过垃圾回收存活的对象,移动到Survivor
的S0
S0
作为源区,存放Eden
区回收过来的存活对象S1
作为目标区,初始状态为空,在进行Minor GC
之后,存储源区存活的对象GC
后,S0
存活对象移动到S1
,变为空区,S1
接收源区存活的对象S0
变为目标区,接收下次GC
存活的对象,S1
变为源区,存储上一次GC
存活的对象- 存活对象在
S0
与S1
之间不断移动,直到年龄达到阈值,晋升到老年代 Survivor
区空间不足时,会将一些对象直接晋升到老年代- 如果一个对象太大,无法在新生代中容纳,会直接分配到老年代
虚拟机栈(JVM Stack)
特点
- 存储方法的局部变量,操作数栈,动态链接,方法返回地址等
- 每执行一个方法会创建一个栈帧(
Stack Frame
),方法执行完毕栈帧会弹出并销毁 - 栈线程是私有的,每个线程都有一个独立的栈
- 当方法的调用深度过大(递归),超过了虚拟机栈的最大深度,就会导致栈溢出
生命周期
- 方法调用
- 当一个方法被调用时.JVM为该方法创建一个新的栈帧并将其压入虚拟机栈
- 栈帧中的局部变量表和操作数栈会根据方法的参数和执行逻辑进行初始化
- 方法执行
- 方法执行过程中,操作数栈用于执行字节码指令,局部变量表存储参数和局部变量,方法的每一条指令都会操作这些数据
- 方法返回
- 方法执行完毕,栈帧弹出并销毁,方法返回的结果从栈帧中取出,栈指针指向上一个栈帧
组成
- 局部变量表(Local Variables)
- 存储方法参数和局部变量
- 局部变量表中的变量在方法执行过程中是按顺序存储的
- 操作数栈(Operand Stack)
- 用于存储方法执行过程中计算的操作数
- 操作数栈的大小和内容在方法执行过程中动态变化
- 例如
a + b
有两个操作数a
和b
- 先将
a
和b
压入操作数栈中 - 执行加法运算,从操作数栈中弹出
b
和a
并进行加法运算 - 计算并将结果
8
压入操作数栈 - 最后,JVM会将操作数栈栈顶的
8
取出,存储到局部变量表中
- 先将
- 动态链接(Dynamic Linking)
- 用于支持JVM中的符号引用
- 在程序运行时,解析和连接外部方法
- 返回地址
- 存储方法调用返回时的跳转地址
本地方法栈(Native Method Stack)
- 用于支持本地方法
- 结构组成与虚拟机栈相同,区别在于存储的是本地方法的信息
程序计数器(PC Register)
- 存储当前线程所执行的字节码指令的地址
- 每执行一条字节码指令,程序计数器都会更新,指向下一个需要执行的指令
- 方法执行完毕,程序计数器会指向调用该方法之前的位置
- 每个线程拥有独立的程序计数器,切换线程时,程序计数器也会切换
执行引擎(Execution Engine)
字节码解释器
- 解释并执行字节码指令
即时编译器
- 动态的将字节码编译为本地机器码
垃圾回收(GC)
- 回收不在使用的对象,释放内存资源
- 可以使用
System.gc()
方法手动调用
分类
- Minor GC
- 新生代回收
- 速度快,因为新生代中存活对象较少
- Full GC
- 老年代回收
- 回收整个堆内存
- 速度慢,因为老年代内存空间较大
算法
- 标记-清除算法(Mark-Sweep)
- 从根对象开始,递归标记所有被引用的对象,回收未被标记的对象
- 优点: 简单直观,回收过程不需要对内存进行额外处理
- 缺点: 存在内存碎片问题,即有大量不连续的内存空间,导致无法有效使用内存
- 复制算法(Copying)
- 将内存平均分为两部分,每次只使用其中一部分,当这一部分内存满时,复制存活对象到另一个部分,然后将之前一部分清空,
- 优点: 避免了内存碎片问题,
- 缺点: 内存缩小为原来的一半,浪费了一半的内存空间,所以适用于新生代
- 标记整理算法(Mark-Compact)
- 从根对象开始,递归标记所有被引用的对象,将存活的对象移动到内存的另一端,清除空闲的内存空间
- 优点: 避免了内存碎片的问题,同时避免了浪费内存空间的问题
- 缺点: 存活对象过多,大量的复制操作,导致算法效率降低
- 分代收集算法(Generational Collection)
- 实际上就是复制算法与标记整理算法的结合,将堆分为新生代与老年代
- 新生代空间相比老年代较少,对象存活率低,使用复制算法效率是最高的
- 老年代空间大,对象存活率高,大量高存活率的对象,复制算法明显不合适,一般是由标记清楚和标记整理算法混合实现
回收器
- Serial GC(串行垃圾回收器)
- 只有一个线程执行垃圾回收,效率低
- 适合于单核CPU或内存小的场景
- Parallel GC(并行垃圾回收器)
- 采用多个线程并行处理垃圾回收任务,效率较高
- 适用于多核CPU环境
- CMS GC(并发标记清楚垃圾回收器)
- 进行垃圾回收时,尽量减少停顿时间,CMS回收器并发执行标记和清理阶段,从而减少停顿
- 适用于对低停顿时间有要搞要求的应用
- G1 GC(Garbage First 垃圾回收器)
- 将堆划分为多个区域,并行,并发的进行垃圾回收,通过分区的方式,G1可以在不同的区域执行垃圾回收任务
- 适用于大内存,对停顿时间敏感的应用
本地接口(JVM Native Interface)
- 实现
Java
调用其他编程语言编写的函数,同时允许本地代码调用Java
代码