JVM
2022-04-29 08:02:38 17 举报
AI智能生成
JVM知识点梳理
作者其他创作
大纲/内容
JMM
存储器的层次结构
L0:寄存器
L1:高速缓存
L2:高速缓存
L3:高速缓存
L4:主内存
L5:硬盘
L6:远程文件存储
更小 更快 成本更高---更大 更慢 成本更低
硬件层数据的一致性
缓存锁
缓存行
cpu从内存中读取数据以cache line为基本单位,目前长度为64bytes
伪共享
位于同一缓存行的两个不同数据,被两个不同的CPU锁定,产生互相影响
使用缓存行的对齐能够提高效率
每次需要读取的数据独占64byte字节
缓存一致性协议
缓存一致性协议有很多,因为常用的是Intel所以这里讲MESI
S-shared---该状态意味着当前缓存行数据不止存在本地CPU缓存中,
还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的
还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的
E-exclusive---代表该缓存行对应内存中的内容只被该CPU缓存,
其他CPU没有缓存该缓存行中的内容
其他CPU没有缓存该缓存行中的内容
M-modified---代表该缓存行中的内容被修改了,
并且该缓存行只被缓存在该CPU中,未来某一时刻会被写入到内存中
并且该缓存行只被缓存在该CPU中,未来某一时刻会被写入到内存中
I-invalid---代表该缓存行中的内容是无效的
总线锁
CPU访问主内存时加锁
乱序执行
什么是乱序执行
CPU为了提高指令执行效率,会在一条指令执行过程中,
去同时执行另一条指令,前提是,两条指令没有依赖关系
去同时执行另一条指令,前提是,两条指令没有依赖关系
产生的原因
各级缓存读写速度相差巨大
乱序读
当读取的指令在数据上没有依赖关系是,读取指令乱序执行
合并写
Write Combining Buffer 是CPU里面真实的存储单元,是硬件
Intel的CPU中,Write Combining=4byte
L1_cache未命中时,cpu会把待写入的数据写入到WC缓冲区,再写入L2_cache
硬件层面解决乱序问题
CPU内存屏障
save fence
load fence
mfence
Lock指令
JVM层面解决乱序问题
LoadLoadBarrier
LoadStoreBarrier
StoreStoreBarrier
StoreLoadBarrier
volitile实现细节
字节码层面
Access flags:volatile
JVM层面
volatile内存区的读写都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
OS和硬件层面
lock指令保证顺序执行
MESI协议保证可见性
synchornized实现细节
字节码层面
方法:Access flags:synchronized
同步代码块:monitorenter/monitorexit
同步代码块:monitorenter/monitorexit
JVM层面
C/C++调用了操作系统同步机制
硬件层面
X86:lock 指令实现
对象
对象创建过程
字节码load到内存
loading
linking
校验class文件
静态变量赋默认值
符号引用变为直接引用
initializing
静态变量赋初始值
申请内存
指针碰撞
空闲列表
成员变量赋默认值
调用构造方法
成员变量顺序赋初始值
执行构造方法语句
对象在内存中的存储布局
普通对象
MarkWord(HotSpot对象头):8byte
ClassPointer(指针):指向class对象
INSTANCE(实例数据):成员变量字节数
Padding(对齐):8的倍数
数组对象
数组长度:4byte(比普通对象多了一个数组长度)
对象头具体包括什么
对象头总共8byte估计64bit
根据对象的不同状态头的布局是不同的
4bit分带年龄
2bit表示锁标志位
1bit表示偏向锁
当一个对象被计算过identityHashCode,则无法进入偏向锁状态,因为对象头没有足够的空间了
对象定位
句柄池
reference中存储的就是对象的句柄地址
句柄方式最大的好处就是reference中存储的是稳定的句柄地址,
在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,
reference本身不需要被修改
在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,
reference本身不需要被修改
直接指针(HotSpot)
reference中直接存储的就是对象地址
使用直接指针方式最大的好处就是速度更快,他节省了一次指针定位的时间开销
Object o = new Object()在内存中占多少字节
16字节-无论是否开启ClassPointer指针压缩,都是16字节,因为有Padding对齐8的倍数
对象怎么分配
栈上分配
线程私有
小对象
无逃逸
只在某段代码中使用
支持标量替换
可以用一些基本的属性直接替换对象,比如一个对象里面只有1个int类型的变量
无需调优
线程本地分配(Thread Local Allocation Buffer-TLAB)
小对象
占用Eden1%
多线程的时候不用竞争eden区域,就可以申请空间,提高效率
无需调优
Eden区
Old区
JVM Instructions
T t = new T();
new
申请内存
dup
将位于栈顶的值复制1份放入栈顶
invokespecial
调用构造方法
astore_1
将栈顶的值弹出赋值给1位置的局部变量
invoke
InvokeStatic
静态方法调用
InvokeVirtual
调用普通方法
InvokeInterface
调用接口
InovkeSpecial
调用private 方法 , 构造方法
InvokeDynamic
JVM最难的指令:
lambda表达式或者反射,动态产生的Class,会用到的指令
lambda表达式或者反射,动态产生的Class,会用到的指令
JVM Runtime Data Area
Stacks(栈)
frame(栈帧)
Operand Stacks(操作数栈)
用push,pop 操作数据
VariablesTable(局部变量表)
存放:
1)方法参数m(params)
2)本地变量
3)如果是非static方法,还会存放this
Dynamic Linking(动态链接)
将符号引用解析为直接引用的过程
Return Address(动态返回地址)
遇到返回的字节码指令
异常退出
JVM Stacks(虚拟机栈)
为执行Java方法服务
Native Method Stacks(本地方法栈)
为虚拟机使用到的Native方法服务
Progarm Counter(程序计数器)
当前线程所执行的字节码的行号指示器
每个线程都有属于自己的程序计数器,包含在每个线程自己的栈里
Method Area(方法区)
Permanent Generation
1.8之前的实现,叫做:永久代
FGC不会清理
字符串常量位于Permanent Generation
大小启动的时候指定,不能变
Metadata Space
1.8之后的实现,叫做:元空间
字符串常量位于堆
会触发FGC清理
不设定的话,最大就是物理内存
存储信息
Runtime Constant Pool
类型信息
这个类型的完整有效名
这个类型直接父类的完整有效名(除非这个类型是interface或是
java.lang.Object,两种情况下都没有父类)
java.lang.Object,两种情况下都没有父类)
这个类型的修饰符(public,abstract, final的某个子集)
这个类型直接接口的一个有序列表
类型的常量池( constant pool)
类变量
类的静态变量
域(Field)信息
方法(Method)信息
对类加载器的引用
对Class类的引用
Heap(堆)
所有线程共享
Direct Memory(堆外内存)
JVM基础
常见的实现
Hotspot
Oracle官方 1.8之后开始收费
Jrocit
BEA,曾经号称世界上最快的JVM,被Oracle收购,合并于Hotspot
TaobaoVM
Hotspot深度定制版
LiquidVM
直接针对硬件
azul zing
最新垃圾回收的业界标杆(收费)
jvm与class文件格式
任何语言只要能被编译成class文件,符合虚拟机规范都能丢到jvm上执行
jvm是一种规范,是虚构出来的一台计算机
JVM<JRE<JDK
Class加载-初始化
Loading
ClassLoader
分类
BootstrapClassLoader
是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,
或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
ExtClassLoader
负责加载\lib\ext目录或java. ext. dirs系统变量指定的路径中的所有类库
AppClassLoader
负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。
一般情况,如果我们没有自定义类加载器默认就是用这个加载器
一般情况,如果我们没有自定义类加载器默认就是用这个加载器
自定义ClassLoader
继承abstract class ClassLoader
重写findClass(String name)方法
自定义的ClassLoader的parent属性是AppClassLoader
类加载器范围
System.getProperty("sun.boot.class.path")
System.getProperty("java.ext.dirs")
System.getProperty("java.class.path")
双亲委派机制
自底向上检查该类是否已经加载 -->parent方向(缓存中查找)
自顶向下进行实际查找和加载 -->child方向(实际加载)
双亲委派,主要出于安全来考虑
双亲委派的打破
重写loadClass( )
何时打破
JDK1.2之前,自定义ClassLoader都必须重写loadClass()
热启动,热部署
ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
ClassLoader源码执行过程
loadClass--->findLoadedClass--->parent.loadClass---> findClass ---> defineClass
类装载方式
隐式加载
显示加载
lazyloading
JVM规范并没有规定何时加载
加载时机
在遇到 new、getstatic、putstatic 或者 invokestatic 这四条字节码指令时
子类加载前,先加载父类
虚拟机启动时,被执行的主类必须初始化,比如写有main方法所在的类
在使用 java.lang.reflect 包的方法进行反射调用的时候
执行模式
混合执行
编译执行
解释执行
Linking
Verification
验证文件是否符合JVM规定
Preparation
给类中的静态变量分配内存空间,静态成员变量赋默认值
Resolution
虚拟机将常量池中的符号引用替换成直接引用的过程。
符号引用就理解为一个标识,而直接引用就是直接指向内存中的地址
符号引用就理解为一个标识,而直接引用就是直接指向内存中的地址
Initializing
调用类初始化代码 <clinit>
类的静态变量赋初值值
执行构造方法语句
初始化子类前先初始化父类
GC&GC Tuning
什么是垃圾
没有任何引用指向的一个对象或者多个对象
如何定位垃圾
引用计数(Reference Count)
无法解决循环引用的问题
给对象中添加一个引用计数器,
每当有一个地方引用它时,计数器值加1;
当引用失效时,计数器值减1,引用数量为0的时候,
则说明对象没有被任何引用指向,可以认定是“垃圾”对象
每当有一个地方引用它时,计数器值加1;
当引用失效时,计数器值减1,引用数量为0的时候,
则说明对象没有被任何引用指向,可以认定是“垃圾”对象
根可达算法(Root Searching)
GC Roots 的对象作为根
JVM栈中引用的对象(栈帧中的本地变量表)
方法区中,静态属性引用的对象
方法区中,常量引用的对象
本地方法栈中,JNI(即Native
方法)引用的对象
方法)引用的对象
从根上开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),
当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的
垃圾回收算法
Mark-Sweep 标记清除
扫描2次内存
使用Root Serching找到有用的对象
清除垃圾对象
会产生内存碎片
不适用于Eden区,因为Eden区存活的对象很少,如果采用MS会产生大量的内存碎片
老年代
不需要挪动对象
不存在内存浪费
Copying 拷贝算法
扫描1次内存
将内存分为两个区域from/to,使用Root Serching找到有用的对象,
边找边拷贝B区域,拷贝完成后直接清除A
边找边拷贝B区域,拷贝完成后直接清除A
没有内存碎片
适用于Eden
效率高
需要挪动对象
内存浪费
Mark-Compact 标记压缩
需要扫描2次内存
找到有用的对象
把对象往前移动,移动的过程中,如果是多线程还要考虑线程同步,所以效率较低
没有内存碎片
老年代
需要挪动对象
不存在内存浪费
JVM中的内存分代模型
年轻代
很快被回收的对象
Eden区
对象刚创建
Survivor1(From)
第一轮minorGC时,
会将eden区存活的对象先复制到一个Survivor,
然后删除eden区对象
会将eden区存活的对象先复制到一个Survivor,
然后删除eden区对象
Survivor2(to)
触发下一轮minorGC时,
又把surivior1区和eden区的存活的对象转移到另一个surivior2区,
此时交换surivior2和surivior1,然后清空eden+surivior1
又把surivior1区和eden区的存活的对象转移到另一个surivior2区,
此时交换surivior2和surivior1,然后清空eden+surivior1
老年代(Tenured/Old)
超过指定年龄
参数-XX:MaxTenuringThreshold 配置,默认15
大对象
超过-XX:PretenureSizeThreshold 设置的字节数
动态年龄判断规则进入
Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,
当累积的某个年龄大小超过了survivor区的一半时,
取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值
当累积的某个年龄大小超过了survivor区的一半时,
取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值
分配担保
YGC正在发生时,Survivor区放不下,超过的部分转移到老年代
永久代
指的就是方法区(存放Class元数据),回收条件较苛刻
该类位于堆中的class对象没有任何引用
该类的Class对象的实例对象已经从堆内存被回收
该类ClassLoader已经被回收
常见用的垃圾收集器
Serial
单线程清理
内存非常小的情况<100M
Serial Old
Mark-Compact
Parallel Scavenge(PS)
对比Serial是多线程清理的
吞吐量优先
Parallel Old(PO)
ParNew
对PS做了些增强,以便和CMS配合使用
响应时间优先
CMS(ConcurrentMarkSweep)
诞生的原因就是因为无法忍受STW
响应时间优先
阶段
初始标记(单线程)
STW
并发标记
会标记出大部分的垃圾,此时GC的线程和工作线程同时运行
重新标记(多线程)
由于并发标记时工作线程还在执行,会产生新的垃圾
STW
并发清理
此时依然有工作线程在运行,因此会产生浮动垃圾,这部分的垃圾只能进入下一次CMS再清理
问题
浮动垃圾
用于应付较大内存时碎片过多,从而导致新生代转为老年代找不到空间
触发FullGC时转变成Serial Old采用Mark-Compact整理碎片!一定要避免
标记清除算法
三色标记
黑色
不但自己被标记过,且自己的成员变量所引用的对象也被标记过
灰色
仅自己被标记过,成员变量还未被遍历到
白色
未被标记过的对象
漏标
发生于ConcMarkSweep阶段
两个必要充分条件
1.黑色对象直接指向白色对象
2.灰色对象指向白色对象的引用消失了
漏标问题的解决
Remark阶段
Increament Update
重新遍历堆内存,将黑色对象重新至为灰色对象
G1
分代逻辑
逻辑上分代,物理上不分代
Region
Eden
Survivor
Old
Humongous
基本概念
CardTable
CSet(Collection)
RSet(RememberedSet)
新老年代比例
G1进行GC时候会记录每次STW的时间,根据这个时间,会自行动态调整新老年代的比例大小。
关于G1产生的FullGC
在jdk10之前,G1的FullGC采用的单线程回收的,效率非常低,因此要避免
如何避免FullGC
加大内存
Cpu升级
降低MixGC的阈值
MixGC(相当于一套CMS)
初始标记(STW)
并发标记
最终标记(STW)
筛选回收(STW)
可以通过参数设定降低触发MixGC阈值(默认是45%)
-XX:InitiatingHeapOccupacyPercent
算法
标记压缩算法
三色标记
黑色
不但自己被标记过,且自己的成员变量所引用的对象也被标记过
灰色
仅自己被标记过,成员变量还未被遍历到
白色
为被标记过的对象
漏标
发生于ConcMarkSweep阶段
两个必要充分条件
1.黑色对象直接指向白色对象
2.灰色对象指向白色对象的引用消失了
漏标问题的解决
SATB(Snapshot At The Begining)
Remark阶段解决
将灰色对象指向白色对象消失的引用push到GC线程堆栈中
重新扫描时先扫描GC堆栈,可以通过记录的引用重新定位到白色对象
再结合每个Region中保存的RSet,对比,RSet中仍然有引用指向白色对象,该对象就是存活对象,否则就是垃圾
为什么使用SATB
因为可以配合RSet,Remark阶段不用扫描整个堆内存,就可以快速定位到被漏标的对象
ColoredPointer(颜色指针)
引用指针(ClassPointer)的大小在不打开指针压缩的情况下是64bit,G1将其中的3位记录指针颜色的变化
ZGC
Shenandoah
Eplison
0 条评论
下一页