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