1、jvm基础
1.1 什么是jvm?
jvm是一种规范。jvm是一种什么样的规范?
具体实现:hotspot
2 classFileFormat
3:类加载-初始化
3.1、loading 加载class文件到内存中
3.1.1 引起类加载的情况
- new 对象时
- 调用静态属性,静态方法时。(访问static final变量除外 static final修饰的是基本数据类型,或者字符串类型时,会替换为常量。比如有一个类A{static fina int a=10;} 当遇到代码
int a=A.a;
时 。编译器会编译为int a=10;
这样就不会引起A类的类加载 )- java.lang.reflect对类进行反射调用时 Class.forName(),getclassLoader.loadclass()
- 初始化子类时父类先初始化
- 虚拟机启动时被执行的子类首先初始化
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
3.1.2类加载器
- BootstrapClassLoader C++实现 java中get的是null 加载 lib下rt.jar charset.jar等核心类
- ExtClassLoader 加载jre/lib/ext/*.jar
- AppClassLoader 加载classPath下的类
System.out.println(System.getProperty("java.class.path"));
- customClassLoader 自定义类加载器
1、extends ClassLoader
2、overwrite findClass() -> defineClass(byte[] -> Class clazz)
3、加密
4、可以打破双亲委派原则- 双亲委派:类加载器加载一个类,先在缓存里寻找,找不到的话会找父加载器去加载。一次类推直到BootstrapClassLoader也找不到在让子加载器加载。
- 父子关系 Bootstrap->Ext->App->cus
- 注意 父加载器并不是java继承中的父类而是getParent()方法返回的加载器。
3.1.3 load的过程
findInCache -> parent.loadClass -> findClass()【双亲委派】
3.2 Linking
3.2.1 Verification 验证文件是否符合jvm规范
3.2.2 Preparation 给静态属性赋默认值
3.2.3 Resolution 将类、方法、属性等符号引用解析为直接引用 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用: 直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
3.3 Initializing 调用初始化代码给静态成员变量赋初始值。
public class A {
public static void main(String[] args) {
System.out.println(T.count);
System.out.println(TT.count);
}
static class T {
/**
* 类加载先赋默认值再进行初始化
* count=0
* t=null
* count=2
* new T(){count++}
* 最终count=3
* */
static int count = 2;
static T t = new T();
private T() {
count++;
}
}
static class TT {
/**
* 类加载先赋默认值再进行初始化
* t=null
* count=0
* new T(){count++} ->count=1
* count=2
* 最终count=2
* */
static TT t = new TT();//t=null
static int count = 2;//count=0
private TT() {
count++;
}
}
}
4.JMM
4.1 硬件层数据一致性
intel 用MESI
https://www.cnblogs.com/z00377750/p/9180644.html
现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁
读取缓存以cache line为基本单位,目前64bytes
位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题
伪共享问题:JUC/c_028_FalseSharing
使用缓存行的对齐能够提高效率
4.2 乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系
https://www.cnblogs.com/liushaodong/p/4777308.html
写操作也可以进行合并
https://www.cnblogs.com/liushaodong/p/4777308.html
JUC/029_WriteCombining
乱序执行的证明:JVM/jmm/Disorder.java
原始参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/
4.3 如何保证特定情况下不乱序
硬件内存屏障 X86
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
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的写入对所有处理器可见。
4.4 volatile的实现细节
字节码层面
变量添加 ACC_VOLATILE 标记
JVM层面
volatile内存区的读写 都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
4.4 synchronized 实现细节
字节码层面
ACC_SYNCHRONIZED
monitorenter monitorexit
JVM层面
C C++ 调用了操作系统提供的同步机制
OS和硬件层面
X86 : lock cmpxchg / xxx
https://blog.csdn.net/21aspnet/article/details/88571740
4.5 操作指令学习
案例[有空再搞吧]
public class A {
private int count;
void m() {
synchronized (this) {
count++;
}
}
}
java 汇编
【从0号变量{count}加载数据,即读取数据0】
0 aload_0
【将0放到操作数栈顶】
1 dup
【弹出0赋值给index=1的引用】
2 astore_1
【加锁操作 】
3 monitorenter
4 aload_0
5 dup
6 getfield #2 <test/A.count>
9 iconst_1
10 iadd
11 putfield #2 <test/A.count>
14 aload_1
15 monitorexit
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
5 运行时数据区(runtime data area)
5.1 程序计数器
- 指向下一行要执行的代码。不止一个。多线程时会有多个。
- 程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有). 如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行的是Native方法,则这个计数器为空。
5.2 java stack (栈空间)
- 每个方法对应一个栈帧 栈帧包含:
- 局部变量表(local vairable table )
- 操作数栈(operand stack)
- 动态链接 (Dynamic Linking)
- 返回地址(调用者地址,一般是下一个栈帧)
5.3 Heap (堆空间)
主要用于存储对象。jvm调优的重点
5.4 method area (方法区)
- (jdk version <1.8) Perm Space (永久代),字符串常量在此存储,FGC不会清理,大小启动时指定,启动后不能改变。
- (jdk version >=1.8)Meta Space 字符串位于堆空间 会触发FGC清理。不设定的话最大就是物理内存。
- 线程共享
5.5 Runtime Constant Pool(运行时常量池)
它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中
5.6 Native Method Stack(本地方法栈)
java虚拟机通过JNI调用本地方法的栈
5.7 Direct Memory(jdk1.4以后)
直接内存:os管理的内存,不属于jvm管理。但是jvm可以直接访问
JVM可以直接访问的内核空间的内存 (OS 管理的内存)
NIO , 提高效率,实现zero copy
6 JVM GC
6.1 如何定位垃圾?
- 引用计数(ReferenceCount)
- 根可达算法(RootSearching)
- 通过GC Root的对象,开始向下寻找,看某个对象是否可达
- 什么样的对象能作为gc root对象?
- 类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。
- 什么样的对象会被作为gc root对象?
- 活跃的对象。
6.2 常见的垃圾回收算法
标记清除(mark sweep) - 位置不连续 产生碎片 效率偏低(两遍扫描)
拷贝算法 (copying) - 没有碎片,浪费空间
标记压缩(mark compact) - 没有碎片,效率偏低(两遍扫描,指针需要调
整)
- 标记算法:三色标记(CMS,G1)
白色: 未被GC标记过的对象
灰色: 被GC标记过但是子节点未被完全标记过的对象
黑色: 自己及子节点都被标记过了。
- 三色标记存在的问题:
- 问题一 假如有对象A指向B.在标记A时用户线程取消了A到B的引用,新增了黑色对象C到A的引用。会导致B被漏标。而被回收。
- 解决方案:
- incremental update 增量更新: 关注引用的增加,当有黑色对象指向白色引用时,把这个对象重新标记为灰色。remark阶段重新标记。(通过写屏障实现)【
CMS采用的此方案
】- SATB :关注引用的删除。当灰色引用指向白色引用消失时,把白色引用推到GC堆栈中。remark阶段通过堆栈中的引用找到对象所在的Region的RSet查看是否有引用指向该对象。有继续标记。没有不做标记。【
G1采用的此方案
】- 问题二:浮动垃圾 在并行标记时已经标记完成的对象又变成了垃圾对象。这个时候只能等待下次gc时进行回收
6.3 垃圾回收常用模型
- 除Epsilon ZGC Shenandoah之外的GC都是使用逻辑分代模型
- G1是逻辑分代,物理不分代
- 除此之外不仅逻辑分代,而且物理分代
- STW stop the world当内存不足时,串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行GC开始工作,采用单线程方式回收空间并整理内存。
6.4 分代模型
- 年轻代+老年代+永久代(jdk7)+元空间(jdk8)
- 年轻代: 年轻代又划分为一个Eden区和2个S区。
- 对象刚创建时进入eden区。YGC后进入s0区
- 再次YGC时会把Eden区存活的对象和S0区存活的对象复制到S1区。
- 再次YGC时会把Eden区存活的对象和S1区存活的对象复制到S0区。以此类推。
- 每进行一次复制,对象年龄加一,如果达到最大年龄就会进入到老年代。
- 或者S区不够分配了。那么年龄最大的也会进入老年代
- 老年代: 存放年龄比较大的对象。
- 分配担保 如果新的对象太大。年轻代装不下会直接进入到老年代。
- 对象也有可能在栈空间中:
- 如果是线程私有对象且,不产生逃逸,支持标量替换,栈空间又放的下是可以存放在栈空间的。
- 不产生逃逸:只在当前代码块或者方法内有引用指向该对象(线程完全私有)
- 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换;
- Card Table
由于做YGC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card
在结构上,Card Table用BitMap来实现
6.5 常用的垃圾回收器
- Serial
适用范围:年轻代
线程:单线程串行回收,单核CPU效率高
算法:拷贝算法 (copying)
STW:true
- PS(Parallel Scavenge)
适用范围:年轻代
线程:多线程回收,
算法:拷贝算法 (copying)
STW:true
- ParNew
适用范围:年轻代
线程:多线程,配合CMS并行回收 更关注吞吐量
算法:拷贝算法 (copying)
STW:true
- SerialOld
适用范围:老年代
线程:单线程串行回收,单核CPU效率高
算法:标记清除算法
STW:true
- ParallelOld
适用范围:老年代
线程:多线程回收
算法:标记清除算法
STW:true
- CMS
适用范围:老年代
线程:多线程回收,并行回收
算法:三色标记算法,标记清除算法
STW:remark时会短暂stw。用于标记并发标记时产生的垃圾。
- G1(Garbage First)
- G1收集器虽然保留了分带的概念,但是在物理上不在分代,而是划分为若干个大小相等的Region(默认有2048个)。这些Region在物理上可以是不连续的。每个分区也不是确定的为某一年代服务。可以在老年代和年轻代之间来回切换。
- 有的分区内垃圾对象特别 多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的 垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。
- CSet:Collection Set 一组可被回收的分区的集合,当一个Region中存在可被回收的对象时,该Region的地址会被记录在CSet中,可以是Eden区,s区,或者老年代。CSet占整个堆空间的百分之一不到。
CSet也是存在Region中吗?
- RSet: RememberedSet
- 记录了其他Region中的对象到本Region的引用
- 使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象, 只需要扫描RSet即可。每次给对象赋值引用时。需要做一些额外的操作(记录RSet)
6.6 jvm调优命令:
TLAB(Thread Local Allocation Buffer ):在eden为每个线程分配的非常小的专用空间,维护三个指针 [start,end,top]。当该线程创建对象时会优先放入自己的tlab中。当然。tlab中的对象任然是线程共享的。私有的z只是那三个指针
-Xmn -Xms -Xmx -Xss #年轻代 最小堆 最大堆 栈空间
-XX:+UseTLAB # 使用TLAB,默认打开
-XX:+PrintTLAB # 打印TLAB的使用情况
-XX:TLABSize #设置TLAB大小
-XX:+DisableExplictGC # 禁止显式执行GC,不允许通过代码来触发GC。
-XX:+PrintGC # 打印GC信息
-XX:+PrintGCDetails# 打印GC详细信息
-XX:+PrintHeapAtGC # 查看每次GC前后,GC堆的概况
-XX:+PrintGCTimeStamps # 打印CG发生的时间戳
格式:289.556: [GC [PSYoungGen: 314113K->15937K(300928K)] 405513K->107901K(407680K), 0.0178568 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
[Times: user=0.06 sys=0.00, real=0.01 secs] 提供cpu使用及时间消耗,user是用户模式垃圾收集消耗的cpu时间,实例中垃圾收集器消耗了0.06秒用户态cpu时间,sys是消耗系统态cpu时间,real是指垃圾收集器消耗的实际时间。
-verbose:class # 打印类加载详细过程
-verbose:gc # 打印gc信息
-XX:+PrintVMOptions #该参数表示程序运行时,打印虚拟机接受到的命令行显式参数。我们用下面的命令运行程序
-XX:+PrintFlagsFinal # 按字母排序的所有XX参数和值的表格
-XX:+PrintFlagsInitial # 该命令可以查看所有JVM参数启动的初始值
-Xloggc:opt/log/gc.log # 指定GClog的日志
-XX:MaxTenuringThreshold # 对象最大年龄,最大值15
-XX:SurvivorRatio 修改Eden区 和s区 的比例。(默认 8:1:1)
-XX:PreTenureSizeThreshold 新生代对象的大小限制,超过设置的值都会直接放到老年代。
-XX:MaxTenuringThreshold 对象最大年龄,最大值15
-XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
CMS常用参数
-XX:+UseConcMarkSweepGC 使用cms gc
-XX:ParallelCMSThreads CMS线程数量
-XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
-XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩
-XX:CMSFullGCsBeforeCompaction
多少次FGC之后进行压缩
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收
GCTimeRatio
设置GC时间占用程序运行时间的百分比
-XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
G1常用参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis
建议值,G1会尝试调整Young区的块数来达到这个值
-XX:GCPauseIntervalMillis GC的间隔时间
-XX:+G1HeapRegionSize
分区大小,建议逐渐增大该值,1 2 4 8 16 32。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
ZGC做了改进(动态区块大小)
G1NewSizePercent
新生代最小比例,默认为5%
G1MaxNewSizePercent
新生代最大比例,默认为60%
GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间
ConcGCThreads
线程数量
InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例
6.7 调优工具
- jinfo 该命令可以打印出java进程的配置信息:包括jvm参数,系统属性等[环境变量、jdk版本信息]
- jmap - histo pid 查找有多少对象产生 [序号 实例个数 实例占用空间 类全限定名]
- jmap -dump:format=b,file=xxx pid 导出
- jconsole
- jvisualvm
- jprofiler
- arthas