深入浅出JVM内存结构:从面试高频考点到内存泄漏全解析

🌈 引言: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开发体验。

参考:黑马程序员

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值