Java虚拟机笔记
一、总括
-
基于栈的指令集架构(类似RISC),只有PC寄存器,好处在于对硬件的要求低,对标嵌入式类的指令集
注:有一点记错了,RISC架构的寄存器少,寻址方少,指令集小,方便做流水线;CISC架构的寄存器多,寻址方式多,指令集大,不方便做流水线
-
调优的几个方面:代码层面,内存层面(垃圾回收)
-
Java编译器
- 前端编译器(JDK中的javac):Java源代码 => .class文件
- 后端编译器:虚拟机解释.class文件并运行
-
安卓使用不同的Dalvik虚拟机,但是能解释并运行Java源代码 => ART虚拟机(可以将.class文件转为.dex文件)
-
汇编语言到机器码:回忆计组,汇编指令和二进制的机器码可以一一对应
-
对
.class
文件查看(类似机器码,给虚拟机运行,故用二进制而不是文本形式表达,但可以反汇编生成类似汇编语言的文本文件)javap -c
:反编译生成汇编代码javap -v Test.class
:反编译生成汇编代码,会输出行号,本地变量表信息,反编译汇编代码,常量池信息- 使用idea中的Bytecode直接查看源码
- 在Idea中先进行build,然后使用
show bytecode with jclasslib
查看指令对应的字节码
-
翻译机:解释器(逐行翻译),JIT即时编译器(Just In Time,热点代码的翻译缓存,需要增加启动时间)
-
hotspot:具体指热点代码探测技术
-
Java不同版本新特性
- 语法层面
- api层面
- 底层优化:GC
二、类加载子系统
-
ClassLoader:加载Class类到方法区(JDK7之前叫永久代,之后叫元数据),在堆中生成对应的
java.lang.class
对象注:static修饰的类变量存放在方法区
-
类加载
- 验证(verify):魔数
0xCAFEBABE
- 准备(prepare):内存分配相关的初始化,有对字节码文件的attribute的解析(如
static final
修饰的基本类型变量+字符串常量) - 解析(resolve):符号引用 => 直接引用
- 验证(verify):魔数
-
由Jclasslib可知,在<clinit>静态变量的初始化,包括定义时对静态变量的初始化,static块中的初始化
- 按顺序执行
- 只执行一次
注:静态变量的初始化过程包括prepare => <clinit>两个阶段
-
类加载器
- bootstrap类加载器:用
c/c++
写,加载核心包 - ext类加载器:扩展包加载器
- app类加载器
- bootstrap类加载器:用
-
双亲委派机制 :一种任务委派模式
- 避免类的重复加载:由于Java的核心包需要用BootstrpClassLoader(c/c++实现)加载,非核心包用继承了ClassLoader抽象类的类加载,如果不做委派,可能会实现重复加载
- 保护程序安全:BootstrpClassLoader只会加载
JAVA_HOME
下的包,可以防止某些重要类(如:java.lang.String)被替换
-
沙箱安全机制:BootstrpClassLoader只能加载特定位置的核心类,且不会运行main方法(反例:重写String类,添加main方法执行破坏性作业)
-
判定两个class对象为同一个类
- 同样的全类名
- 同样的类加载器(jvm会有做记录)
-
主动加载与被动加载的区别:是否会导致类的初始化(从方法区/元空间去到堆中,
被调用)
三、Java内存分布
-
运行时数据区
- 线程共享:方法区(类的信息,如常量,是堆外空间),堆
- 线程不共享:方法栈区(栈帧),本地方法栈,程序计数器
-
方法区(规范) => 永久代/元空间(实现):在Java7之前称为永久代,受到堆内存上限的限制;在Java8之后称为元空间,使用本地内存,受到本地内存(物理内存,非虚拟机内存)的限制
原因:减少GC次数
-
PC寄存器:记录当前线程执行到的地址
-
内存中的堆与栈
- 堆:存放对象
- 栈:局部变量等,如果是对象,则存放对应的指针
-
栈与栈帧
- 一个线程对应一个栈
- 一个方法对应一个栈帧,一个栈由多个栈帧构成
-
方法栈帧内容:局部变量,操作数栈,动态链接(通过
ctrl+F
搜索具体定义),返回值 -
常见栈错误
-
方法调用过多导致栈内栈帧过多:StackOverFlow
-
线程调用过多导致栈过多:OutOfMemoryError
-
-
局部变量表:
-
是一个数字型数组(char表示为ascii码,Boolean表示为0和非0,引用表示为地址)
- 容量在编译期确定下来
- static修饰的方法无法调用非static变量:方法中无this局部变量
注:局部变量也是垃圾回收的根节点之一
-
-
方法内的code字节码
- LineNumberTable:[line number => start pc:源代码行号 => 字节码指令的行号]
- LocalVariableTable:
- 作用域:start PC + Length
-
slot
- slot对应变量
- 32位对应一个槽,64位对应两个槽
- 槽位复用:当某些局部变量被限制在方法中的
{}
中,可以提前回收
-
操作数栈:
- 在方法的栈帧中,存放计算过程的中间结果,由执行引擎操作,具体过程可以参考逆波兰表达式
- 最大深度在编译期定义好
- 一个栈单位为32bit
- 只能采用出栈和入栈的方式操作
字节码操作(主要观察test方法的运行)
public void test(){ int i = getK(); int j = 10; } public int getK(){ int m = 10; int n = 10; int k = m + n; return k; }
-
非静态函数的
aload_0
通常是向槽内初始化load对象的this指针 -
方法调用完成后返回值会存放在操作数栈中(返回值会被压入调用者方法帧中的操作数栈)
-
变量的赋值也要需要通过操作数栈完成
-
栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中
-
动态链接:指向运行时常量池的方法引用
-
符号引用转为直接引用
- 静态链接:编译期可知
- 动态链接:编译期不可知
-
非虚方法:静态(invokeStatic),私有,实例构造器,显式父类方法——
super.
形式调用(invokeSpecial),final(invokeVirtual,但不允许被重写)虚方法:其它普通方法(invokeVirtual),接口(invokeInterface)
注:invokeDynamic,需结合lambda表达式使用,用于支持动态语言
-
虚方法查找:逐层向上级查找
虚方法表:方法 => 对象(本类有则直接指向自己,否则指向拥有方法的父类)
-
异常处理:用异常表跳
-
本地方法栈:用于本地方法,对应各线程的栈区
仅hotspot中,将本地方法栈和虚拟机栈合二为一
-
线程私有缓冲区(TLAB,Thread Local Allocation Buffer):堆中线程私有缓冲区,用于并发
-
有少部分对象不是在堆上分配
-
栈上分配
-
前提:没有发生逃逸,即对象的作用于仅在方法内
-
code
public String createString(){ StringBuffer sb = new StringBuffer(); //...对字符进行操作 return sb.toString(); //可以看到sb的作用于仅在方法内 }
-
注:HotSpot默认开启了逃逸分析
-
-
标量替换
- 前提:未发生逃逸
- 标量:无法再分解的数据,如Java中的原始数据类型
- 对于一个在栈上分配的对象,本来要构造一整个对象,现在可以只构造对象中基本数据标量
注:以上优化不是发生在编译后的字节码中,而是发生在虚拟机执行过程中
-
-
堆空间细分
- JDK7:新生区,老年区,永久代(与堆连续的空间)
- JDK8:新生区,老年区,元空间(与堆不连续的本地内存空间,但字符串常量池保留在堆中)
注:原因在于方法区(永久代,元空间)大小难以确定
-
堆区细分
- 年轻代:Eden(伊甸园,包括TLAB ),survivor0,survivor1
- 老年代
- 字符串常量池
注:
- JDK计算堆空间时survivor0/survivor1只计算一个,因为另一个用于换入换出
- minor gc只发生在Eden区满的时候,谁空谁是to
- Eden => 老年代
- 大对象(SystemGc后Eden区还是放不下)
- 活得久的对象
- 动态
-
常见回收
- Minor Gc:只堆新生代回收
- 触发:Eden区满
- Stop The World
- Major Gc:只对老年代回收
- Mixed Gc:新生代+部分老年代
- Full Gc:包括方法区
关系:
- MinorGc前会检查老年代最大连续空间是否大于新生代当前对象空间
- 如果大于,表示安全(新生代对象都可以进入老年代),进行MinorGc
- 如果小于,会检查一次老年代最大连续空间是否大于之前晋升的平均大小
- 若大于,则MinorGc
- 若小于,则FullGc
- Minor Gc:只堆新生代回收
-
线程安全
- TLAB
- 加锁
-
堆方法区中同一个对象的访问是需要同步的,排他访问
-
方法区
- 类信息:常量,静态常量
- 常量池(字符串常量池在堆中)
-
static
变量与static final
变量-
code
package com.exp.chatgpt.entity; public class TestEntitiy { public static int test = 1; public static final int testFinal = 1; }
-
字节码
-
verify
-
prepare
public static int test; descriptor: I //非final变量在这个阶段只设置默认值 flags: ACC_PUBLIC, ACC_STATIC public static final int testFinal; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 1 //可以看到final变量在这个阶段置初值
-
resolve
: 0: iconst_1 1: putstatic #2 // Field test:I 4: return
注:可以看到test在这个阶段附初值
-
-
-
常量池:一张表,用于虚拟机指令找到要执行的类
-
方法区回收:常量池,不再用的类 => 提高回收效率
-
就实现而言,静态变量的引用在堆中的反射Class对象中(堆中)
-
常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
-
对象的实例化
-
属性赋值
- 类属性(静态)
- 非final:
- prepare阶段:赋默认值
- 初始化阶段:调用/
赋初值
- final
- 基本数据类型或字符串类型:在prepare阶段直接赋初值
- 其它对象类型或与方法调用有关(如通过Random中的方法初始化):在初始化阶段调用
赋初值
- 非final:
- 对象属性(非静态)
- 默认初始化
- 构造器中初始化(包括了final+显式初始化)
- 类属性(静态)
-
对象的内存布局:对象头(运行时元数据,类型指针),字段,对齐填充
-
OOM错误:栈溢出,堆溢出,直接内存(方法区)溢出
-
Java是半解释半编译型语言
- 解释:最一开始的定位
- 编译:对应及时编译器,可以直接生成本地代码
-
高级语言通过编译翻译成汇编语言,汇编语言还必须通过汇编翻译成机器指令
-
热点代码:执行频率较高的代码 => 热度衰减
- 方法调用计数器:方法调用次数
- 回边计数器:循环次数
-
JIT编译器
- client端:c1编译器,普通优化
- server端:c2编译器,激进优化,包括逃逸分析,栈上分配等
-
aot(Ahead Of Time)编译器:提前编译为
.so
,破坏了链接过程的动态性 -
String
- 1.8之前:使用char[]
- 1.9开始:使用byte[],减少空间
注:基本字符都是Final不可变的
-
StringPool通过HashTable实现,底层是一个链表数组
-
字符串拼接
- 字符串常量池中不会存在相同的内容
- StringBuilder拼接的结果最终会调用
new String()
放在堆中 - 但是StringBuilder.intern()的结果可以放在字符串常量池中,并返回地址
注:
String s1 = "a" + "b" + "c";
在编译期将被优化为String s2 = "abc"
- 如果拼接符号的前后有变量,显然不太好提前优化,故实际是调用了
new StringBuilder().append()...toString()
-
String与StringBuilder拼接效率比较
-
code
String temp1 = ""; for(int i=0;i<1000;i++){ temp1 += i; } StringBuilder temp2 = new StringBuilder(""); for(int i=0;i<1000;i++){ temp2.append(i); }
-
后者更快的原因
- 前者在拼接过程中需要不断构造
StringBuilder
对象 - 前者在实际的append之后调用了
toString(); //其实还是new String()
,在堆中构造了大量的中间对象
- 前者在拼接过程中需要不断构造
-
优化:为避免频繁扩容,可以一开始就在StringBuilder中确定较大capacity
-
注:StringBuffer线程安全
-
-
intern()方法(堆字符串常量池操作 )
- 没有创建
- 有就返回
-
关于String构造
- 两种基本方法的不同含义
String test = "test";
:常量池String test = new String("test");
:堆
- 在常量池中构造对象的不同方法
String test = "test"
.intern()
- 两种基本方法的不同含义
-
字符串构造
-
new String("test")
:-
现在常量池中需要有一个基本的常量对象
test
,用于虚拟机表达基本的字符注:这个基本指的是不包含变量字符串拼接的对象。因为对于常量的拼接的字符串,前端编辑器可以优化为一个字符串常量,即常量池中一个基本常量对象;对于包含变量的字符串拼接,可以视为又多个字符串中的基本常量对象组合而成
-
其次通过这个基本的常量对象,在堆中构建new的对象,指向字符串常量池中的对象
-
-
new String("a") + new String("b")
- 基本同上,共构建了6个对象
- 最后的
toString()
是在堆中构建对象,不会在常量池中构建对象
-
-
new String(a+b).intern()
- jdk6:
intern()
在常量池中创建了一个对象 - jdk7/jdk8:
intern()
在常量池中创建了一个指针指向堆中的new String()
,更加节省了空间
- jdk6:
-
8种基本类型都有常量池
-
字符串:少用new,多有intern()字符串常量池
三、垃圾收集器
-
客户端与服务端虚拟机的不同需求
- 客户端:低延迟
- 服务端:吞吐量
-
目标(优先级自高向低):
- 降低OOM可能性(能跑)
- 减少Gc时间(高吞吐)
- 减低延迟(低延迟)
-
CMS需要了解
-
GC两个阶段
- 标记:引用计数,可达性分析
- 清除:标记-清除算法,复制算法,标记压缩算法
-
引用计数算法
额外属性:引用计数器
存在问题:无法解决循环引用问题
- 开始:
...=> A => B => C => B
,显然B身上的计数为2 - 而后:
B => C => B
,显然由于存在循环引用,B身上的计数为1导致无法回收
使用:Python
- 开始:
-
可达性分析算法(根搜索算法)
-
基本算法:搜索被根对象集合所连接的目标对象是否可达
-
根对象(GcRoots)
- 栈中对象(局部变量表中,包括参数,局部变量)
- 本地方法栈中对象
- 类静态属性引用的对象
- 字符串常量池
- 同步锁synchronized对象
- 虚拟机内部引用,如:类加载器
- 分代手机和局部回收的一些“临时性”对象:如某些新生代对象被老年代对象引用,那么老年代应该作为GcRoot
注:主要被栈中指向堆的指针
-
-
finalize():由低等级的Finalizer线程调用,尽量不要主动调用
对象的三种状态:
- 可触及
- 可复活:由finalize()方法触发,且只会触发一次。如果与引用链上任一对象搭上关系(通常为静态变量)则可复活
- 不可触及
通过finalize()方法复活举例
-
code
public class TestEntitiy { public static Object obj = null; public static void main(String[] args) throws InterruptedException { obj = new TestEntitiy(); obj = null; System.gc(); Thread.sleep(2000); //等待低优先级的finalizer线程执行 if(obj!=null){ System.out.println("obj活着"); }else{ System.out.println("obj死了"); } obj = null; System.gc(); Thread.sleep(2000); if(obj!=null){ System.out.println("obj活着"); }else{ System.out.println("obj死了"); } } @Override protected void finalize() throws Throwable { System.out.println("finalize方法调用"); obj = this; //将当前对象和类静态变量搭上关系 } }
-
result
finalize方法调用 obj活着 obj死了
-
标记-清除算法:需要stw,具体分为两步
- 标记:递归遍历根节点,标记可达对象
- 清除:线性遍历所有对象,清除未被标记的对象
缺点:产生内存碎片
-
复制算法(不需要标记)
通过复制销毁数据,好处:
- 可以不用去管垃圾对象
- 复制过来的空间是连续的
应用:垃圾多,存活对象少的场合,复制少,适合新生代
-
标记-压缩算法
- 标记
- 压缩:双指针法内部整理
标记-清除 => 压缩
应用:能整理空间且不需要分割对象,适合老年代
-
区别
- 标记-清除算法:不需要移动,不需要额外空间(有碎片),需要标记效率一般
- 复制算法:不需要标记效率快,需要整理,需要额外空间
- 标记-整理算法: 不需要额外空间,不需要额外空间(无碎片),需要标记和整理效率慢(需要STW)
-
分代收集算法
- 新生代:复制算法
- 老年代:标记-清除,标记-整理算法
-
补充
- 增量收集算法:并发执行,降低STW时间
- 分区算法:新生代,老年代等空间是非连续的小区间
-
system.gc()
:不保证一定执行,会产生STWSystem.runFinalization();
:通过源码尽力调用引用的对象的finalize()
方法 -
OOM错误:没有空闲内存,且垃圾收集器FullGC后也无法提供更多内存
-
内存泄漏:对象不会再被程序用到,但是GC又不能回收
- 单例模式下:该单例对象关联到外部对象,导致外部对象无法及时回收
- 一些需要close的资源(数据库,网络,IO等)未关闭:无法做到及时回收
-
串行(serial)与并行(parallel)
-
安全点
- 含义:特定的GC位置,需要所有线程都在安全点才会GC
- 位置:方法调用,循环跳转,异常跳转
- 方式:抢占式中断,主动式中断(线程主动挂起)
-
安全区域:安全点的扩展
-
引用
-
强引用:
- 概念:99%场合下的使用,gc不会主动回收
- 使用:
Object obj = new Object()
-
软引用
-
概念:根节点可达,内存不足即回收
-
使用:
//vmOption:-Xms10m -Xmx10m -XX:+PrintGCDetails public static void main(String[] args) { byte[] o = new byte[1024*1024*5]; //直接放在老年代 SoftReference<byte[]> softReference = new SoftReference<>(o); o = null; //置空 byte[] o1 = new byte[1024*1024*5]; System.out.println(softReference.get()); //返回Null,说明已被回收 }
-
应用:MyBatis中的高速缓存(缓存对象:有空间则存在提高效率,没空间则删除增加可用空间)
-
-
弱引用
- 概念:下一次垃圾回收之前
- 使用
- 创建:
WeakReference<Object> wr = new WeakReference<Object>(obj);
- 使用:
wr.get()
- 创建:
- 应用:
ThreadLocal
-
虚引用
-
概念:用于对象回收的跟踪 => 从而进行更深层次的清理
-
与Finalization的区别:真正地感知到对象的消亡
-
code
public class TestEntitiy { public static ReferenceQueue<TestEntitiy> phantomQueue = null; public static void finalClear(){ while(true){ Reference<? extends TestEntitiy> reference = null; if(phantomQueue!=null){ //追踪到了 try { reference = phantomQueue.remove(); } catch (InterruptedException e) { throw new RuntimeException(e); } if(reference!=null){ System.out.println("已通过线程追踪到回收"); } } } } public static void main(String[] args) throws InterruptedException { TestEntitiy obj = new TestEntitiy(); phantomQueue = new ReferenceQueue<TestEntitiy>(); PhantomReference<TestEntitiy> objectPhantomReference = new PhantomReference<TestEntitiy>(obj, phantomQueue); obj = null; System.gc(); new Runnable(){ @Override public void run() { finalClear(); } }.run(); Thread.sleep(10000); } } //result:已通过线程追踪到回收
-
-
-
终结期引用:实现对象的Finalize()方法
-
gc性能指标:暂停时间STW,吞吐量
通常情况下二者的关联为:
- 一次暂停时间高 => 频率低 => 单位时间吞吐量低
- 一次暂停时间低 => 频率高 => 单位时间吞吐量高
目前优化方向:吞吐量优先,尽量减少暂停时间(所有GC都有STW,只能尽量缩短)
-
七款垃圾收集器
- 串行回收器: Serial,Serial Old
- 并行回收器:ParNew,Parallel Scavenge,Parallel Old
- 并发回收器:CMS,G1
- 黑线:可以组合
- 绿线:Deprecated
- 红线:组合关系已被移除
注:JDK8默认使用
Parallel Scavenge Gc
和Parallel Old Gc
-
Serial收集器:(新生代)复制算法,串行回收 => 适用于client模式
-
Serial Old收集器:(老年代)标记压缩算法
-
ParNew(New指新生代)收集器:(新生代)可以看做是Serial收集器的多线程版本
注:线程数默认与CPU数相同
-
Parallel Scavenge:吞吐量优先,有自适应调节策略
参数设置
- 线程数:
-XX:ParallelGCThreads
- 最大停顿时间(STW,谨慎设置):
-XX:MaxGCPauseMillis
- 垃圾回收时间/总时间:
-XX:GCTimeRatio
- 其它堆大小参数设置...
- 线程数:
-
Parallel Old:搭配Parallel Scavenge使用,注重吞吐量
-
CMS回收器(Concurrent-Mark-Sweep):(老年代)标记清除算法,主打低延迟,搭配ParNew使用
- 过程:初始标记(标记GCRoots直接关联到的对象) => 并发标记(遍历整个对象图) => 重新标记(STW下对并发标记过程新产生的垃圾对象的修正) => 并发清理 => 重置线程
- 仅初始标记,重新标记需要STW
- 并发清理需要开启新的线程
- 当堆内存使用率达到某一阈值时就开始回收
- 若内存不足,则转而使用Serial Old收集器
- 标记清理而不是标记压缩:其它线程还在运行并使用相关地址
- 参数设置
-XX
:CMSInitiatingOccupanyFraction:设置堆内存使用率。当内存使用率增长较快,应降低该阈值;当内存使用率增长较慢,应提高该阈值- fullGc后的碎片整理
- 缺点
- 内存碎片,无法产生大对象
- 占用线程,降低吞吐量
-
一般性选择
- 低内存和并行开销:SerialGc
- 高吞吐量:ParallelGc
- 低Gc中断或停顿时间:CmsGc
-
G1收集器:基于区域分代思想,希望能在延迟可控的情况下获得尽可能高的吞吐量
基本思想:
- 先将内存分为不同的区域,将这些区域分为不同代
- 优先回收价值最大的Region——可回收的空间大
优点
- 并行与并发兼具:这里的并行与并发有自己独特的含义,并行指STW下多个GC线程一起跑,并发指GC线程和用户线程一起跑
- 先分区,后分代
- Region内部的连续空间采用指针碰撞分配
- 空间整理:复制算法
- 软实时:根据所限定的停顿时间选择回收的空间
- 适合大内存
缺点:没有特别突出的优点
参数设置
- Region大小
- 最大GC停顿时间
替换CMS:
- 超过50%的Java堆中有活动数据
- 对象分配频率,年代提升频率变化很大
- Gc停顿时间过长
分区:Eden,Surivor,old,humongous
注:humongous是一块独立的连续(这里的连续是指当一个对象大于region,那么所构成的多个region是连续的,而非整个humongous都是连续的)空间用于存储大对象(针对CMS中大对象存储在老年期,但可能是存活时间较短的)
过程:
- Eden区用完:minor gc
- 堆内存使用达到45%:minor gc +老年代并发标记
- 第二步完成 => mixed gc
记忆集(Remember Set)
- 每个年轻代的region都有一个记忆集
- 解决问题:
- 对于MixedGc,如果一个老年代对象与新生代对象之间有相互引用的关系是无所谓的,因为MixedGc会同时回收老年代和新生代
- 对于MinorGc,如果一个老年代对象引用了新生代对象(反之:对于新生代对象引用老年代对象是无所谓的),那么想要获得这一关系需要扫描全图,显然这是不现实的
- 作用:记录有老年代对象指向了新生代对象
优化建议(尽量用自身的动态)
- 尽量不要调整年轻代大小
- 暂停时间不要太严苛
-
算法
- 新生代:复制算法
- 老年代:大多为标记整理算法,仅CMS为标记-清除算法(主打低延迟)
- G1为基于region的复制+标记-压缩算法
-
垃圾收集器选择
-
GC日志分析(基于Springboot运行)
-
基本运行参数
-
vmOption:
-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails
- 最小堆空间20m
- 最大堆空间20m
- 新生代10m
- eden区对survivor区比例:
8:1:1
-
code
public class TestEntitiy { private static final int INT_1MB = 1024*1024; //-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails public static void main(String[] args){ //基于Springboot运行 } }
-
result
Heap PSYoungGen total 9216K, used 2930K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) //Parallel Scavenge新生代 总可用空间(Eden+一块survivor), (总可用) [虚拟空间起始位, 虚拟空间已使用位, 虚拟空间结束位) eden space 8192K, 35% used [0x00000000ff600000,0x00000000ff8dc980,0x00000000ffe00000) from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000) Metaspace used 3289K, capacity 4496K, committed 4864K, reserved 1056768K class space used 359K, capacity 388K, committed 512K, reserved 1048576K
注:可以看到,Springboot启动自身带有35%
-
-
增加内存,不回收
-
code
public class TestEntitiy { private static final int INT_1MB = 1024*1024; //-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails public static void main(String[] args){ byte[] arr1 = new byte[1*INT_1MB]; byte[] arr2 = new byte[2*INT_1MB]; } }
-
result
PSYoungGen total 9216K, used 6002K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 73% used [0x00000000ff600000,0x00000000ffbdc960,0x00000000ffe00000) from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000) Metaspace used 3360K, capacity 4496K, committed 4864K, reserved 1056768K class space used 365K, capacity 388K, committed 512K, reserved 1048576K
-
-
增加内存,触发回收
-
code
public class TestEntitiy { private static final int INT_1MB = 1024*1024; //-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails public static void main(String[] args){ byte[] arr1 = new byte[1*INT_1MB]; byte[] arr2 = new byte[2*INT_1MB]; byte[] arr3 = new byte[3*INT_1MB]; } }
-
result
[GC (Allocation Failure) [PSYoungGen: 5838K->1000K(9216K)] 5838K->4211K(19456K), 0.0020686 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 4154K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 38% used [0x00000000ff600000,0x00000000ff914930,0x00000000ffe00000) from space 1024K, 97% used [0x00000000ffe00000,0x00000000ffefa020,0x00000000fff00000) to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) ParOldGen total 10240K, used 3211K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) object space 10240K, 31% used [0x00000000fec00000,0x00000000fef22f88,0x00000000ff600000) Metaspace used 3366K, capacity 4496K, committed 4864K, reserved 1056768K class space used 366K, capacity 388K, committed 512K, reserved 1048576K
注:
- (JDK8)可以看到触发了GC,大对象直接进入了老年区
- 实际上JDK7下是将前面的对象进入老年区,较大对象进入新生代
-
-
-
ZGC:分区不分代
-
触发fullGC的几种情况
- (新生代空间不足 => minorGc)
- 老年代空间不足
- 方法区空间不足(对于其中的字符串常量池对象,按正常的堆对象处理)
- 显式调用
System.gc()
四、Java字节码
-
JDK:前端编译器,虚拟机运行时环境
-
前端编译器:不直接涉及编译优化
-
示例1
-
code
public static void main(String[] args) { Integer x = 5; int y = 5; System.out.println(x == y); }
-
字节码
0 iconst_5 1 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;> 4 astore_1 5 iconst_5 6 istore_2 7 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;> 10 aload_1 11 invokevirtual #4 <java/lang/Integer.intValue : ()I> 14 iload_2 15 if_icmpne 22 (+7) 18 iconst_1 19 goto 23 (+4) 22 iconst_0 23 invokevirtual #5 <java/io/PrintStream.println : (Z)V> 26 return
-
结论
- 数字对象的生成调用了
.valueOf()
方法=>点击方法可知,对于-128~127
之间的数,Java基于享元模式直接返回常量池中同一对象 - 拆箱是通过Integer对象的
.intValue()
方法
- 数字对象的生成调用了
-
-
示例2
-
code
public class TestEntitiy { static class Father{ int x = 0; Father(){ print(); x=10; } public void print(){ System.out.println("father:x=" + x); } } static class Son extends Father{ int x = 20; Son(){ print(); x=30; } public void print(){ System.out.println("son:x=" + x); } } public static void main(String[] args){ Father father = new Son(); System.out.println(father.x); } }
-
result
son:x=0 son:x=20 10
-
对应字节码
0 new #2 <com/exp/chatgpt/entity/TestEntitiy$Son> 3 dup 4 invokespecial #3 <com/exp/chatgpt/entity/TestEntitiy$Son.<init> : ()V> 7 astore_1 8 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;> 11 aload_1 12 getfield #5 <com/exp/chatgpt/entity/TestEntitiy$Father.x : I> //可以看到调用的是父类的x字段 15 invokevirtual #6 <java/io/PrintStream.println : (I)V> 18 return
-
结论
-
属性不存在多态,方法存在多态
从实现的角度看
- 属性的调用是直接在堆空间中找对象位置,父类属性在前,子类属性在后
- 方法的调用是通过this指针,从而实现了多条
-
由于this对象是子类,故调用print()方法是子类的
-
-
-
_info
后缀:表 -
class文件结构
-
官方文档
-
内容
- 魔数
- 版本号
- 常量池(字面量+符号引用,索引值0表示不引用任何一个常量池项目)
- 访问标志符
- 当前类
- 父类
- 接口
- 字段
- 方法
- 属性(指类名等辅助 信息)
-
注
- u4:无符号的四字节
- count(2个字节)+数组:表
-
-
类型
-
字节码文件:符号引用 => 运行时:直接引用
-
常量池
-
个数
-
常量池内容
-
常量Tag
-
常量内容:具体格式与常量Tag相关,如Tag为1的字符串常量格式为[Byte长度+内容]
-
注:主要是各种信息的复用,最终这些信息都是指向Tag为1的字符串
-
-
javac -g xx.java
:生成局部变量表等信息 -
指令集
-
加载与存储指令:数据从栈帧的局部变量表和操作数栈之间的传递
指令:
- 局部变量表 => 操作数栈:load
- 操作数栈 => 局部变量表:store
- 常量 => 操作数栈:const => (bi/si) => ldc
-
算术指令
-
特殊情况
- infinity情况:(整数)0/(浮点数)0.0
- nan情况:(浮点数)0/(浮点数)0.0
具体:详见虚拟机规范
-
取反:与-1的补码全1做异或
-
iinc:不改变操作数栈,对局部变量表的数自增
-
-
创建实例指令:
Object o = new Object();
0 new #2 <java/lang/Object> 3 dup //将栈顶元素赋值一份(用于接下来两个操作各消耗一份) 4 invokespecial #1 <java/lang/Object.<init> : ()V> 7 astore_1
注:需要区分类创建和数组创建,这里仅以类创建为例
-
方法调用指令
- invokevirtual
- invokeinterface:实例
- invokespecial:能确定方法(静态分派,包括构造器,private,
super.
) - invokestatic:也是静态分派
- invokedynamic
-
字段指令:getfield,putfield
-
方法返还指令:先将要返回的值放入操作数栈,然后调用areturn指令
-
栈操作指令:如pop用于栈内东西没有接收
-
条件跳转指令和多条件分支跳转指令
-
条件跳转指令(语法)
case 1 代码块 1 case 2 代码块 2 ...
-
多条件分支跳转指令(对应switch-case语法)
case 1 case 2 ... 代码块 1 代码块 2 ...
注:
- 从形式上看switch-case效率更高
- 对于String类型的switch-case,会先比较HashCode,考虑到Hash碰撞冲突还会进一步通过equals比较
-
-
异常处理:异常表
- throws:在方法下生成Exceptions
- try-catch:在方法的code下生成StackMapTable
-
同步指令
-
方法级同步:sychronized主要体现在flag中,由虚拟机处理(字节码不体现)
-
代码块级同步:
-
code
public class TestEntitiy { static Object o = new Object(); static void test(){ synchronized (o){ } } public static void main(String[] args) { test(); } }
-
字节码
0 getstatic #2 <com/exp/chatgpt/entity/TestEntitiy.o : Ljava/lang/Object;> 3 dup 4 astore_0 5 monitorenter //加锁 6 aload_0 7 monitorexit //正常解锁 8 goto 16 (+8) 11 astore_1 12 aload_0 13 monitorexit //遇到异常解锁 14 aload_1 15 athrow 16 return
-
-
注:
- 大部分数据的处理,如:byte,char,short实际上都会转为int处理,区别在于表示的范围(由于局部变量表的槽为32位)
-
-
常量池在内存中的存放为HashSet的结构(与多条件分支跳转指令有关switch-case)
-
finally是否会影响到try中的return
-
code
public class TestEntitiy { static String test(){ String ans = "hello"; try{ return ans; }finally { ans = "int finally"; } } public static void main(String[] args) { System.out.println(test()); //sout:hello } }
-
字节码
0 ldc #2 <hello> 2 astore_0 3 aload_0 4 astore_1 5 ldc #3 <int finally> 7 astore_0 8 aload_1 9 areturn 10 astore_2 11 ldc #3 <int finally> 13 astore_0 14 aload_2 15 athrow
-
理解
-
对于返回值的处理,是先复制了一份ans中的内容到槽1(hello),最后返回的也是槽1
-
对于Finally的处理,是修改槽0
-
根据StackMapTable,line 10为异常处理
这里看起来比较奇怪的一点是没有Exception出来?
实际上,如果在try中有异常出现,异常会放在当前方法的栈顶,通过line10将其先放在槽2,然后执行FInally中的内容,然后通过athrow继续抛出异常
-
通了
-
-
五、类加载
-
类加载(可以基于这个框架做思维导图TODO)
- 加载
- 链接:验证,准备,解析
- 初始化
- 使用
- 卸载
-
类加载
- 类加载器将类模板加载到元空间中
- 堆中生成Class对象,并指向方法区的类模板
注:
-
数组由JVM直接创建
数组中的元素:
- 基本数据类型:直接创建
- 引用数据类型:递归创建
-
验证:魔数,格式等
-
准备:静态变量分配内存(赋默认值0)
基本数据类型和String类型+
static final
修饰符的特殊之处(对象类型仍旧在clinit中)- 在准备阶段赋值
- 赋的是最终的常量(记录在field的属性中,而不是在clinit中)
-
解析:符号引用 => 直接引用
-
初始化:
-
类初始化方法clinit(静态代码显示赋值+static代码块)
不会生成clinit的情况
- 无静态字段
- 静态字段无显示赋值
- 静态字段final(基本类型+字面量类型)
- 无静态代码块
static变量准备环节赋值:final+非new
上型:从内存的角度看,涉及到仅方法区内部的操作都不需要调用clint,例如基本类型的初始化,对于对象类型,String从逻辑上来说是在方法区内部(实现上来说是在堆内),通过
.class
文件加载生成,故也可以视为不需要设计到堆操作 -
主动加载:会调用clinit方法
被动加载举例:
- 调用
static final
字段(基本变量类型),在准备阶段完成初始化 - 通过类加载器加载(通过
Class.forName
调用属于主动加载)
- 调用
-
-
使用
-
卸载
-
ClassLoader和Class对象相互指向,Class对象又指向方法区中的类模板
类模板回收(即Class对象被回收):由于ClassLoader和CLass对象相互指向,故重点在ClassLoader被回收
想要回收自定义类模板:需要自定义类加载器
-
-
接口初始化检验(
static
+输出)interface inf{ public static final Thread t = new Thread(){ { System.out.println("初始化"); } }; }
六、类加载器
-
功能:将
.class
字节码加载到内存(即类加载的加载阶段——验证阶段之前) -
不同加载器加载的类一定是不同的
自己实现不同的类加载器:指创建不同的类加载器实例,而不是定义不同的类加载器类
-
类加载器层次
-
c++层面
Bootstrap启动类加载器:只加载java,javax,sun开头
-
Java层面(继承ClassLoad类,通过Launcher类初始化 )
- ExtensionClassLoader
- ApplicationClassLoader
- 自定义类加载器
-
-
数组类型的加载器
- 基本数据类型数组:无加载器
- 对象类型数组:与数组元素的类加载器相同
-
双亲委派机制
- 先向上:查看是否已被加载
- 后向下:分派任务
优点:
- 避免重复加载
- 防止核心类被篡改(如果仍由底层ClassLoader加载,那么可以自己写一些类如:重要String类,造成破坏)
缺点:由于顶层加载器无法调用底层加载器加载的类,当顶层的类加载需要依赖底层加载器加载的类时会出问题(如:Service Provider Interface机制)
-
ClassLoader的两种重写方式
- 重写loadClass方法:可以破坏双亲委派机制
- 重写findClass方法:不能破坏双亲委派机制
-
Class.forName()与ClassLoader.loadClass()
Class.forName()
:类方法,主动加载(会调用clinit方法,故常用作数据库驱动加载)ClassLoader.loadClass()
:实例方法,被动加载
-
破坏双亲委派机制
- SPI机制(引入了线程上下文加载器)
- 代码热替换(独立线程不断尝试加载最新的
.class
文件),热部署(OSGI )
-
沙箱:隔离,限制资源访问
-
自定义加载类
- 隔离类加载(如一个Tomcat下可以加载多个不同的包,即时这些包的命名是有冲突的)
- 扩展加载源(数据库,网络等)
- 源码加密解密
-
JDK9对类加载器结构修改较大(模块化)
- BootClassLoader
- 不同模块用不同类加载器加载
七、调优
-
调优目的
- 防止,解决OOM
- 减少FullGc出现的频率
-
步骤
- 发现问题:监控(被动)
- 排查问题:分析(主动)
- 解决问题:调优
-
性能评价标准
- 响应时间,STW时间
- 吞吐量
- 并发数
- 内存占用
关系:
- 并发数小 => 吞吐量小,响应时间断
- 并发数较大 => 吞吐量大,响应时间长(理想情况)
- 并发数过大 => 吞吐量小,响应时间长
- web应用主要关注时间,吞吐量
-
JVM命令行监控工具
-
jps(java process status):打印所有Java进程,查看参数,可远程
-
jstat(JVM statistics monitoring tool):监视虚拟机进程的类装载,内存,垃圾手机,JIT编译等数据
格式:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
option:
- 类加载相关:
-class
- JIT相关:
-compiler
/-printCompilation
- 垃圾回收相关:
-gc
等
应用:
- GC时间/启动时间,当该比例大于20%说明堆压力较大
- 老年代使用比例不断上涨 => 可能有内存泄漏
- 类加载相关:
-
jinfo(configuration info for Java):查看,修改(仅被标记为manageable的)虚拟机配置参数
-flags PID
:复制参数-flag 具体参数 PID
:具体参数查看,或者修改
-
java -XX:+PrintFlagsFinal
:输出最终运行时所有参数 -
jmap(JVM Memory Map):内存快照,基于安全点机制
-
-dump
:堆转储快照(所有对象,类,GCRoots,线程栈和局部变量),二进制文件,需要使用特定方式打开使用:
jmap -dump:live,format=b,file=文件名.hprof PID
(live表示只显示活的对象) -
-heap
:堆空间的详细信息 -
-histo
:对象创建信息
注:
-XX:+HeapDumpOnOutOfMemoryError
,则在发生OOM的情况下导出dump文件(通常在一次FullGc后),通过-XX:HeapDumpPath=文件名.hprof
-
-
jhat(JVM Heap Analysis Tool):以网页的形式分析并呈现堆转储文件(在JDK9中被VisualVM取代),了解即可
-
jstack(JVM Stack Trace):线程快照(重点关注死锁Deadlock,等待资源Waiting on condition,等待获取监视器Waiting on monitor entry,阻塞Blocked)
-
jcmd:指令集合(除jstat外),通过
jcmd pid help
列出进程支持的所有命令 -
jstatd:作为代理服务器建立本地计算机与远程监控工具的通信
-
-
JVM图形GUI监控工具
三个JDk自带工具:
- jconsole:简单,了解即可
- visualVm:常用,采样器(CPU——方法或线程执行时间,内存采样)
- JMC(Java Mission Control):低开销收集Java虚拟机性能数据
外部工具:
-
MAT:Eclipse工具,优势在于可以看到GCRoot,引用关系
-
histogram:关于类的统计信息 ,浅堆(浅拷贝),深堆(深拷贝,cascade,是对象被回收的真实空间) => 更高一级的概念是对象的实际大小
A浅堆:A
A深堆:A+D
A浅实际大小:A+C+D
-
支配树:基本对象引用关系 => 直接的支配关系
-
-
JProfiler
- 数据采集模式
- Instrumentation重构模式:全功能模式,通常搭配Filter使用
- Sampling采样模式
- 内存
- 优化:根据"所有对象"
- 内存泄漏排查:根据"记录的对象"
- 数据采集模式
-
Arthas:适合生产环境,火焰图(查看方法栈调用时间)
-
JMC(Java Mission Control):官方
-
Tprofiler等
-
JVM参数
-
类型
-
标准参数:
-
开头,稳定,例如-version
-
-X
参数:较稳定,可通过java -X
查看具体可用的参数常见:
-Xss
:栈空间大小(基于虚拟内存)-Xms
:堆空间大小,最小或者说是初始-Xmx
:堆空间大小,最大-Xmn
:堆空间新生代大小-Xloggc:/path/to/gc.log
:GC日志存到文件
-
-XX参数
:不太稳定类型:栈,堆(新老年代比例,新生代中伊甸园区和survivor区比例,新生代到老年代的阈值和年龄,元空间最大大小,直接内存大小),OOM选项
常见:
-XX:+PrintCommandLineFlags
:打印命令行相关参数-XX:NewRatio=2
:新生代与老年代比为1:2-XX:-UseAdaptiveSizePolicy
:设置Eden区与两个Survivor空间比例是否自适应(建议打开,特定于Parallel Scavenge垃圾回收器)-XX:SurvivorRatio=8
:Eden区与两个Survivor空间比例为8:1:1-XX:HeapDumpOnOutOfMemoryError
:在出现OOM之前Dump-XX:OnOutOfMemoryError
:发生OOM时区执行脚本,例如:重启,发送邮件-XX:+PrintGC
-XX:+PrintGCDetails
:打印垃圾回收细节-XX:+PrintGCDateStamps
:实际时间戳-XX:+PrintGCTimeStamps
:相对于开机后的时间戳
-
-
配置
- Tomcat添加配置:catalina.sh
- 运行中配置:
jinfo
,需要manageable标识
-
-
GC日志分析工具
- GCEasy:在线日志分析网站,适合日志较大情况下生产环境
- GCViewer等
-
获取Dump文件
- JVM参数
- jmap工具
- VisualVm可视化工具
- MAT可视化 工具
-
内存泄漏:
-
可达但不需要
-
通常表现为,长生命周期对象保持了对短生命周期对象的引用
-
内存变化
每次内存回收后的内存稳步增长
常见情况:
- 静态变量引用
- 内部类对象被外部类引用
- 数据库,网络 连接等
- HashSet中的对象属性修改导致哈希值不同
- 缓存泄漏 => 建议使用WeakHashMap
-
八、经验
- 暂定包括GC层面,参数层面,监控层面
- FullGc门阀设置
- 当内存增长快速时,可以将门阀设置较低:优先考虑避免OOM
- 当内存增长缓慢时,可以将门阀设置较高:不用担心触发OOM,优先减少fullGc频率
- 当元空间(方法区的实现)大小达到MetaSpaceSize会触发FullGc,未避免频繁的FullGc,应当适当提高该值
- 基本方案
- vmOption
-XX:HeapDumpOnOutOfMemoryError
:OOM时dump-XX:+PrintGCDetail
-XX:+PrintGCDateStamps
:基本时间戳-XX:+PrintGCTimeStamps
:相对于虚拟机运行的时间戳-Xloggc:/path/to/gc.log
:gc日志位置-XX:OnOutOfMemoryError
:OOM时执行的脚本
- 堆内存查看
- 开发环境:visualGc,jprofiler
- 生产环境:Arthas(轻量级)
- gc查看:在线GCViewer
- vmOption
-
静态语言与动态语言
- 静态语言:编程时确定好类型,如:Java
- 动态语言:动态时确定好类型,如:js
-
没认真看:P62多线程调用
-
从逻辑上说字符串常量池属于常量池在方法区中,从实现上说字符串常量池在堆中
-
关于DGTEC-TASK中的几种方案
- 深度拷贝
- 栈上分配
-
绘制一张内存图
-
P156内存回收没看明白
-
补充序列化与反序列化
Serializable:接口,仅作为标识用
序列化:内存对象 => 文件
反序列化:文件 => 内存对象
-
上型:父类统一接口,子类不同实现
-
扩展类:如子类,扩展父类的功能
-
G1堆String去重使用了HashTable => 集合
HashMap
,HashTable
应该完全掌握