JVM
jvm有两个运行模式 server和client模式
JVM基础知识
jvm和java无关
查看 binep
javap -v class文件路径
jclasslib
jvm method code 指令集
https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.aconst_null
对class文件有过深入研究,了解class文件的详细信息。
class类加载过程
1.loding
类加载器 classloader
●bootstrap C++
●extension 扩展jar包
●app classpath
●customClassLoader 自定义
双亲委派 保证类加载安全
父加载器是parent对象的classloader
主要保证jvm类加载安全,其次直接找缓存提升效率
jdk历史上有几次破坏双亲委派机制?
https://zhuanlan.zhihu.com/p/372730404
自定义类加载器
ClassLoader -> findClass() 设计模式:模板方法 懒加载需要才加载
自定义类加载器的方式:
1继承classloader
2重写findclass()
3defineClass (byte[]-> Class clazz) 加密实现自定义class
混合执行 编译执行 解析执行
1 检测热点代码 -XX:CompileThreshold=10_000
2打破双亲委派机制 重写loadClass()
2.linking
a verification 校验是否符合JVM规范
b preparation class静态变量赋默认值
cresolution 解析 将符号解析为指针,直接引用
3.initializing
静态变量附初始值
* JVM并没有规定什么时候加载* 但是严格规定了什么时候初始化loadClass->默认值->初始值
new ->申请内存空间->默认值->初始值
单例 DCL(双重检查) 使用volatile 与 new对象有关
JIT指令重排
0 new #2 <com/zzz/test01/Test>
3 dup
4 invokespecial #3 <com/zzz/test01/Test.<init> : ()V>
7 astore_1
8 return
4和7的顺序有可能会发生指令重排导致 未new完成导致另一个线程拿到默认值的对象
java的内存模型 jmm
1.硬件
现代CPU的数据一致性实现是通过 缓存锁(MESI等)+总线锁
1.MESI cache 缓存一致性 (缓存锁)
Modified EXclusive Shared Invalid
2.CacheLine 缓存行对齐概念 伪共享
多数是64字节
伪共享: Cpu一次load了多个内容(a,b) 一个cpu 修改a的时候,会使整个缓存行(64字节)失效,导致另一个cpu在缓存中的b也随之失效,导致重新load
使用缓存行对齐
2.乱序
CPU为了提高指令执行效率,会在一条指令执行过程中,去执行另一条与上条无关的指令。
wcbuff 合并写
如何保证有序
内存屏障
load store
volatile 有序
volatile的实现
1.字节码层面
ACC_volatile
2.JVM层面
volatile内存读写都加屏障 load store
3.硬件层面
hsdis 虚拟机反汇编
Windows lock 指令
synchronized的实现
1.字节码层面
ACC_SYNCHRONIZED
monitorenter monitorexit
2.JVM层面
C/C++ 调用操作系统提供的同步机制
3.硬件层面
x86 lock comxchg
观察虚拟机配置
C:\>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266325824 -XX:MaxHeapSize=4261213184 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
1.普通对象
1对象头: markWord 8
2ClassPointer 指针 :-XX:+UseCompressedClassPointers 为4字节 不开启为8字节
3实例数据
1.引用类型 -XX:+UseCompressedOops 为4字节 不开启为8字节
4padding对齐 8的倍数
2.数组对象
1对象头: markword 8
2ClassPointer 指针
3数组长度: 4字节
4数组数据
5对齐8的倍数
private static class P { //8 markword //4 class pointer 开启压缩4字节 不开8字节 int id; //4 String name; //4 引用类型 默认64位的 由于oops开启 占4字节 int age; //4 byte b1; // 1 byte b2; // 1 Object o; // 4 byte b3; // 1 }
3.对象头包含信息
32bit
1没有重写过hashCode ->31位hashCode System.identityHashCode( )
当一个对象已经计算过identityHashCode 无法进入偏向锁状态
2为什么GC年龄默认是15?
因为分代年龄只有4位 2的4次方 范围是0-15
4.对象定位
1句柄池
2直接指针 Hotspot
Java运行时数据区和常用指令
PC 程序计数器 program count
每一个jvm虚拟机线程都有自己的pc
JVM stacks
每个线程一个栈 每个方法一个栈帧 frame
每一个jvm线程都有独有的jvm栈
栈帧frame
每个方法对应一个栈帧
1.local variable table 局部变量表
2.opreand stack 操作数栈
3.dynamic Linking 常量池的符号链接
4.return address 方法的返回值地址
Heap
jvm所有的线程共享一个堆
Native method stacks
Java本地方法 C/C++ JNI
Direct Memory 直接内存
jdk1.4 之后 新增
直接访问内核空间 NIO zero cpoy
Method area 方法区
运行时常量池
方法区共享在所有的jvm线程中
存放Class的结构
1perm space <1.8 字符串常量位于permSpace FGC不会清理
2meta space >=1.8 字符串常量位于堆 会触发FGC 清理
问题:
1.为什么每个线程独有PC?
CPU切换,线程记录执行记录
看代码 读原理
// 为什么 i 输出是8?
public class T01_Test { public static void main(String[] args) { int i = 8; //对应0 2 指令 i = i++; //对应3 4 7 指令 }}
0 bipush 8 // 将8作为byte,扩展为int 压栈
2 istore_1 // 出栈 放入局部变量表index为1的地址
3 iload_1 // 把局部变量表index=1 的值 压栈
4 iinc 1 by 1 // 局部变量表 index=1 的值+1
7 istore_1 // 出栈 放入局部变量表index为1的地址
8 return
int i = 128;
bipush 1<<8 [-128,127 ]在此区间内
sipush 1<<8 不在此区间
public void m2(int k) { int i=300;}
sipush 300
istore_2 // 非static 局部变量表index=0 存在this
public void add(int a, int b) { int c = a + b;}
0 iload_1 //局部变量表 index=1 压栈
1 iload_2 //局部变量表 index=2 压栈
2 iadd 两数相加
3 istore_3 // 操作数栈顶的值int必须是类型,它从操作数堆栈中弹出,并将 < n > 处的局部变量的值设置为value
public static void main(String[] args) { Hello_02 h1 = new Hello_02(); h1.m1();}public void m1() { int i = 200;}
0 new #2 <com/zzz/test01/Hello_02>
3 dup
4 invokespecial #3 <com/zzz/test01/Hello_02.<init> : ()V>
7 astore_1
8 aload_1
9 invokevirtual #4 <com/zzz/test01/Hello_02.m1 : ()V>
12 return
aload 将局部变量表压入操作数栈
astore 它从操作数堆栈中弹出,并将 < n > 处的局部变量的值设置为value
GC
GC垃圾寻找算法、 1.引用计数 很难解决循环引用 2.根可达算法 哪些是根: 1.线程栈变量 2.静态变量 3.常量池 4.JNI指针 GC垃圾清除算法1.标记清除适用于存活对象比较多的情况下需要两次扫描 1标记2清除 容易产生碎片2.拷贝内存一份为二,将可回收与不可收回区分 适用于存活对象比较少的情况,扫描一次 没有碎片空间浪费、需要复制移动对象,调整对象引用地址。3.压缩扫描两次、需要移动对象不会产生碎片、方便对象分配 JVM分代模型G1属于逻辑分代 其他的属于逻辑+物理分代 新生代 1 拷贝算法eden survivor(幸存者区) survivor 8:1:1MinorGC /YGC 老年代 2 标记清除 或 压缩算法MajorGC /FullGC 一个对象从出生到消亡的过程对象产生-> 栈-> eden-> s1-> s2-> old JVM 调优 1.栈上分配 (无需调整)线程私有小对象 无逃逸 -(只在一段代码中使用)标量替换 -(使用普通类型代替) 2.线程本地分配TLAB (无需调整)占用eden 1%默认线程独有空间小对象 -XX:-DoEscapeAnalysis // 逃逸分析-XX:-EliminateAllocations //标量替换-XX:-UseTLAB // 线程本地分配 对象什么时候进入老年代-XX:MaxTenuringThreshold 与 对象头 MarkWord CG大小4位有关系CMS=6动态年龄:当 Eden+s1->cpoy->s2 时,如果超过S2的50%时,年龄最大的直接进入老年代 如何确定一个对象存放的是栈还是堆?
2.年轻代和老年代的对比 1:2 1.8 JDKjava -XX:+PrintFlagsFinal -version|grep NewRatio
分配担保YGC期间,当survivor区不够,空间担保直接进入老年代 常见的垃圾回收器1.Serial/Serial Old2.Parallel Scavenge/Parallel Old3.ParNew/CMS 演进: JDK的诞生Serial追随,为了提高效率,诞生了PS。 为了配合CMS 诞生了PN。 CMS是JDK1.4后期引入 是里程碑式的。 CMS问题较多,目前没有JDK版本默认使用CMS。 并发垃圾回收是因为无法忍受STW 开启了并发垃圾回收STW (stop the world) safe point1.Serial/Old 单线程清理垃圾 清理算法:mark sweep / compact2.Parallel Scavenge/Old (PS+PO) 多线程清理垃圾 清理算法:copy / compact3.ParNew(ParallelNew) PS的增强 清理算法:copy 能够搭配CMS 4.CMS (concurrent mark sweep)从线程角度:CMS四个阶段1.初始标记 STW 2.并发标记3.重新标记 STW4.并发清理 CMS的问题1.内存碎片 memory fragmentation2.浮动垃圾 floating garbage 解决方案:降低触发CMS的阈值 -XX:CMSInitiatingOccupancyFraction 92% 92%的时候产生FGC,让CMS保持老年代足够的空间-- jvm调优 调优之前的概念: 1.吞吐量:用户代码时间/用户代码执行时间+垃圾回收时间 2.响应时间:STW越短, 响应时间越好 所谓调优 即追求 吞吐量/响应时间 优先 吞吐量优先:PS+PO 响应时间:G1 CMS+PreNew 调优从业务场景开始 没有业务场景的调优都是耍流氓无监控(压力测试,能看到结果)不调优步骤:1.熟悉业务场景(没有最好的垃圾回收器,只有最适合的垃圾回收器) a.响应时间、停顿时间(CMS G1 ZGC) (需要给用户响应) b.吞吐量=用户时间/(用户时间+GC时间)[PS]2.选择回收器组合3.计算内存需求4.选择CPU(越高越好)5.设定年代大小,升级年龄