JVM虚拟机
jvm是java的运行环境(java二进制字节码运行环境)
JVM虚拟机的组成
JVM(Java Virtual Machine)是Java程序运行的核心,主要由以下几个模块组成:
-
类加载子系统(ClassLoader Subsystem)
负责加载、验证、准备、解析和初始化类文件(.class文件),将字节码转换为JVM可执行的格式。 -
运行时数据区(Runtime Data Areas)
包括方法区(Method Area)、堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、程序计数器(Program Counter Register)。 -
执行引擎(Execution Engine)
负责解释或编译字节码为机器码并执行,包含解释器(Interpreter)、即时编译器(JIT Compiler)和垃圾回收器(Garbage Collector)。 -
本地方法接口(JNI, Java Native Interface)
提供调用本地方法(如C/C++)的接口。 -
本地方法库(Native Method Libraries)
存放本地方法的具体实现。
程序计数器
程序计数器是线程私有的(每个线程都有一份,没有线程安全问题)小块内存,记录当前线程执行的字节码指令地址。程序计数器是唯一不会抛出OutOfMemoryError(内存溢出)的内存区域,它的存在保证了线程切换后能恢复到正确的执行位置。
java堆
java堆是线程共享的(有线程安全问题),用于存放对象实例和数组。几乎所有对象的实例都在堆上分配内存。是垃圾收集器管理的主要区域。内存不够抛出OOM异常。
堆可以分为新生代(Young Generation)和老年代(Old Generation)。
新生代(Young Generation)进一步细分为Eden区和2个Survivor区。
新创建的对象通常会被分配在新生代的Eden区。Eden区是对象最初分配的区域,大多数对象生命周期较短,很快会被垃圾回收。
如果垃圾处理后还存活的对象会被移动到Survivor区(S0或S1)。
每次垃圾处理后,存活的对象的年龄(age)会增长,长到一定阈值就会被放到老年代。
老年代存放长期存活的对象。
元空间/方法区:存放类的结构信息,如类名、方法、字段、字节码、常量池等,取代了早期版本堆中的永久代,为了让堆节省内存空间,防止内存溢出(OOM)。
堆内存溢出报错:OutOfMemoryError
java虚拟机栈
先进后处。每个线程运行时所需要的内存就是java虚拟机栈,每个线程都有自己独立的虚拟机栈(线程私有的)。每个栈有多个栈帧组成(栈帧包含局部变量表、操作数栈、动态链接、方法返回地址等信息)。每个线程只有一个活动栈帧,一个方法调用对应一个栈帧。
栈内存溢出报错:StackOverflowError
方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
方法区/元空间
内存共享的,存放类的结构信息,如类名、方法、字段、字节码、常量池等,取代了早期版本堆中的永久代,为了让堆节省内存空间,防止内存溢出(OOM)。
元空间内存不足报错:OutOfMemoryError : Matespace
常量池
常量池(Constant Pool)是JVM方法区(元空间)中的一个重要组成部分,用于存储编译期生成的各种字面量和符号引用
查看字节码结构(类的基本信息,常量池,方法定义)
编译代码,在命令窗口执行:javap -v 字节码文件名
运行时常量池
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
类加载器
作用:就是将字节码文件加载到JVM中
JVM内置的类加载器类型
1. 启动类加载器(Bootstrap ClassLoader)
- 由C++实现(非Java类)
- 加载
<JAVA_HOME>/lib
下的核心类库 - 唯一没有父加载器的加载器
2. 扩展类加载器(Extension ClassLoader)
- 由
sun.misc.Launcher$ExtClassLoader
实现 - 加载
<JAVA_HOME>/lib/ext
目录的类 - 父加载器是Bootstrap
3. 应用程序类加载器(Application/System ClassLoader)
- 由
sun.misc.Launcher$AppClassLoader
实现 - 加载classpath指定的类
- 父加载器是Extension
4. 自定义类加载器
开发者可以继承java.lang.ClassLoader
创建自己的加载器,典型用途:
- 实现热部署
- 加载非标准来源的类
- 类隔离(如Tomcat的Web应用隔离)
类加载器双亲委派机制
加载某一个类,(自己先不加载)先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,(那就往下加载)子加载器尝试加载该类
JVM为什么使用类加载器双亲委派机制?
避免类重复加载
为了安全,保证类库API不会被修改。
类装载的步骤
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始从入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象
垃圾回收机制
垃圾回收主要针对堆中的对象。
对象什么时候被垃圾回收机制回收?
如果一个对象没有任何的引用和指向它,那它就是垃圾,它就可能被回收
如何定位垃圾
引用计数法:对象每被引用一次,引用次数+1,引用次数为0,代表可被回收。循环引用可能出现问题。
可达性分析算法(主要):扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收。
哪些对象可以作为GC Root?
垃圾回收算法
标记清除算法用的少,有内存碎片
下面俩算法用的多。
jvm
jvm常见调优参数
- 设置堆空间大小
- -Xms:2048m # 初始堆大小(默认物理内存1/64)
- -Xmx:2048m # 最大堆大小(默认物理内存1/4)
- 虚拟机栈的设置
- -Xss256k # 默认1MB(Linux/x64),Web应用建议256k-512k
- 年轻代中Eden区和两个Survivor区的大小比例
- -XX:SurvivorRatio=8 # 默认8表示 Eden:Survivor=8:1:1
- -XX:NewRatio=<ratio> # 老年代/年轻代的比例
- 年轻代晋升老年代阈值
- -XX:MaxTenuringThreshold=15 # 默认15(CMS下6)
- 设置垃圾回收收集器
jvm调优工具
命令行工具,JDK自带的:
- jsp:列出所有 Java 进程(PID 和主类名)
- jstack:生成线程快照(检查死锁、线程阻塞等问题)
- jmap:查看堆内存分布
- jstat:实时监控 GC 情况(堆内存、GC 次数/时间等),1000ms 刷新一次
可视化工具:
- JConsole:JDK 自带,基础监控(内存/线程/类加载/MBean),支持 JMX 连接。在JDK的bin目录下的exe文件
- VisualVM:JDK 自带,支持 CPU/内存监控、线程分析、堆转储分析、JMX 连接远程服务器
java内存泄露问题如如何排查
内存泄露主要指堆内存
- 堆内存溢出报错:OutOfMemoryError
报错原因:一些比较大的对象一直存活,一直没有被垃圾处理机制处理
- 栈内存溢出报错:StackOverflowError
报错原因:递归问题
- 元空间/方法区内存不足报错:OutOfMemoryError : Matespace
报错原因:动态加载的类太多了
运行闪退,运行一段时间就宕机如何排查
- 获取堆内存快照dump,使用jmap,只能在项目运行的时候用
- VisualVM去分析dump文件
- 通过查看堆信息的情况,定位内存溢出问题
CPU飙高的排查方案与思路
- 使用Linux的top命令查看整体CPU使用情况,找到占用高的进程,拿到进程ID。
- 获取进程中的线程:使用ps命令,找到进程中哪个线程占用高。将线程id10进制转16进制备用。
- 使用jstack [进程ID] 查看该线程。
jstack [进程ID]
命令会生成该Java进程内所有线程的详细日志,日志里的线程id是16进制的。 - 到jstack 日志中找到该线程(需要10进制转16进制),日志里应该会报错误原因。