JVM知识总结
2022-08-19 19:25:17 1 举报
AI智能生成
JVM知识总结
作者其他创作
大纲/内容
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM
通过Java命令执行代码的大体流程
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载到内存
1.加载
1.文件格式验证:验证二进制字节流是否符合Class文件格式的规范
2.元数据验证:主要对字节码的描述信息进行语义分析,以保证其描述信息符合java语言规范
3.字节码验证:类的字节码进行校验分析,保证该类的方法不会在运行时做出危害虚拟机安全的事
4.符号引用验证:可以看做是类对自身以外(常量池中各种符号引用)的信息进行匹配性校验
校验字节码文件的正确性
2.验证
给类的静态变量分配内存,并赋予默认值
3.准备
1.类或接口的解析:把一个从未解析过的符号引用N解析为一个类或接口的直接引用
2.字段解析:解析字段所属的类或接口的符号引用
3.类(静态)方法解析:首先解析出类方法表class_index项中索引的方法所属的类或接口的符号引用
4.接口方法解析:解析出接口方法表的class_index 项中索引的方法所属的类或接口的符号引用
虚拟机常量池内的符号引用替换为直接引用的过程
4.解析
触发初始化的场景
对类的静态变量初始化为指定的值,执行静态代码块
5.初始化
案例分析:加强理解
类加载过程
运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类被加载到方法区中后主要包含
这个类到类加载器实例的引用
类加载器的引用
对应class实例的引用
注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
代码案例
1.类加载和运行过程
引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
自定义加载器:负责加载用户自定义路径下的类包
1.类加载器类型
说明
源码
1.加载说明
先找父亲加载,不行再由儿子自己加载
子主题
JVM类加载器的层级(不是继承关系)
应用程序加载类的双亲委派机制源码
2.双亲委派机制
2.类加载器初始化过程:
1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改2.避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
3.为什么要设计双亲委派机制?
2.类加载器和双亲委派机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入
3.全盘负责委托机制
4.自定义类加载器示例:
代码和说明
思考一下:Tomcat是个web容器, 那么它要解决什么问题:
Tomcat 如果使用默认的双亲委派类加载机制行不行?
Tomcat打破双亲委派机制
1.tomcat的几个主要类加载器
2.自定义加载器委派关系
3.tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?
4.自定义类加载器类图
5.模拟Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离
6.注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
原理
7.模拟实现Tomcat的JasperLoader热加载
Tomcat自定义加载器详解
5.打破双亲委派机制
6.Hotspot源码JVM启动执行main方法流程
1.JVM类加载机制解析
内存模型链接
minor gc过程中对象挪动后,引用如何修改
参数说明
JVM内存参数设置
结论:对JVM优化就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收
日均百万级订单交易系统如何设置JVM参数
2.JVM内存模型剖析
1.类加载检查
否:执行类加载流程
“指针碰撞”(Bump the Pointer)(默认)
“空闲列表”(Free List)
1.如何划分内存
CAS(compare and swap)
2.并发分配问题
是:2.分配内存
2.是否已经加载
3.初始化零值
存储对象自身的运行时数据
类型指针
对象头(Header
实例数据(Instance Data)
保证大小是8字节的整数倍
对齐填充(Padding)
HotSpot虚拟机,对象在内存中存储的布局
32位
64位
对象头内存模型
4.设置对象头
5.执行<init>方法
什么是java对象的指针压缩?
为什么要进行指针压缩?
关于对齐填充:
6.一些问题
1.对象的创建
对象内存分配流程图
通过逃逸分析确认对象是否在栈上分配内存
就是分析对象动态作用域,分析对象是否会被外部方法所引用,例如作为调用参数传递到其他地方中
开启逃逸分析参数(-XX:+DoEscapeAnalysis)
开启方式
实现方式:标量替换
对象逃逸分析
栈上分配依赖于逃逸分析和标量替换
对象栈上分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC
指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快
Minor GC/Young GC
一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上
Major GC/Full GC
Eden与Survivor区默认8:1:1
对象在Eden区分配
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
为了避免为大对象分配内存时的复制操作而降低效率。
作用
大对象直接进入老年代
长期存活的对象将进入老年代
希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
对象动态年龄判断
老年代<年轻代大小,老年代的可用内存>之前年轻代minor gc后进入老年代的对象的平均大小---Full GC
老年代空间分配担保机制
2.对象内存分配
弊端
引用计数法
可达性分析算法
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选。
2. 第二次标记
分析
官方明确声明为不推荐使用的语法
finalize()方法最终判定对象是否存活
1.该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的 ClassLoader 已经被回收。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
如何判断一个类是无用的类
普通的变量引用
强引用
将对象用SoftReference软引用类型的对象包裹
应用
软引用
弱引用
虚引用
常见引用类型
3.对象内存回收
用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量就是指由字母、数字等构成的字符串或者数值常量
字面量
类和接口的全限定名 字段的名称和描述符 方法的名称和描述符
符号引用
Class常量池可以理解为是Class文件中的资源仓库
Class常量池
类常量池量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池
运行时常量池
对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
为字符串开辟一个字符串常量池,类似于缓存区
创建字符串常量时,首先查询字符串常量池是否存在该字符串
存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
1.设计思想
字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
1.new String();
这种方式创建的字符串对象,只会在常量池中
2.直接赋值字符串
只有一个
3.intern方法
2.三种字符串操作(Jdk1.7 及以上版本)
字符串常量池位置
字符串常量池,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。
JDK1.6
JDK1.7
一个经典面试题解析
String s0=\"zhuge\";String s1=\"zhuge\";String s2=\"zhu\" + \"ge\";System.out.println( s0==s1 ); //trueSystem.out.println( s0==s2 ); //true
String s0=\"zhuge\";String s1=new String(\"zhuge\");String s2=\"zhu\" + new String(\"ge\");System.out.println( s0==s1 ); // falseSystem.out.println( s0==s2 ); // falseSystem.out.println( s1==s2 ); // false
String a = \"a1\"; String b = \"a\" + 1; System.out.println(a == b); // true String a = \"atrue\"; String b = \"a\" + \"true\"; System.out.println(a == b); // true String a = \"a3.4\"; String b = \"a\" + 3.4; System.out.println(a == b); // true
String a = \"ab\";String bb = \"b\";String b = \"a\" + bb;System.out.println(a == b); // false
String a = \"ab\";final String bb = \"b\";String b = \"a\" + bb;System.out.println(a == b); // true
String a = \"ab\";final String bb = getBB();String b = \"a\" + bb;System.out.println(a == b); // falseprivate static String getBB() { return \"b\"; }
String常量池问题的几个例子
String s = \"a\" + \"b\" + \"c\"; //就等价于String s = \"abc\";String a = \"a\";String b = \"b\";String c = \"c\";String s1 = a + b + c;
关于String是不可变的
最后再看一个例子:
八种基本类型的包装类和对象池
字符串常量池设计原理
字符串常量池
3.对象创建与内存分配
只是根据对象存活周期的不同将内存分为几块,根据各个年代的特点选择合适的垃圾收集算法
分代收集理论
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉
标记-复制算法
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
标记-清除算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
标记-整理算法
1.垃圾收集算法
单线程
串行
STW
新生代采用复制算法,老年代采用标记-整理算法。
Serial Old收集器是Serial收集器的老年代版本
Serial收集器
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似
多线程,并行收集,STW
新生代采用复制算法,老年代采用标记-整理算法
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。
Parallel Scavenge收集器
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作
ParNew收集器
Concurrent Mark Sweep
工作原理流程图
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快
初始标记
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变
并发标记
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记
重新标记
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)
并发清理
重置本次GC过程中的标记数据。
并发重置
收集步骤
并发收集、低停顿。
优点
对CPU资源敏感(会和服务抢资源)
在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是\"concurrent mode failure\",此时会进入stop the world,用serial old垃圾收集器来回收
缺点
CMS的相关核心参数
CMS收集器
G1将Java堆划分为多个大小相等的独立区域(Region)保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
2.默认年轻代对堆内存的占比是5%,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%
3.年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1
4.动态分配:一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化
对象转移到老年代跟之前讲过的原则一样
G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
特点
初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快
并发标记(Concurrent Marking):同CMS的并发标记
最终标记(Remark,STW):同CMS的重新标记
筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间
主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片
回收算法
并行与并发
分代收集
空间整合
可预测的停顿
YoungGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发
MixedGC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
Full GC
G1垃圾收集分类
G1收集器参数设置
G1垃圾收集器优化建议
什么场景适合使用G1
G1收集器
2.垃圾收集器
说明:,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色
黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过
灰色:表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色:表示对象尚未被垃圾收集器访问过
三色
本轮不会进行清除
多标-浮动垃圾
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug
黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
增量更新
原始快照
解决方案
虚拟机的记录操作都是通过写屏障实现的
实现原理
所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):
写屏障
漏标-读写屏障
三色标记
在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。
记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围
记录集
hotspot使用一种叫做“卡表”(Cardtable)的方式实现记忆集
记忆集与卡表
垃圾收集底层算法实现
4.垃圾收集器详解
查看应用进程id
Jps
来查看内存信息,实例个数以及占用内存大小
jmap -histo 14660 #查看历史生成的实例
jmap -histo:live 14660 #查看当前存活的实例,执行过程中可能会触发一次full gc
查看堆信息
jmap -heap 54623
堆内存dump
设置内存溢出自动导出dump文件
-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=./ (路径)
Jmap
还可以用jvisualvm自动检测死锁
jstack加进程id查找死锁
1,使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
2,按H,获取每个线程的内存情况
3,找到内存和cpu占用最高的线程tid,比如19664
4,转为十六进制得到 0x4cd0,此为线程id的十六进制表示
5,执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
6,查看对应的堆栈信息找出可能存在问题的代码
jstack找出占用cpu最高的线程堆栈信息
Jstack
查看正在运行的Java应用程序的扩展参数
Jinfo -flags 12345
查看jvm的参数
Jinfo -sysprops 12345
查看java系统参数
Jinfo
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量
jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况
垃圾回收统计
jstat -gccapacity pid
堆内存统计
jstat -gcnew pid
新生代垃圾回收统计
jstat -gcnewapacity pid
新生代内存统计
jstat -gcold pid
老年代垃圾回收统计
Jstat
1.常用命令
2.Arthas
5.调优工具详解及实战
JVM性能调优
0 条评论
回复 删除
下一页