JVM相关知识点
2023-12-03 13:29:26 0 举报
AI智能生成
JVM相关知识
作者其他创作
大纲/内容
java 文件执行过程
1、java文件通过javac将源文件编译成class
2、编译好的class+java相关的类库会被ClassLoader load 进内存
3、装载完之后,会调用字节码解释器或即时编译器进行解释或编译
4、解释或者编译完成之后会交给执行引擎开始执行
认识Class文件
是一组以8字节为基础单位的二进制流
ClassFile 查看方式
IDEA 插件 - BinEd,Idea中File → Open as Binary 就可以查看Class文件对应的十六进制内容
Class 文件
查看ByteCode的方法
JClassLib - IDEA插件(看着方便)
安装插件——鼠标光标放在类体 —— View —— Show Bytecode With jclasslib
字节码
Constant pool count :常量池个数 16
Access flags: 0x0021 = ACC_PUBLIC & ACC_SUPER
ACC_PUBLIC 0x0001 是否为public类型
ACC_SUPER 0x0020 该标记必须为true,jdk1.0.2之后编译出的内容必须为真
This class: 当前类的位置,在 Constant Pool(第一个编号是1) 的 2号位置
Super class: 父类的位置,在Constant Pool 的第3号位置
Interface count: 实现了多少个接口
Fields count:属性的个数
Methods count:方法个数
Attributes count:附加属性个数
Constant Pool 详细信息
0 号是预留的,没有任何引用指向这个位置,是从1号开始 的,个数 = constant pool count - 1 = 15 个
字节码文件
JVM在执行一个方法的时候,会从字节码中读取一条指令 如 2a,则再查找对应的汇编指令,即 aload_0,做相应的操作,接下来再读下一条指令 b7,查找到对应的汇编指令 invokespecial (调用构造) ,一直进行类似的操作到结束 到b1指令——return
2ab7 0001 b1
这5个字节可以表示一个构造方法的调用过程
2a:aload_0 表示把 this 压栈 扔进去
b7:invokespecial 需要两个参数 00 和 01
01:代表常量池中的第一项java.lang.Object 中的构造方法 即 默认构造调用的是父类Object的构造方法
b1:return
Class文件加载过程
1、loading
把一个class 加载到内存,懒加载,需要的时候再加载
双亲委派
jvm有一个类加载器的层次,分别加载不同的class
一个class 被 load 到内存后, 内存中创建了两块内容
一块是将class对应的二进制内容扔进 内存中
同时也生成了一个class类的对象并指向二进制文件,这个对象保存在mataspace中
类加载器的分类
Bootstrap ClassLoader
JDK 加载核心类的类加载器,加载lib/rt.jar charset.jar 等核心类,C++实现
sun.boot.class.path
Extension ClassLoader
加载扩展jar包, jre/lib/ext/*.jar 或 由 -Djava.ext.dirs 指定的包
java.ext.dirs
App ClassLoader
加载classpath 指定的内容
java.class.path
Custom ClassLoader
自定义的ClassLoader
自定义加载器:重写findClass —— 只修改了加载class的逻辑
Class 类加载器加载过程
class首先会从 Custom加载器(缓存 ——每个类加载器 维护一个list 用来管理已经加载的对象引用)中找,找到返回结果,找不到会从上层加载器APP中找,如果还是找不到再到Ext加载器中招,一直到Bootstrap加载器,(前面的find 都是从缓存中找),如果缓存中没有,则find class 并加载,如果 bootstrap没有加载到,则交给下一层加载器 ext find class 并加载 ,直到custom 。如果custom 没有找到class,则报错 ClassNotFoundException异常
父加载器不是“类加载器的加载器”,也不是“类加载器的父类加载器”
为啥要搞双亲委派
主要是为了安全
假如没有双亲委派,我自己定义一个 java.lang.String ,这样自定classloader 则会加载到的是自己写的,不是oracle的,如果是双亲委派,则find cache 的时候 找到的jdk的String
其次是效率问题
如何打破双亲委派机? 即不想用双亲委派机制
由于委培机制是在 ClassLoader 类中的 loadClass方法中完成的
通过parent.loadClass 进行向上查找 findClass 向下加载 完成
所以如果要想打破这个机制,则重写 loadClass 方法即可
通过parent.loadClass 进行向上查找 findClass 向下加载 完成
所以如果要想打破这个机制,则重写 loadClass 方法即可
2、linking
verification:校验class、是否满足class的格式
preparation:把class中静态变量设置成默认值,比如 int 类型为 0
resolution
解析loadeClass方法中的第二个参数 true 为解析 false 不解析
即 将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
3、initializing
静态变量赋初始值,调用静态代码块
在类初始化代码<clinit>,给静态成员变量赋初始值
内存屏障
乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系
如何保证特定情况下不乱序
硬件内存屏障 X86
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
JVM级别如何规范(JSR133)
LoadLoad 屏障
对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
StoreStore屏障
对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障
对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
StoreLoad屏障
对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见
Volatile
作用
保证线程可见性
禁止指令重排序
字节码实现
字节码通过一个 flag 控制 ACC_VOLATILE
JVM层面 会在字节码 flag 地方添加内存屏障
volatile内存区的读写 都加屏障
StoreStoreBarrier volatile写操作 StoreLoadBarrier
LoadLoadBarrier volatile读操作 LoadStoreBarrier
OS和硬件层面
volatile i++ load时 | i | ++操作 | i | | 表示屏障
lock 指令实现 | MESI实现
X86 : lock cmpxchg / xxx
cmpxchg 表示对内存某个区域修改的一条指令
加了lock,表示 cmpxchg执行的过程中,这个区域只有我这个指令时可以修改的
cmpxchg 表示对内存某个区域修改的一条指令
加了lock,表示 cmpxchg执行的过程中,这个区域只有我这个指令时可以修改的
指令重排要遵循的规则
hanppens-before 原则
程序顺序规则
程序执行的先后顺序,即源代码前面的操作一定会被后面的操作看到
监视器规则(管理锁定规则)
同一把锁,unlock 先于 lock
volatile变量规则
对一个volatile变量的写操作一定要发生于后面对这个变量的读操作
线程启动规则
Thread的start操作,先行发生于这个线程的每一个操作
join 原则
如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
线程终止原则
线程中的所有操作都先行于此线程的终止检测。可以通过 Thread.isAlive()的返回值等手段检测线程的终止
程序中断规则
对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生
对象finalize规则
个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始
传递性
A happened-before B,B happened-before C,那么 A happened-before C 即 A先行于B,B操作先行于C,那么A先行于C
as if serial 原则
为了提升处理器计算效率,对没有数据依赖关系的指令在执行的过程中可能会乱序(重排序),为了保证单线程情况下最终指令计算结果的正确性(最终结果不变),编译器、处理器都必须遵守 as-if-serial 语义
Object的内存布(hotspot)
普通对象
对象头
hospot 中叫 markword 占8个字节
ClassPointer指针
-XX:+UseCompressedClassPointers 为4字节 不开启为8字节 , t = new T() 出来对象后 有一个指针 t , 这个指针指向 T.class
实例数据
long int String ....
引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节
Padding对齐,8的倍数
读取时按照块来读取的 ,目的是为了提升效率 ,对齐后对象大小是8的倍数
数组对象
对象头:markword 8
ClassPointer指针同上
数组长度:4字节
Padding对齐,8的倍数
JVM Runtime Data Area
run time constant pool
constant pool:运行的时候将常量内容仍到这个区域里面
ProgramCounter:程序计数器存放下一条指令位置的内存区域,虚拟机的运行
Heap:存放对象
native method stacks:本地方法(c/c++—→java调用了JNI) ,调用了内部 c 和 c++ 写的方法时的栈,一般不管这个,也没办法去调优
JVM stacks:java内部JVM管理的栈,java 运行的时候,每一个线程都有一个栈,装的是栈帧 frame
Direct Memory:直接内存
java 1.4 之后增加的,JVM可以直接访问的内核空间的内存(OS管理内存)。直接内存区域,不归JVM管理,归操作系统管。一般情况下所有的内存一般都是JVM直接管理,为了增加IO的效率1.4之后增加了直接内存的概念,也就是在jave虚拟机实际是可以访问操作系统里面的内存的,提高效率
Method area:装的是各种各样的class和常量池的内容
PermSpace 和 Meta Space 是不同版本对Method area的实现
1、Perm Space 1.8 之前 永久区域
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变
2、Meta Space 1.8以及以后 元数据区
字符串常量位于堆
会触发FGC清理
不设定的话,最大就是物理内存
线程共享区域
每一个线程都有自己的 Program counter——目的是线程切换
每一个线程都有自己的 JVM stacks 装的是一个个 栈帧 frame,每个方法都有自己独立的栈帧
每一个线程都有自己的native method stack
栈帧 Frame
—每个方法对应一个栈帧
帧用于存储数据和部分结果,以及执行动态链接、方法返回值和分派异常
每个线程都有自己的 JVM Stacks,每一个jvm stack 存放的是好多个栈帧,每一个栈帧里面都有自己的操作数栈
每个方法的栈帧都有如下几个组成部分
Local Variables :局部变量
Operand Stacks 操作数栈 , 计算的时候压栈出栈的区域
Dynamic linking 动态链接
从constant pool 找到符号链接,看有没有解析成直接引用,如果没有解析则动态解析,如果已经解析,则直接拿来用
A 方法 调用了B 方法,而B 方法要去常量池中找,这个过程就叫Dynamic Linking
return address 返回地址
JVM常见指令
<clinit> class 静态语句块执行执行
<init> 构造方法
_store 出栈 从栈里弹出来,然后本地变量表存储栈里面的东西
_load 压栈 本地变量表的常量 加载进栈
pop 弹出
add 加法
sub 减法
mul 乘法
div 除法
invode
invokeStatic:调用静态方法用到的指令
invokeVirtual:调用普通方法用到的指令
自带多态,栈里面压得是哪个就是哪个
invokeSpecial:调用可以直接定位,不需要多态的方法,即:private 和 构造方法
invokeInterface:调用接口的方法
invokeDynamic:(最难)
lambda 表达式、或反射或者其他动态语言 会动态产生自己对应的Class,会用到该指令
for(;;){
I aa = C :: c ; // 这样会创建很多内部类,1.8 之前 经常OOM 1.8之后 回收不及时会OOM
}
I aa = C :: c ; // 这样会创建很多内部类,1.8 之前 经常OOM 1.8之后 回收不及时会OOM
}
GC
GC 基础
如何找到垃圾
Reference count:引用计数法
python 采用的是这种
有一个引用指向一个对象,在对象头写一个数字 ,有几个引用指向它,则标记为几,引用变成0的时候,则表示垃圾
弊端:不能解决循环引用的垃圾堆,比如 两个对象互相引用,再没有任何对象引用它俩
Root Searching:根可达算法
hotspot 采用这种
从根对象一直往下找,找不到的就是垃圾
怎么定义根对象
线程变量
main方法开始会启动一个线程栈,栈里面会有很多栈帧,对应栈帧里面开始的这些对象算根对象
静态变量
class被load进内存的时候,会对这些静态变量初始化,这些静态变量能够访问到的对象也算是根对象
常量值
当前class 会用到其它class 对象的 常量也算根对象
本地方法调用者
调用了 java 写的 C / C++ 本地的方法 也算 根对象
GC清除算法
Mark-Sweep (标记清除)
优点
算法相对简单
缺点
要经过两遍扫描,效率偏低,第一遍找出有用的,第二遍找出没用的垃圾
容易产生空间碎片——即 内存之间会产生很多的空闲块 很多窟窿
使用场景
适用于存活对象比较多的情况下效率较高
Copying (拷贝复制)
优点
只扫描一次,内存分布清晰,可以紧挨在一起,不会产生碎片
缺点
对象复制过程,需要调整对象引用位置,浪费空间
使用场景
存活对象较少的情况
Mark-Compact (标记压缩)
优点
不会产生碎片,方便对象分配,且不会产生内存减半
缺点
效率低,扫描两次,第一遍扫描,找到不可回收的,然后从前面扫描空闲位置或者要回收的位置,再移动该不可回收的对象到前面,这样后面的整个区域空闲且连续
使用场景
存活对象较多的情况
分代GC JVM内存分代模型
-Xms -Xmx 设置堆的最小空间和最大空间
-Xms -Xmx 设置堆的最小空间和最大空间
Young Generation
1/3 的堆空间
0Xmn 设置新生代堆大小
1/3 的堆空间
0Xmn 设置新生代堆大小
Eden
8/10的Y空间
8/10的Y空间
Survival
2/10的Y空间
2/10的Y空间
S0
S1
采用 Copying算法
Old Generation
2/3的堆空间
2/3的堆空间
采用标记清除或标记压缩(存活率高,回收少)
垃圾分配
new 对象之后,首先尝试往栈上仍,栈扔不下,先仍进对堆中的 Eden区域,垃圾回收一次后,进入 Survival S0, 再回收 进入 S1,下一次回收循环进入S0,循环复制年龄超过限制时进入old 通过参数:-XX:MaxTenuringThreshold 配置
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
对象在各个区域的分配
分配过程
new 一个对象后,首先尝试到栈上分配(栈有一个好处 pop 完后就结束 无需 GC)
主要是私有小对象
栈分配不下,根据指定的参数查看是否装下,比较对象大小,过大,直接进入old FGC 回收
如果对象大小小于配置参数值,先看是否满足TLAB,然后分配到Eden区
线程本地分配 TLAB
Thread Local Allocation Buffer
占用eden,默认 1%
栈上分配不下的会优先进行TLAB分配,每个线程在 eden区域分配一个1%的空间,这个空间只是某个线程独有,分配空间的时候,首先往线程独有的这个空间分配,这样就不会和其它线程产生争用,所以效率就会变高
在Eden区域 进行GC清除,如果清除成功 结束
Eden主要是新对象
如果Eden GC没有清理,S1 再GC清除,如果年龄不够进入 S2,如果年龄够进入old区域
Old 主要是大对象、持久对象
Card Table(卡表)
主要在分代模型中,帮助我们进行垃圾回收、垃圾回收速度比较快
在结构上底层是通过 Bit Map来实现的
由于做YGC时,需要扫描整个Old区,效率非常低,所以JVM设计了Card Table,如果Old区域Card table 中有对象指向Y区,就将它设置为dirty,下一次扫描时,只需要扫描Dirty card ,
JVM内部把整个内存分成了一个一个的card(Y 和O区都区分),具体的对象存在于一个一个card中,如果Old区域中某一个card中有指向Y区的对象,就把这个Card标记为 Dirty,说明这个Card里面有指向Y区的对象
用来标记 Old 区的对象,Y区的不需要标记在 CardTable 中
常见垃圾回收器组合
组合
常见组合
Serial + Serial Old
Serial
单线程清理年轻代,在安全点上所有工作线程停止等待 STW 时间
Serial Old
单线程清理老年代
Parallel Scavenge + Parallel Old (1.8默认)
PS
多线程清理年轻代
PO
多线程清理老年代
PerNew + CMS
PerNew
是PS的一个变种,是配合CMS使用的,PS 不能和CMS组合
CMS(Concurrent mark sweep)
高响应,低停顿
标记的不是垃圾,没被标记的最后被清理
CMS 的几个常见阶段
并发是用户线程和GC线程并发
并发是用户线程和GC线程并发
1、初始标记(STW) 单个GC线程
直接找到最根上的对象并标记,只标记开始对象,所以时间并不长
2、并发标记 单个GC线程
一边产生垃圾,一边标记(这一块市最浪费时间的,所以和用户线程并发执行,只是可能客户端访问变慢了一点)
3、重新标记(STW) 多个GC线程
并发过程中会产生新的垃圾,也有可能是原来的垃圾变成了非垃圾、需要重新标记
4、并发清理 单个GC线程
这个过程中也会有问题,并发清理时其它工作线程也会产生新垃圾,叫浮动垃圾,浮动垃圾就要等下次CMS做清理时再次清理
CMS 缺点
CMS 一旦不行,Serial Old 就要上场了, 单线程扫天安门广场
内存碎片化严重
CMS 叫 Concurrent mark sweep,天然问题就是碎片化
浮动垃圾过大
因为是并发清理,所以在清理过程中会产生浮动垃圾
怎么解决???
降低触发CMS的阈值
调整参数 –XX:CMSInitiatingOccupancyFraction 92%
意思是92%的时候才会触发FGC,可以把这个值降低一点 ,老年的内存被占用到92%的时候CMS开始工作。让CMS保持老年代足够的空间
G1 (逻辑分代,物理不分代)
概念
Garbage First Garbage Collector 垃圾优先GC
G1 是将内存分成一小块一小块的region,在某一块内存工作的时候,可以清理别的块内存。分而治之
Garbage First 的意思就是里面存活对象最少的Region 就是垃圾最多的region,优先回收这样的Region
G1 快的原因
在堆中有一块区域 Collection Set(1%的堆空间):用来存放需要回收的Region集合
每个Region有一个Remember Set(hashset):记录其它Region对象到本Region对象的引用,回收的时候会查看是否有指向自己的,如果没有就回收
特点
并发收集
压缩空闲空间不会延长GC暂停时间
更容易预测的GC暂停时间
适用不需要实现很高的吞吐量的场景
追求吞吐量 追求响应时间
G1 新老年代比例一般不需要手工指定(会自动优化)
由于G1配置少,内部自动优化的原因:G1 要时刻盯着内存的垃圾,所以CPU计算时间大量花费在了GC上
总结
并发垃圾回收是因为无法忍受STW
Serial 阶段 都是单线程清理垃圾
PS/PO阶段 变成了多线程清理垃圾
CMS 分了四个阶段,STW的阶段是不耗时操作,将耗时的GC和用户线程并发处理了
GC标记算法
主要是为了解决并发过程中的漏标问题
因为引用关系正在发生变化
主要是为了解决并发过程中的漏标问题
因为引用关系正在发生变化
三色标记 (CMS、G1)
顺着root一直向下标记,标记到的说明有对象引用、能找到,最后没有遍历到的就是垃圾
把对象在逻辑上分成三个不同的颜色
黑色:全部标记(自身和成员变量均已标记完成)
灰色:标记了一半(自身被标记、成员变量未被标记)
白色:未标记
颜色指针(ZGC)
一个指针在JVM中如果没有压缩的话,占用8个字节64位,会从这64个bit中拿出来3个标记这个对象指针的变化。如果变过了,在进行GC的时候,会扫描变化过的指针,所以叫颜色指针。
漏标
并发标记和工作线程是同时进行了,A对象全部标记后,工作线程又把已经标记的A 又指向了D,原来B到D的这个指向没了,满足这两个条件的情况才会漏标
漏标产生步骤
背景:A 全部标记、B 标记一半、C 未标记
过程:A 标记完之后, 在标记B的过程中 A指向了 C,B 取消了对C的引用
这样如果不再对A进行扫描的话,就会把C 回收掉,但这个时候 C 是被A 引用的,显然回收是不合适的
如何解决漏标
核心思路:打破上述条件之一即可
Incremental Update (CMS使用):跟踪A指向C后重新扫描一次A,重新扫描后标记会灰色
SATB (G1使用):跟踪B指向C的引用消失后再扫描一次D,扫描D后找到A,标记为灰色
Incremental update 和 SATB
incremental update
关注引用增加
增量更新
把黑色的重新标记为灰色,下次重新扫描属性
而增量更新中为变成灰色后需要对灰色的重新扫描,效率低
SATB (Snapshot at the beginning)
关注引用的删除
当B—>C消失时,要把这个引用推到GC的堆栈,保证D还能被GC扫描到
引用删除表示将该发生改变引用压到JVM堆栈里面,下一次GC的时候会从堆栈里面扫描变更的对象,然后根据这个对象D找到指向它的相关对象(通过RememberSet找关系),再进行标记即可
为什么G1 要用 SATB
灰色—>白色引用消失时,指向白色引用会被push到堆栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆取查找指向白色的引用,效率比较
SATB配合RSet,浑然天成
incremental update 增量更新,会将原来已经扫描过后的对象成员重新扫描一遍,效率很低,STAB只需要关注更改过的对象即可,扫描次数变少
GC调优
Hotspot 参数分类
标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消
系统CPU经常100%,如何调优?
CPU100%原因是一定有线程在占用系统资源
找出哪个进程cpu高(top)
该进程中的哪个线程cpu高(top -Hp)
导出该线程的堆栈 (jstack)
查找哪个方法(栈帧)消耗时间 (jstack)
工作线程占比高 | 垃圾回收线程占比高
系统内存飙高,如何查找问题?
内存标高一定是堆占的比较高
首要要做的事导出堆内存 (jmap)
然后根据工具分析 (jhat jvisualvm mat jprofiler ... )
如何监控JVM
top命令观察到问题:内存不断增长 CPU占用率居高不下
top -Hp pid 观察进程中的线程,哪个线程CPU和内存占比高
jps和jstack
jps 定位具体java进程 jps 会把java 的进程都列出来 === ps -ef | grep java
jstack jstack 3984 会把里面所有线程的信息列出来
也能定位到具体代码行数
会将所有线程详细信息输出,包括线程状态 。 重点关注:WAITING 状态 或 BLOCKED 状态信息
jinfo pid 将进程(jps 获取的那个pid)
将进程的详细信息打印出
jstat -gc 动态观察gc情况
jstat -gc 4655 500 : 每个500个毫秒打印GC的情况
jmap - histo 4655 | head -20,查找有多少对象产生
jmap -dump:format=b,file=xxx pid : 堆转储文件命令
jvisualvm 分析 dump
java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError
HeapDumpOnOutOfMemoryError 这个参数表示如果OOM 会默认生成一个堆文件
arthas在线排查工具
java -jar arthas-boot.jar 命令启动该项目
启动之后会将该机器对应启动的程序进程列举出来 然后输入对应的序号,arthas 会挂在当前序号对应的进程下
thread定位线程问题
thread 48 可以查看对应线程的详细信息
GC常用参数
基本
Parallel常用参数
CMS常用参数
G1常用参数
0 条评论
下一页