【JavaSE】JVM
文章目录
- 定义
- 类加载器
- 类加载器
- 双亲委派机制
- 沙箱安全机制
- `native`关键字
- Native Method Stack
- PC寄存器
- 方法区
- 队列
- 栈
- 学习思路
- 功能、特点
- 栈+堆+方法区的关系:
- HotSpot和堆
- HotSpot
- 堆
- 新生区、永久区、堆内存调优
- 新生区
- 养老区(Old)
- 永久区(元空间)
- 永久区历程
- 什么情况永久区会出问题:
- 方法区
- 堆内存调优
- 查看堆内存
- 问题及解决方式
- JPofiler 分析OOM
- GC垃圾回收
- 概念
- 引用计数法
- 复制算法
- 标记清除算法
- 标记压缩算法
- 总结
- JMM
- 什么是JMM?
- `volatile`关键字
定义
JVM是java虚拟机,体现了java 的运行环境;
整体的一个流程是将java编译成为Class类,加载到类加载器Class Loader,之后在加载到JVM中
流程如下:
类加载器
类加载器
- 虚拟机自在的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序(系统)加载器
.getClassLoader
:查看加载器
DemoJVM01 demoJVM01 = new DemoJVM01();
DemoJVM01 demoJVM02 = new DemoJVM01();
DemoJVM01 demoJVM03 = new DemoJVM01();System.out.println(demoJVM03.hashCode());
System.out.println(demoJVM02.hashCode());
System.out.println(demoJVM01.hashCode());System.out.println("---end");Class<? extends DemoJVM01> aClass = demoJVM01.getClass();
System.out.println(" Class Loader:"+aClass.getClassLoader());
System.out.println(" Class Loader`s Parent:"+aClass.getClassLoader().getParent());
System.out.println(" Class Loader`s Parent`s Parent:"+aClass.getClassLoader().getParent().getParent()); // rt.jar
Class Loader:sun.misc.Launcher$AppClassLoader@18b4aac2Class Loader`s Parent:sun.misc.Launcher$ExtClassLoader@3d04a311Class Loader`s Parent`s Parent:null
双亲委派机制
如果在jar包中如果有一模一样名字的方法的情况,新定义的内容不会生效,这称之为双亲委派机制
app >> ext >> rt (Boot)
双亲委派机制:
- 类加载器收到类加载的请求;
- 将这个请求向上委托 给父类加载器去完成,一直向上委托,直至启动类加载器;
- 启动加载器检查 是否能够 加载当前这个类,能加载就结束,使用当前的加载器,否则 抛出异常,通知子加载器进行加载;
- 重复步骤3 直至 Class Not Found;
沙箱安全机制
沙箱机制 (sandbox),将java代码限定到虚拟机(JVM)之中,并且严格限制代码对本地系统的资源访问。通过这样的措施来保证代码的有效隔离。
native
关键字
private native void start0();
,start()调用了start0()方法,使用了native
关键字,使用调用了C语言的库,java的功能已经实现不了了。
JNI的作用:扩展Java的使用,融合不同的语言 给Java使用;
- C+±- → Java
- C++++ → C#
使用Java驱动本地东西或者调用计算机的时候native 会出现,但大多时候都是使用C++去实现;
Native Method Stack
PC寄存器
程序计数器:Program Counter Register
每一个线程都有一个程序计数器,是线程所私有的,就是一个指针,指向方法区的方法字节码(用于存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间;
方法区
Method Area, 方法区 是被所有线程共享,所有字段和方法字节码,以及一些特殊的方法:接口,构造函数;
简单的说,所有定义的方法的信息都会保存在该区域,此区间属于共享区间;
静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是 实例变量存在堆内存中, 和方法区无关;
static、final、Class、常量池;
队列
类似于管道流程,
线程的执行就是队列,新进先出(FIFO)原则,
栈
学习思路
程序=数据结构+算法;
程序=框架+业务逻辑~:淘汰!
类似于桶的样态;先进后出(FILO)原则,
main方法会放在最底下,执行的方法在上面执行之后会将执行过的弹出;
如果空间设置不对,会出先栈溢出异常;递归自调会出现这种状态;
递归自调用如果没有终止递归的方法就会出现栈溢出;
存放的是进行引用的地方;
功能、特点
栈内存,主管程序的运行,生命周期和线程同步;
线程结束,占内存就会释放,对于栈来说,不存在垃圾回收问题;当线程结束,站就会结束;
栈中保存着 8大基本类型+对象引用+实例的方法;
栈的运行原理:通过栈帧实现;
栈满了 会出先StackOverflowError
栈溢出错误,
栈+堆+方法区的关系:
HotSpot和堆
HotSpot
查看版本 java -version
C:\Users\杨>java -version
openjdk version "17.0.16" 2025-07-15
OpenJDK Runtime Environment Temurin-17.0.16+8 (build 17.0.16+8)
OpenJDK 64-Bit Server VM Temurin-17.0.16+8 (build 17.0.16+8, mixed mode, sharing)C:\Users\杨>
已经没有HotSpot了;
堆
堆(Heap),真正放置实例内容的地方 就是堆;
类加载器 读取了类的文件后,_常量,类,方法 ,变量 _等都会保存到堆中;
堆内存细分为三个区域
- 新生区:young
- 养老区:old
- 永久区:Perm
GC(垃圾回收)有轻量级和重量级两种GC机制:
- 轻量级的GC在新生区,在堆容量特别大的情况下会 进行重量级GC;
- GC的垃圾回收,主要在伊甸园区和养老区;
- 如果内存特别大的时候,会出现OOM(OutOfMemoryError),堆内存溢出;
- jdk1.8之后,永久存储区的名字 >> (源空间)
OOM的出现的场景,一直往新生区的加对象 直至加到上限,就会出现这个问题;
动态的不断生成反射类,一直加载直到内存满都会出现OOM;
新生区、永久区、堆内存调优
jdk1.8之后 元空间 和 JVM 分离了;
新生区
存储新创建的对象,大部分对象生命周期较短,通过垃圾回收(GC)快速清理无用对象。
分为伊甸园(Eden区)、幸存者0区(Survivor0)和幸存者1区(Survivor1)。
对象在Eden区创建,经历GC后存活的对象被移至幸存区,多次存活后晋升至老年代(Old Generation)。
养老区(Old)
在新生区的对象 经过伊甸园区,之后到幸存0区,幸存1区,当新生区全都满了之后,回去执行一次重GC,之后还能存在的情况才会到达养老区;
直至最后到达养老区满了之后会到达顶峰。在增加会出先OOM;
99%的对象到不了永久区,都是临时对象,大部分对象都是临时对象;
永久区(元空间)
永久区 所占用区域是常驻内存中,用来存放jdk携带的Class对象,interface元数据,存储的是Java运行的一些环境;
这个区域不会存在GC,在我们关闭虚拟机的时候,会去释放这个区域的内存;
永久区历程
- jdk1.6之前:名字称之为永久代,常量池是在方法区之中;
- jdk1.7:永久代慢慢退化了,将常量池移动到堆中;
- jdk1.8:彻底去永久代,常量池到元空间中,持久代已经变成元空间了;
存储类元数据(如类定义、常量池)、静态变量等,属于方法区的替代实现。
什么情况永久区会出问题:
- 启动类启动时加载了大量的外部jar;
- tomcat在启动时部署了太多的应用;
- 在程序执行时,编写了大量的反射类,不断的被加载,直到内存满,出现OOM;
方法区
方法区是与所有的内存区共享的,但是存在在堆之中,又称之为非堆;
方法区这个存在在元空间之中,在逻辑上我们理解是有的,但是物理上是不占用的;
堆内存调优
查看堆内存
package Jbm;public class DemoJVM01 {public static void main(String[] args) {// 获取JVM内存信息long maxMemory = Runtime.getRuntime().maxMemory();// 获取JVM初始化总内存long totalMemory = Runtime.getRuntime().totalMemory();System.out.println("maxMemory"+maxMemory+"Byte\t"+(maxMemory/1024/1024)+"MB");System.out.println("---");System.out.println("totalMemory"+totalMemory+"Byte\t"+(totalMemory/1024/1024)+"MB");System.out.println("---end");}
}
maxMemory3736076288Byte 3563MB
---
totalMemory253231104Byte 241MB
---end
默认分配的总内存是电脑内存的1/4,初始化的内存是1/64
可以手动配置堆内存扩大;
问题及解决方式
:::info
有碰到过OOM(堆内存溢出)吗?
- 配置内存,扩大内存看下显示是否恢复;
- 分析内存,查看代码的那个地方出现了问题;
:::
JPofiler 分析OOM
出现OOM怎么办,如何排除?
- 使用内存分析工具,看第几行代码出错:MAT,Jpofiler;
- debug,查看代码出错的地方;
MAT、Jprofiler的作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据;
- 获得最大的对象;
GC垃圾回收
概念
JVM在进行GC时,并不是对三个区域统一回收,大部分时候,回收都是新生代
- 新生代;
- 幸存区:0,1;
- 老年区
GC的两种:
- 轻GC :普通GC
- 重GC:全局GC
GC的题目
- JVM的内存模型和分区~ 详细到每个区放什么?
- 堆里面的 分区有哪些?伊甸园区 幸存区 form to,老年区,说说他们的特点!
- GC算法有哪些?标记清除法,标记压缩法没复制算法,引用计数器,具体怎么用的?
- 轻GC和重GC分别是什么时候发生?
引用计数法
将内存中的地址进行标注,标注如果又被引用的继续保留,如果有的地址被引用为0,则进行垃圾回收
复制算法
使用幸存区 0区和1区进行GC;
幸存区的form和to的判断标准:【谁空谁是to】;
流程:
- 每次GC之后,都会将伊甸园区的对象移动到幸存区;
- 在伊甸园区清空from区,变成to区,
- from区和to区来回切换,将两个区互相复制,期间为保持一个干净的空间,from会不断清空切换为to;
- 当GC次数到达一定的次数之后,会将对象复制到养老区;
可以使用 -XX: -XX:MaxTenuringThreshoid=9999 调整GC的任期时间,JVM调优的方式;
伊甸园区和幸存from区有内容,移动到To区,之后To是满的,变成From区;
From区将所有的东西copy到to区后,From变成了To;
- 好处:没有内存碎片
- 坏处:浪费了内存空间,多了一一般的空间被使用;
- 在对象存活度较低的情况下,使用复制算法最佳!
标记清除算法
对 用过的对象做个标记;
扫描所有的对象,对对象进行标记;
之后清除 所有的没有标记的对象,进行清除;
- 优点:不需要额外的空间;
- 缺点:两次扫描,严重浪费时间,会产生内存碎片;
标记压缩算法
使用压缩,对内存空间再次扫描,向一段 移动存活的对象。
多了一层移动的成本
标记清除算法+标记压缩算法=标记清除压缩算法。
总结
- 内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记压缩算法 > 标记清除算法
- 内存利用率:标记压缩算法=标记清除算法 > 复制算法
最优的算法?
只有最合适的算法,没有最好的算法; —> 分代收集算法
年轻代:
- 存活率底
- 复制算法√
老年代:
- 区域大,存活率高
- 标记清除(内存碎片不是很多)+标记压缩算法
JMM
什么是JMM?
JMM(Java Mermory Model)java内存模型,是java的缓存一致性协议,用于定义读写的规则。
JMM定义了线程的工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,期间的每一个线程都是有个自己的本地内存(Local Memory)。
在JVM中只有一个主内存,线程执行后 会copy出来自己的一个内存区域,这是自己的工作区域;
之后数据会变得不一样,为了解决 数据不一致,共享内存的内容可见性的问题,出现了volatile
关键字;
volatile
关键字
volatile
是Java中保证多线程可见性和禁止指令重排序的关键字,主要用于解决共享变量的内存同步问题。
JMM是一种抽象的观念;