导图社区 JVM
JVM完整体系(JDK8)知识梳理,一张图带你了解JVM内存模型、对象生命、垃圾回收算法、垃圾回收器的相关内容~
编辑于2023-02-17 13:02:53 浙江省JVM
内存模型
堆(heap)
绝大多数的对象分配在该内存区域,栈上分配等例外
用-Xms指定初始内存大小,用-Xmx指定最大内存大小
方法区(Method Area)
存放类的一些元信息,方法属性等数据
Hotspot虚拟机1.7之前用永久代实现
在JDK1.7之前还包括常量池区域,1.7之后移到堆内存
在JDK1.8的时候,移除永久代,用MetaSpace代替,主要存放是JDK1.7之后剩余的类元信息等
在JDK1.8中,用-XX:MetaspaceSize指定初始容量大小,用-XX:MaxMetaspaceSize指定最大容量,如果不指定,则默认和Xmx指定的容量大小一致
JVM 栈(jvm stacks)
执行Java方法所需要的内存区域,存放运行时,栈帧数据(局部变量表,操作数栈,动态链接,方法出口等等)
是线程独有的区域,不同的线程都需要不同的栈区域
用-Xss指定栈的大小,该参数越大,则每个线程占用内存增加,同样的系统内存下,线程数更少,当然每个线程的调用深度越深,如果该参数越小,则反之
本地方法栈(native method stacks)
执行非Java代码时所需要的栈空间,理解几乎和栈一致
在Hostpot中,几乎和栈一致,没有做实际的内存区分
用-Xoss指定,实际并无作用
程序计数器
线程独有,主要用来该线程代码运行的指令,利于线程切换
在执行Java方法的时候,则存储所代表的指令,执行本地方法的时候为null
运行时常量池
Hostpot中,JDK1.7之前属于永久代中的内存区域,存放字符串常量,符号引用等一些常量数据
在JDK1.7之后则是堆内存中的一部分区域
直接内存
在JDK1.4之后,使用了nio(DirectByteBuffer),为提升读写能力,则可以直接使用堆外内存,则使用的直接内存进行处理
通过-XX:MaxDirectMemorySize进行指定最大容量,不指定则是物理内存大小
只是Java程序员容易忽略的内存区域
对象生命
对象创建
创建方式
通过new关键字
通过反射,反序列化
对象克隆
new对象
通过new关键字,触发对应的指令,该指令会去找对应的符号引用的类,如果该类已经加载,则通过对象的大小进行内存分配
如果没有加载该类 ,则执行类加载流程,进行类的加载,详情类加载流程
在内存分配的过程中,会存在线程安全问题,可以使用TLAB和同步方式解决
内存分配完成后,则需写入对象的引用,引用的方式也有多种,第一种采用直接引用,第二种则采用句柄引用
直接引用: 访问链路短,但是移动对象,需要移动更多的引用(Hostpot采用的是这种方式)
句柄引用:访问链路加长,移动对象方便,只用改变句柄中的引用
对象分配
在客户端模式下,基于serial + serial old收集器的情况下的对象分配策略,不同的收集器会有些不一样。生产环境也不会这样配置
对象创建后,大多数都是分配的Eden区,经过一次回收后会进入Survivor区
如果对象太大,可能直接进入老年代,Survivor区放不下Eden区回收后的对象,也可能进入老年代
如果对象年龄达到默认15,进入老年代,或者超过动态年龄也会进入老年代
内存布局
主要是对象头,对象实例本身,数据对齐填充
对象死亡
一个对象,不存在任何引用(被死亡的对象引用除外),则该对象已经死亡,可以被回收
死亡判断
引用计数,引用+1,失去引用-1,最终为0则死亡,不能解决循环引用问题
可达性分析法,通过GCroots(静态引用或运行栈中局部变量等等)遍历,不被遍历到则被回收,Hostpot采用的是这种方式
对象引用
强引用:最常规的引用,就编码时的指向
软引用:内存不足的时候,才会被回收
弱引用:下次垃圾回收的时候就会被回收
虚引用:无法通过引用获取对象,只是为了回收通知
垃圾回收算法
算法说明
主要就是如何找到垃圾,如果将垃圾回收掉
引用计数
对象被引用+1,失去引用-1,为0则为垃圾,如何清除并未提到
简单,但是问题多,循环引用不能解决,很容易出现内存泄漏
标记清除
标记算法:通过GCroots遍历,如果不被遍历到的,则是垃圾对象
清理算法:是垃圾,直接清除掉,缺陷,内存碎片严重
标记整理
标记算法:同标记清除
清理算法:清除后,将对象压缩到内存的一侧,所以也叫标记压缩,缺陷,如果较多对象需要移动,耗CPU,停顿时间长
复制算法
标记算法:同标记清除
清理算法:清除后,将对象移动到另外一块内存,缺陷,存活对象较多,复制耗时,另外内存有浪费
新生代使用serial,老年代可以选择seial old 或者cms 回收器
分区,分代
在上述算法中,采用分区分代思想,进行不同的区域进行不同的回收
常见:老年代,新生代;老年代使用标记清除,标记整理算法,新生代使用复制算法,因为新生代的对象大多都是朝生夕亡
并发
线程实现
jvm中的线程,最终都会被映射为内核线程,也就是严格意义上的线程
每个线程都有独立的栈,堆为线程共享区域,在并发访问堆中数据时,存在一定的安全性
线程安全
在多线程环境下,不考虑任何线程调度和交替执行的情况下,对象还能被安全的使用,则认为这个对象是线程安全的
不可变:如果一个对象创建后,任何线程都不能修改它,则该对象一定是线程安全的
绝对线程安全:条件极为苛刻,比如线程安全的容器,复合操作可能出现线程安全问题
相对线程安全:单次或简单操作下的线程安全,通常就是我们常说的线程安全
线程兼容:对象本身不安全,但是可以通过手段让其安全
线程对立:无法通过同步保证线程安全,通常是因为无法确定调用顺序导致
JVM锁优化
jvm在实现的时候,对锁的优化思路,值得学习
自旋锁和自适应自旋:在获取锁失败的时候,并非立马挂起当前线程,而是通过循环一定次数尝试获取,自适应自旋则是次数非固定,根据锁状态自行判断
锁消除:根据逃逸分析技术,如果一个对象不可能被多线程共享使用,则自动消除该对象的同步锁
锁粗化:如果一个对象完成多次操作,要反复加锁释放锁,则可以通过一次获取锁完成所有的业务
偏向锁:在对象头上加标志位,如果对象是偏向锁,持有锁的线程进入就不需要同步
轻量级锁:偏向锁如果被破坏,则升级为轻量级锁,通过cas获取轻量级锁,获取不到则升级为重量级
垃圾回收器
串行回收器(serial)
单线程垃圾回收器,回收时STW,一个GC线程,多半用于客户端
新生代使用serial,老年代可以选择seial old 或者cms 回收器
serial old
serial收集器的老年代版本
cms回收失败时的备用选择
ParNew收集器
serial的多线程版本,其他均一致
老年代可以使用 serial old 和 cms回收器
在JDK1.9之后逐渐退出历史,因配合使用的cms回收器不再推荐使用
Parallel Scavenge收集器
也是并行收集器,区别在于更关注吞吐量
老年代可使用 serial old 或 parallel old 收集器
parallel old
parallel scavenge的老年代版本,并非同时推出
在jdk1.5之前还是使用serial old
CMS 收集器
专注老年代的回收器,目的是减少STW
将垃圾回收分为四个阶段,初始标记,并发标记,重新标记,并发清除
并发标记和并发清除和用户线程同时执行,可以减少一定的STW
G1回收器
JDK1.7之后的分区,不分代收集器
对内存分区,每个区可以是新生代,老年代的区域
根据不同的区域情况,可以预估回收时间,以达到最大性价比的回收方案,让回收时间在一定的情况下可控
Shenandoah收集器
OpenJDK12之后的低延时收集器,能够在任何堆内存下使得停顿时间不大于10ms
相比G1,支持并发整理,不采用分代收集,删除记忆集
ZGC收集器
18年提交给OpenJDK11的实验收集器
深入G1回收器
因为G1回收器的实现较复杂,其次也是cms的替代产品,所以单独讲解,其他的回收器可以自行了解,其原理配置并不复杂
基本原理
将连续的堆内存划分成若干个Region,根据不同的策略进行处理
每个Region根据需要,可能是Eden区域,Survivor区域,或者老年代空间,也可以是特殊区域(存放大对象区域,超过Region的一半大小的对象)
在回收的过程中,可以计算出各个Region的回收价值,并维护优先级列表,根据用户配置的停顿时间,回收价值比较大的部分Region
分步: 初始标记 > 并发标记 > 重新标记 > 筛选回收
初始标记
遍历GCroots,找到直接的引用并进行标记,同时改变TAMS指针
因为只标记GCroots的直接引用,所以耗时非常短,停顿基本忽略
并发标记
该过程是和用户线程同时进行,将初始标记的对象进行递归扫描
同时将对象引用变动会影响回收的灰色对象,进行STAB记录
重新标记
会发生STW,需要将STAB中的记录进行重新处理,停顿还好
筛选回收
所有标记完成后,对回收区域价值计算并排序
选择要回收的Region,对其中的存活对象进行移动,然后回收整个Region区域,需STW
优缺点
优点:回收时间可控,配置参数简单,对大内存堆非常友好
缺点:更多的内存用于垃圾回收,需要写后屏障维护STAB
类加载
类加载流程
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载(验证+准备+解析统称为链接)
加载->验证->准备->初始化->卸载,顺序不能改变,其他的阶段倒不是那么绝对
加载: 将Class文件的字节码内容,加载到虚拟机,并生成一个java.lang.Class的对象
验证:对字节码内容进行验证,保证虚拟机自身安全
准备:为类中定义的静态变量分配内存并赋值
解析: 将常量池中的符号引用替换为直接引用的过程
初始化:执行类的静态初始化方法的阶段(程序员可控阶段)
类加载器
类加载器(class loader)完成类的加载流程,有jvm自带加载器和用户自定义加载器
启动类加载器(bootstrap classloader)主要加载javaHome下的lib中的jar
扩展类加载器(Extension classloader)主要加载JavaHome下的lib/ext下的jar
应用程序类加载器(Application classloader)加载用户路径(classpath)下的所有的jar
同一个类加载器加载的同一个类,才能算是严格意义上的同一个类
类加载按照双亲委派模型进行加载,先递归交给父类进行加载,父类无法加载时则自己加载
双亲委派原则可以在自定义加载器和JNDI的情况下被破坏
性能监控
jps命令
查看Java进程信息
-q:省略主类,只显示进程ID
-l:显示主类的类名
-m:显示main函数的参数
-v:显示jvm参数
jstat命令
主要用于监控垃圾回收情况
jstat -options 查看操作
jstat -gc pid 3s 10: 连续10次,间隔3s,查看gc回收情况
jinfo命令
jinfo -flags pid: 打印参数值
可以设置部分参数的值, 限制较大,用得不太多
jmap命令
生成堆快照,jmap -dump:live,format=b,file=heap.bin <pid>
jhat命令
分析堆快照,例如:配合jmap导出的堆快照文件,进行分析
jhat dump.bin;然后访问localhost:7000查看
jstack命令
查看线程信息
jcmd命令
多功能命令,几乎是前几个功能的组合命令
VisualVM工具
可视化工具,可安装插件,功能十分强大