知识点扫盲-JVM
2021-09-01 14:40:40 11 举报
AI智能生成
小白一枚,多学多记!
作者其他创作
大纲/内容
类加载机制
加载过程
加载
Loading
类的二进制字节流
将字节流代表的静态存储结构转换为方法区的运行时数据结构
生成java.lang.class
验证
Verification
确保class文件字节流包含信息符号虚拟机要求
准备
Preparation
为类变量分配内存,并设置该变量的初始值,即零值
解析
Resolution
将常量池内的符号引用转换为直接引用的过程
初始化
initialization
执行类构造器方法<clinit>()
双亲委派
工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是委托给父类的加载器去执行;如果往上还有父类,则向上递归;
如果父类加载器可以完成类加载任务,则返回成功,否则子加载器尝试加载,这就是双亲委派模式
如果父类加载器可以完成类加载任务,则返回成功,否则子加载器尝试加载,这就是双亲委派模式
自定义加载器
Custom ClassLoader
系统类加载器
Application ClassLoader
拓展类加载器
Extension ClassLoader
引导类加载器
Bootstrap ClassLoader
出于安全考虑,仅包含java、javax、sun等开头的类
JAVA的核心库都是使用引导类加载器进行加载
优点
避免类的重复加载
java类对应加载器有层次关系,避免重复
保护程序安全,防止核心API被随意篡改
举例:java.lang
自定义java.lang.String,写个静态代码块,调用String控制台没有打印静态代码块内容,
就是因为还是用的核心库的String,默认使用的父类4
就是因为还是用的核心库的String,默认使用的父类4
loadClass()
破坏双亲委派机制
重写findClass()
子主题
热替换
沙箱安全机制
内存模型
堆
Heap
概述
一个JVM实例存在一个堆内存,堆是JAVA内存管理的核心区域
JAVA堆区在JVM启动时创建,大小确定,是JVM管理的最大一块内存空间
堆内存调节
-xms
设置堆空间初始内存大小
-x 是JVM运行参数,ms-memory start
-xmx
设置堆空间最大内存大小
默认初始大小为电脑内存/64,最大内存为电脑内存/4
开发中建议-xms/-xmx设置一致,避免GC后调整堆内存大小,造成系统额外压力
所有线程共享
可以划分线程私有缓冲区
Thread Local Allocation Buffer
TLAB
所有的对象实例和数组都应该在运行时分配在堆上
几乎
GC垃圾回收的重点区域
分代
年轻代(YoungGen)
Eden
几乎所有的对象都是在Eden区new出来的(一些特别大的对象,直接进人老年代)
Survivor0(From)
Survivor1(To)
老年代(OldGen)
占比
默认:-XX:NewRatio=2,表示年轻代占1,老年代占2,年轻代占1/3
默认:Eden:S0:S1=8:1:1
实际可能不是这个比例,因为自适应内存分配策略
调整比例:-XX:SurvivorRatio=8
-XX:xmn设置新生代的空间的大小,一般默认
晋升机制
-XX:MaxTenuringThreshold
默认15,大于则进入老年区
对象在survivor区,每经历一次minorGC,年龄+1
new对象先放到Eden区
当Eden区满的时候,再创建对象,则会触发垃圾回收(MinorGC),销毁不再被引用的对象,将新对象放进来
然后将Eden区存活对象放到S0区
如果再次触发垃圾回收,之前没被回收的S0区对象会放到S1区
再次经过垃圾回收,会存放到S0区域
当触发阈值,则进人老年区
养老区内存不足时,触发GC(MajorGC);GC后还不能保存对象,则OOM
备注
关于S0/S1:复制之后有交换,谁空谁是to
关于GC:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集
元空间(永久代)
jdk1.8之前是永久代
方法区
Non-heap(非堆)
存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
随着jdk版本的更新会有变化;
类型信息
类class、接口interface、枚举enum、注解annotation
包路径、父类、修饰符等
域信息
方法信息
常量
常量池
字节码文件(.class)中的常量池
为什么需要常量池?
一个java源文件中的类、接口编译后产生一个字节码文件。而java中的字节码需要数据支持,
通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个
字节码包含了指向常量池的引用(符号引用)。在动态链接的时候会用到运行时常量池
通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个
字节码包含了指向常量池的引用(符号引用)。在动态链接的时候会用到运行时常量池
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量类型等
运行时常量池
常量池表是class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区中的运行常量池中
运行时常量池包含所中不同的常量,包括编译期就已经明确的数值字面量,也包括运行期解析后
才获得的方法或者字段引用。此时不再是常量池中的符号地址,这里转换为真实地址
才获得的方法或者字段引用。此时不再是常量池中的符号地址,这里转换为真实地址
相较于常量池有动态性
创建类或接口的运行常量池,如果需要创建的内存空间超过了所能提供的最大值,则会OOM
静态变量
静态变量和类关联在一起,随着类的加载而加载
类变量被类的所有实例共享,即使没有类的实例也能访问
变化
1.6
有永久代,静态变量存放在永久代
1.7
有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
StringTable
永久代回收效率低只有当FGC才会触发,导致了StringTable回收效率不高;
但是我们开发中会有大量的字符串被创建,会导致永久代内存不足,放到堆
里能及时回收
但是我们开发中会有大量的字符串被创建,会导致永久代内存不足,放到堆
里能及时回收
静态变量
jdk7往后HotSpot虚拟机选择把静态变量与类型在java语言一端的映射class对象放到一起,存储于java堆中
1.8
无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆
永久代为什么被替换为元空间?
为永久代设置空间大小很难确定
在某些场景下,如果动态加载类过多,容易产生Perm区OOM
元空间和永久代的最大区别
元空间不在虚拟机中,而是使用的本地内存
对永久代调优很困难
方法区垃圾回收
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件比较苛刻,但是这个部门区域的回收有时又确实有必要的
垃圾回收
常量池中废弃的常量
字面量
字面量比较接近Java语言层次的常量概念,如文本字符串、final常量等
符号引用
符号引用属于编译原理方面的概念,包括下面三类
1、类和接口的全限定名
2、字段的名称和描述符
3、方法的名称和描述符
HotSpot虚拟机对常量池的回收策略是很明确的
只要常量池中的常量没有被任何地方引用,就可以被回收
不再使用的类型
回收条件
该类的所有实例都已被回收
加载该类的类加载器已被回收
该类对应的java.lang.class对象没有被任何地方引用,无法通过反射访问该类的方法
本地方法栈
java虚拟机栈管理java方法的调用,本地方法栈管理本地方法的调用
本地方法?
一个Native Method就是一个java调用非java代码的接口
有时候java应用需要与java外面的环境交互,这是本地方法存在的主要原因
本地方法是由C语言实现的
本地方法栈-->本地方法接口<--本地方法库
Hotspot JVM 将本地方法栈和JAVA虚拟机栈合二为一
JAVA虚拟机栈
Java虚拟机栈中是一个个栈帧,每个栈帧对应一个被调用的方法
栈遵循的是后进先出的原则,所以线程当前执行的方法对应的栈帧必定在Java虚拟机栈的顶部
栈帧
局部变量表
方法中定义的局部变量是否线程安全?
内部产生内部消亡,则线程安全
操作数栈
动态连接
方法返回地址
一些附加信息
内存溢出问题
-xss
调整栈大小
线程请求分配的栈容量超过栈允许的最大容量,抛出StackOverFlowError异常
如果可以动态扩展,尝试扩展的时候没有申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的栈,那么会抛出OOM
程序计数器
可以看作是当前线程所执行的字节码指令的行号指示器
线程私有
如果线程执行的是非本地(native)方法,则程序计数器中保存的是当前需要执行的指令地址;
如果线程执行的是本地方法,则程序计数器中的值是undefined
如果线程执行的是本地方法,则程序计数器中的值是undefined
为什么本地方法在程序计数器中的值是undefined的?
因为本地方法大多是通过C/C++实现的,并未编译成需要执行的字节码指令
因为本地方法大多是通过C/C++实现的,并未编译成需要执行的字节码指令
由于程序计数器中存储的数据所占的空间不会随程序的执行而发生大小上的改变,因此,程序计数器是不会发生内存溢出现象
垃圾回收
GC
部分收集(Partial GC)
新生代收集
Minor GC/YGC
Eden\S0\S1
触发机制
当Eden空间不足,触发YGC;每次YGC会清理整个年轻代
因为java对象有朝生夕死的特性,所以YGC很频繁,回收速度也快
YGC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程回复运行
老年代收集
Major GC/OGC
代表:CMS GC
触发机制
出现Major GC,一般会伴随至少一次Minor GC
即老年代空间不足会先去MinorGC,空间还不足才会MajorGC
MajorGC一般速度比MinorGC慢10倍以上,STW时间更长
如果MajorGC后空间不足,会报OOM
混合收集
Mixed GC
代表:G1 GC
收集整个新生代和部分老年代
整堆收集(Full GC)
整个java堆和方法区的垃圾回收
触发机制
调用System.gc(),系统建议执行,但不一定执行
老年代空间不足
方法区空间不足
MinorGC后进入老年代的平均大小大于老年代的可用内存
Eden区、S0区向S1区复制时,S1空间不足,则把对象放到老年区,切老年区的可用内存大小不足
垃圾回收算法
标记阶段
对象存活判断
引用计数算法
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况(引用+1、引用失效-1,为0则可回收)
优点
实现简单、垃圾对象便于辨识;判定效率高,回收没有延时
缺点
存储空间开销大
单独存储计数器
时间开销
每次赋值更新计数器,伴随加减法
无法处理循环引用
可达性分析算法
又叫根搜索算法/追踪性垃圾收集
有效解决在引用计数算法中循环引用的问题,防止内存泄漏
基本思路
以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径成为引用链
如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象
GC Roots
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈内JNI(通常说的本地方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁synchronized持有的对象
java虚拟机内部的引用
除了以上固定集合,还可以有其他对象“临时性”加入
如分代收集和局部回收
小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root
注意
保障一致性,需要STW
标记-清除
Mark-Sweep
适用场景
对象存活比较多的时候
老年代
缺点
提前GC
碎片空间
扫描两次
标记存活对象
清除没有标记的对象
这里的清除,并不是真的清空,而是把需要清除的对象地址保存在空闲的地址列表里;下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放
标记-复制
Copying
适合场景
存活对象少,比较高效
扫描整个空间(标记存活对象并复制移动)
适合年轻代
缺点
需要空闲空间
需要复制移动对象
标记-整理
Mark-Compact
等同于标记-清除后再进行一次碎片整理
标记-清除是非移动式的回收算法;标记-整理是移动式的
评估GC的指标
吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
暂停时间
一个时间段内应用程序线程暂停,让GC线程执行的状态
最大吞吐量优先的情况下,降低停顿时间
垃圾回收器
串行回收器
Serial
Serial Old
并行回收器
ParNew
Parallel Scvenge
Parallel Old
并发回收器
CMS
G1
G1收集器主要涉及到Mixed GC,MixedGC会回收新生区和部分老年区
新生代
Serial、ParNew、Parallel、G1
复制算法,G1是标记整理/复制算法
老年代
Serial Old、Parallel Old、CMS、G1
标记-整理算法,CMS是标记清除
查看回收器有没有用
jinfo -flag UseG1GC pid
发展阶段
Serial(串行)-->Parallel(并行)-->CMS(并发)-->G1-->ZGC
怎么选择?
优先调整堆的大小让JVM自适应完成
如果内存小于100M,使用串行收集器
如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
如果是多CPU,需要高吞吐量,运行停顿时间超1秒,选择并行或者JVM自己选择
如果是多CPU,追求低停顿时间,需要快速响应,使用能够并发收集器,官方推荐G1,性能高。现在互联网的项目,基本都是G1
引用
强引用
无论任何情况下,只要强引用关系还存在,垃圾回收器永远不会回收掉被引用的对象
软引用
内存不足即回收
弱引用
发现即回收
虚引用
对象回收跟踪
对象
对象的实例化
创建对象的方式
new
最常见的对象
变形1:Xxx的静态方法
变形2:XxxBuilder、XxxFactory的静态方法
Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求
使用clone()
不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
使用反序列化
从文件中、从网络中获取一个对象的二进制流
第三方库Objenesis
创建对象的步骤
1.判断对象对应的类是否加载、链接、初始化
2.为对象分配内存
如果内存规整
指针碰撞
如果内存不规整
虚拟机要维护一个列表
空闲列表分配
说明
3.处理并发安全问题
采用CAS配上失败重试保证更新的原子性
每个线程预先分配一块TLAB
4.初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5.设置对象的头信息
6.执行init方法进行初始化
内存布局
对象头(Header)
包含两部分
运行时元数据(Mark Word)
hashCode
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
类型指针
指向类元数据InstanceKlass,确定该对象所属的类型
说明:如果是数据,还需记录数组的长度
实例数据(Instance Data)
说明
它是对象真正存储的有效信息,包括程序代码定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
规则
相同宽度的字段总是被分配在一起
父类中定义的变量会出现在子类之前
如果CompactFields参数为true(默认为true):子类的窄变量可能插入父类变量的空隙
对齐填充(Padding)
不是必须的,也没特别含义,仅仅起到占位符的作用
String的intern()
将字符串对象放入串池,如果串池有,不会放入,返回已有的串池中的对象的地址;如果没有(jdk6会把此对象复制一份/jdk7会把对象的引用地址复制一份),放入串池,并返回串池中的引用地址
执行引擎
将字节码指令解释/编译为对应平台上的本机机器指令(简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者)
解释器
将源码编译成字节码文件,然后在运行时通过解释器将字节码文件转换为机器码执行
当程序执行后,解释器可以马上发挥作用,省去编译的时间,立即执行
JIT编译器
为了提高执行效率,会使用即使编译技术将方法编译承机器码后再执行
编译器想要发挥左右能够,需要把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高
HotSpot VM采用解释性和JIT编译器并存
为什么不抛弃解释器?
当虚拟机启动时,解释器可以先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率
性能监控和调优
性能监控(发现问题)
GC频繁
cpu load过高
OOM
内存泄漏
死锁
程序响应时间长
性能分析(排查问题)
打印GC日志,通过GCviewer或者http://gceasy.io来分析日志信息
灵活运用命令行工具,jstack、jmap、jinfo等
dump出堆文件,使用内存分析工具分析文件
使用阿里Arthas或jconsole、JVisualVM来实时查看JVM状态
jstack查看堆栈信息
性能调优(解决问题)
适当增加内存,根据业务背景选择垃圾回收器
优化代码,控制内存使用
增加机器,分散节点压力
合理设置线程池线程数量
使用中间件提高程序效率,如缓存、消息队列等
....
性能指标
1.响应时间
2.吞吐量
3.并发数
同一时刻,对服务器有实际交互的请求书
4.内存占用
java堆区所占用的内存大小
5.相互之前的关系
0 条评论
下一页