JVM 全面详解:深入理解 Java 的核心运行机制
基础:
Jvm就是java虚拟机,
java执行流程:
文本文件(java)->编译器->.class文件->jvm->虚拟指令->执行引擎->解释为机器指令执行
编译的本质是将java源文件转为jvm能够认识的16进制class文件
类加载机制:
类加载的过程:
将.class文件转化为二进制文件加载到内存中,并转为可以运行的java.lang.Class对象,供jvm使用
1.1装载
(1)通过类加载器获取类的全限定类名,将class文件转为二进制文件
(2)将二进制流中类的描述信息存入方法区,如:创建时间
(3)将Java.lang.Class对象存入堆中
1.2链接
(1)验证加载类的正确性
(2)在方法区为静态变量分配空间,设置默认0值
(3)把类的符号引用转为直接引用
1.3初始化
为类的静态变量赋值,执行静态代码块
常见类加载器
BootstrapClassLoader(启动类加载器):加载核心类库(
rt.jar
、java.base
)。如:java.util.Date,java.lang.StringExtensionClassLoader(扩展类加载器):加载
jre/lib/ext
或-Djava.ext.dirs
指定目录下的类库。AppClassLoader(应用类加载器):加载
classpath
下的类。
双亲委派模型
类加载收到加载请求时,先由父类加载器去加载,父类加载器无法加载时,再由自己加载
BootstrapClassLoader<-ExtensionClassLoader<-AppClassLoader 依次继承
面试题:如何打破双亲委派机制?
自定义类加载器类,继承ClassLoader类,重写loaderClass方法
内存模型
什么是jvm内存模型 每启动一个java程序,jvm会在进程中划分一块内存区,jvm将内存区划分为5块
jvm内存划分
jvm按照线程是否共享将内存首先分成两大类
线程独享区
只有当前线程能访问数据的区域,线程之间不能共享 线程独享区随线程的创建而创建,随线程的销毁而回收
线程共享区
所有线程都可以访问的区域, 当线程被销毁的时候,共享区的数据不会立即回收,需要等待达到垃圾回收的阈值之后才进行回收
程序计数器
记录当前线程要执行指令的内存地址,为了多线程切换时,能恢复到正确执行位置 只记录一个地址,不会出现内存溢出的问题
虚拟机栈
存当前线程中声明的变量,包括基本数据类型的数据和引用数据类型的引用
基本数据类型:能够确认占用内存的大小
引用数据类型:不能够确认占用内存的大小
将值的引用存放到虚拟机栈中,而对象存放在堆内存中
栈帧
每一个线程会创建一个虚拟机栈,线程中的每个方法都会在虚拟机栈中创建一个栈帧,存放本次方法执行过程中所需要的所有数据,先进后出
虚拟机栈溢出及调优
栈溢出99%是因为递归,可以通过修改内存大小设置栈帧的最大深度
本地方法栈(了解,并非四大分区)
存储的是维护非java程序的执行过程中产生的数据
方法区(java8之后叫元空间)
方法区会存储类信息,静态变量,常量,机器指令
如果加载大量class文件,也会造成方法区内存溢出
jvm执行引擎
将字节码指令解释/编译为对应平台上的本地机器指令
解释器:将字节码文件解释为机器指令
即时编译器:会将字节码文件编译为机器指令,存在方法区中,编译完成后直接执行本地机器指令即可
混合模式:
解释器逐条执行字节码,记录热点方法 热点方法交给编译器编译成机器指令
堆内存模型
jvm将对象存放在堆内存,堆内存空间是比较大的。所以我们对于jvm调优,也是主要针对于堆内存调优,比如分配堆内存的空间
对象内存布局
对象头+实例数据+对齐填充()
jvm内存溢出(OOM)和垃圾回收(GC)机制
如果对象只创建不回收,则会造成内存溢出(OOM)
为什么进行堆内存划分?
1.垃圾回收后可以更好的利用内存空间,存放大对象 2.提高搜索垃圾的效率 3.尽可能减少GC次数
堆内存的划分
新生区:占1/3
又分为Eden区和两个Survivor区,且始终有一个Survivor区闲置。对象创建后会先放入Eden区,当Eden区满了之后会进行GC,之后将存活下来的对象复制到闲置的Survivor区,并清空Eden和正在使用的Survivor区。
老年区:占2/3
对象优先分配到新生区内存,每次GC没有回收的对象年龄+1,年龄到15还没有回收,则存放到老年区内存;如果对象较大,超过新生区内存的一半,对象也会放到老年区
YongGC和OldGC
OldGC:之后会进行补齐十分影响性能,尽可能减少
Survivor区空间并不大,满了怎么办?
会触发担保机制,提前将对象存入Old区
为什么需要Survivor区?
为了减少垃圾回收带来的空间碎片,空间碎片过多会频繁触发YongGC
为什么需要两块Survivor区?
为了减少Survivor区的kong
使用VisualVM监听java进程的内存模型
垃圾回收
如何判断垃圾?
引用计数法:
当有一个地方引用它时,计数 +1;引用失效时,计数 -1。
当计数为 0 时,说明对象不再被使用 → 可回收。
会出现循环引用的问题
可达性分析:
从一组 GC Roots 对象出发,沿着对象引用链不断向下搜索。
如果某个对象不能通过任何引用链连接到 GC Roots,就判定为“不可达对象”,就是垃圾。
GC Root对象:
栈帧中引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法栈中引用的对象
回收算法
1.标记-清除
标记所有可达对象,然后清除未标记的,内存碎片化严重
2.复制(新生代)
将内存分为两块,每次只使用一块,当满了时,将存活的对象复制到另一块,清除原有区域
3.标记-整理(老年代)
标记所有可达对象,然后清除未标记的,让存活对象移向一端,减少内存碎片
为什么新生代用复制算法,老年代用标记-整理?
新生代对象存活率低,复制成本小,老年代对象存活率高,复制成本太大,所以用标记-整理
垃圾收集器
CMS收集器
CMS是并发收集器,是基于标记-清除算法进行垃圾回收的,用于OldGC
并发收集,低停顿,但是有内存碎片,停顿时间不可控
G1收集器
并发收集器,能支持YongGC和OldGC
G1没有固定的Old,Yong,Eden,Survivor区,而是将内存分为大小相等的Region,每次回收后,Region的用途可以改变
可以根据开发者设置的参数,停顿时间的期望值,优先回收垃圾最多的Region
“CMS 和 G1 有什么区别?” 你可以这样答:
CMS 是一种 低延迟的并发收集器,采用标记-清除算法,适合中小堆内存、对响应时间敏感的应用,但存在内存碎片和浮动垃圾问题。 G1 是 JDK9 默认的收集器,采用 分区化的标记-整理算法,可以避免碎片,并且能根据用户设定的停顿时间优先回收垃圾最多的 Region,更适合大堆内存和对延迟可控要求高的场景。
jvm调优
参数类型
-X参数 -XX参数
1.防止OOM
2.垃圾回收调优
2.1提高吞吐量 2.2减少停顿时间
堆内存文件查看工具
给jvm配置以下代码,让项目发生OOM时自动下载堆内存信息
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
PerfMa查看生成的文件,可以看到各类对象实例数,实例的大小,使用的类加载器
GC日志查看工具
给jvm配置以下代码,将GC信息打印出来到gc.log文件
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log
使用GCViewer查看log文件的GC信息,可以看到吞吐量,平均停顿时间,GC次数
jvm监控工具
jvisualVM
能查看参数命令,堆,cpu,类的使用情况
G1GC调优
经过实践,Parallel,CMS,G1,得出G1的吞吐量大,平均停顿时间少,GC次数多
1.适当增加堆内存 2.设置停顿时间 ;设置停顿时间:-XX:MaxGCPauseMillis=
常见面试题补充
内存泄漏和内存溢出是一样的概念吗?
不一样,内存泄漏是指不再使用的对象无法被回收,一直占用内存空间 内存溢出是指程序运行的最大内存大于能提供的最大内存
方法区中的类会被回收吗?
有可能,1.该类的所有实例都被回收,2.该类的类加载器被回收了
CMS 和 G1 的区别
1.cms 只能适用于老年代,g1 可以用于老年代和新生代。
2.cms 使用了标记——清除算法,会产生大量碎片,g1 使用标记——整理算法,减 少了碎片的产生。
3.g1 更加灵活,它将内存分为了一块块 region,并且停顿时间可控。