🌈 引言:JVM(Java Virtual machine) 是执行Java程序的环境,将Java字节码转换为机器码,以便在计算机上运行,就像你的Java程序管家
- 跨平台管家:Java代码无需关心操作系统差异,JVM自动搞定环境适配,代码一次编写,到处运行
- 内存清洁工:自动垃圾回收机制,程序员无需手动管理内存 - 安全卫士:自动检查数组越界等危险操作
- 多态翻译官:支持面向对象特性,让代码更灵活
常见的JVM
调优参数设置注意:
- +代表使用这种规则,-代表不使用/关闭。
JVM内存结构
JVM内存结构全景图
JVM内存结构主要包括以下几部分:
1. 程序计数器:书签式导航
Program Counter Register,也叫寄存器
作用:记住下一条jvm指令的执行地址(就像读书时用的书签)
JAVA程序:JVM指令—>解释器(翻译)—>机器码—>CPU 执行指令
C/C++程序:直接编译成指定系统的机器码,针对不同的目标操作系统一般需要重新编译
所以,c/c++更适合系统级编程和需要直接访问底层硬件资源的任务,而Java更适合跨平台应用程序开发和具有良好可维护性的应用程序。
特点:
- 📍 线程私有(每个线程有自己独有的程序计数器)
- 💾不会存在内存溢出(内存占用极小)
2. 虚拟机栈:方法执行的流水线
虚拟机栈(Java Virtual Machine Stacks),每个线程运行时需要的内存空间
栈帧(Frame):每个方法运行时需要的内存(包含参数,局部变量,返回地址信息)
活动栈帧:对应着栈当前正在执行的那个方法
关系:每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存;每个线程只能有一个活动栈帧
经典三问❓:
1.栈需要垃圾回收吗?
:不需要,方法调用完毕后栈帧会自动出栈(就像吃完外卖自动收餐盒)
2.栈内存分配越大越好吗?
:错!默认-Xss1M(一般用默认),设太大会减少线程数量(就像会议室太大反而座位少)
3.方法内的局部变量是否线程安全?
:如果没有逃离方法的作用范围,它是线程安全的;如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。如 作为形参传递、作为返回值返回给调用者,这些情况下变量可能会被其他线程修改。(好比办公室文件不外带就安全)
2.2 常见问题:栈内存溢出
java.lang.StackOverflowError
- 栈帧过多导致内存溢出(如无限递归、类之间循环引用)
- 栈帧过大导致内存溢出(不常见)
2.3 线程运行诊断
案例1:cpu占用较多
定位(linux系统) 进程—>线程–>问题代码
1.用 top 定位哪个进程对cpu的占用高
2.ps H -eo pid, tid, %cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
3. jstack 线程id(可根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
案例二:程序运行久久等不到结果(可能是线程死锁)
定位(linux系统):根据 jstack 进程id 定位到问题线程
3. 本地方法栈:跨语言桥梁
java无法实现的跟操作系统直接打交道(Java为了实现跨平台性和安全性,对底层资源的访问做了安全限制)
native method,本地方法用关键字native修饰,Native 方法专用(c/c++底层实现的方法)
常见方法:Object的clone()方法、wait()方法、notify()方法、hashCode()方法
作用:实现Java无法直接操作的功能(如硬件访问)
**开发趣闻🗺️ **:Java早期通过本地方法实现线程管理,直到后来才用纯Java实现
4. 堆 :对象的大本营
堆 Heap:通过new关键字,创建的对象和数组都会使用堆内存
特点:
- 🌐线程共享(堆中对象都需要考虑线程安全的问题)
- 🗑️有垃圾回收机制(GC回收的主要区域)
4.2 堆内存溢出
java.lang.OutOfMemoryError:Java heap space
堆空间参数:-Xmx8M(VM options里配置为8M,默认为4G)
OutOfMemoryError案例:电商系统订单对象未及时回收,导致堆内存溢出
4.3 堆内存诊断
命令行 Terminal 窗口输命令
1.jps工具:查看当前系统中有哪些java进程
2.jmap工具( jmap -heap 进程id):查看堆内存占用情况
3.jconsole工具:图形界面的,多功能的监测工具,可以连续监测(选择当前进程–>不安全的连接)
案例:垃圾回收后,内存占用依然很高
jvisualvm工具(命令行Terminal窗口输命令):可视化方式展现虚拟机内容,堆 dump[堆转储] 抓取当前堆的内存快照(包含哪些对象,对象内存占用情况等信息)
5. 方法区:类的档案馆
- 方法区只是一个规范,1.8之前叫永久代,1.8之后叫元空间;
- 所有Java线程共享,存储相关类信息:包括成员方法、构造器等。
版本变迁
JDK版本 | 实现方式 | 参数设置 |
---|---|---|
≤1.7 | 永久代 | -XX:MaxPermSize |
≥1.8 | 元空间 | -XX:MaxMetaspaceSize |
5.2 组成
- JDK1.7/1.8区别:StringTable串池 from 方法区 to 堆
类加载器ClassLoader类(可以用来加载类的二进制字节码)
ClassWriter 作用是生成类的二进制字节码
5.3 方法区内存溢出
1.8 以前会导致永久代内存溢出
演示永久代内存溢出 -XX:MaxPermSize=8m
java.lang.OutOfMemoryError: PermGen space
- 1.8之后会导致元空间内存溢出
演示元空间内存溢出 -XX:MaxMetaspaceSize=8m
java.lang.outOfMemoryError: Metaspace
场景:spring、mybatis 等使用不当,会使用cglib动态加载类
5.4 运行时常量池
二进制字节码:包括类基本信息、常量池、类方法定义,包含虚拟机指令。
终端执行 javap -v xx.class 显示class文件反编译之后的详细信息
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址转变为真实地址。
StringTable[] : 串池,hashTable结构,不能扩容
5.5 StringTable串池
- 常量池的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是(StringBuilder) JDK1.8
- 字符串常量拼接的原理是编译器优化
- 可以使用**intern()**方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池。最后会把串池中的对象返回(当前对象指向为串池中的对象)
- 1.6 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则会把此对象复制一份放入串池,会把串池中的对象返回
5.6 StringTable位置:
永久代的常量池(1.6)—>堆空间(1.8)
位置调整原因:永久代回收效率低,fullgc才触发(要等老年代回收才回收),而串池在程序中占用内存多,容易发生内存不足
5.7 StringTable在内存不足时会触发垃圾回收
-XX:+UseGcOverheadLimit(默认使用) 花了98%时间进行垃圾回收,只释放了2%的堆内存,会放弃回收
-XX:+PrintStringTableStatistics 打印串池字符串实例信息
-XX:+PrintGCDetails
-verbose:gc 打印垃圾回收详细信息
8 StringTable性能调优
- 调整 -XX:StringTableSize=桶个数(桶个数越大,哈希碰撞概率越小,花费时间越少),默认60013。若系统中使用大量字符串,可以适当增加桶个数
- 考虑将字符串对象是否入池(存在大量字符串且有不少重复串,可考虑入池,减少重复字符串的反复存储)
6. 直接内存:高速公路旁的道
Direct Memory,常见于 NIO(Non-blocking IO)操作,用于数据缓冲区
特点:
- 分配回收成本较高,但读写性能高🚀
- 不受JVM内存回收管理 (操作系统内存),但也会发生内存溢出⚠️
6.1 直接内存-基本使用
6.2 直接内存-内存溢出
java.lang.outOfMemoryError: Direct buffer memory
-XX:+DisableExplicitGC 禁用显式垃圾回收,如System.gc(),写了会失效。该块内存只有在full gc时才会进行垃圾回收
6.3 分配和回收原理
- 使用了Unsafe对象完成 直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
📊 内存结构对比总结表
内存区域 | 存储内容 | 线程关系 | 溢出风险 | 调优参数 |
---|---|---|---|---|
程序计数器 | 指令地址 | 私有 | ❌ | 无 |
虚拟机栈 | 栈帧 | 私有 | StackOverflow | -Xss |
堆 | 对象实例 | 共享 | OOM | -Xms/-Xmx |
方法区 | 类元信息 | 共享 | OOM(Metaspace) | -XX:MaxMetaspaceSize |
直接内存 | NIO缓冲区 | 共享 | Direct OOM - | -XX:MaxDirectMemorySize |
💡 高频面试灵魂三问
方法区存放什么?与永久代什么关系?
StringTable为什么要从永久代移到堆?
直接内存如何避免内存泄漏?
理解JVM的内存结构对于Java开发者来说至关重要。掌握这些基本概念,可以帮助你更有效地管理内存,优化程序性能,并快速定位问题。通过合理使用JVM提供的工具与参数,提升你的Java开发体验。
参考:黑马程序员