JVM是一个跨语言的平台,加载字节码文件,翻译成不同系统的机器指令进行运行
字节码文件可视化,可以在 idea-plugins 安装【jclasslib bytecode viewer】
通过idea-view-show bytecode with jclasslib 查看
class文件结构:
ClassFile {
u4 magic; (魔数,识别class文件格式 0xCAFEBABE) 4字节
u2 minor_version; (小版本) 2字节
u2 major_version; (大版本) 2字节
u2 constant_pool_count; (常量池计数器) 2字节
cp_info constant_pool[constant_pool_count-1]; (常量池表) n字节
u2 access_flags; (访问标识) 2字节
u2 this_class; (类索引) 2字节
u2 super_class; (父类索引) 2字节
u2 interfaces_count; (接口计数器) 2字节
u2 interfaces[interfaces_count]; (接口索引集合) 2字节
u2 fields_count; (字段计数器) 2字节
field_info fields[fields_count]; (字段表) n字节
u2 methods_count; (方法计数器) 2字节
method_info methods[methods_count]; (方法表) n字节
u2 attributes_count; (属性计数器) 2字节
attribute_info attributes[attributes_count]; (属性表) n字节
}
包装类对象缓存范围
包装类 | 缓存范围 |
---|---|
Byte | -128~127 |
Short | -128~127 |
Integer | -128~127 |
Long | -128~127 |
Float | 没有 |
Double | 没有 |
Character | 0~127 |
Boolean | true、false |
类的加载过程:
加载(Loading):
- 通过本地磁盘/网络获取class文件
- 在【运行时数据区-元空间】生成这个Class类信息 (会在验证通过后执行)
链接(Linking):
- 验证(Verification)
验证class文件是否合法,例如魔数检查,开头0xCAFEBABE;语义校验等。 - 准备(Preparation)
给类中的静态变量
分配内存,并赋值默认值 (如果是static final修饰的,就不会赋值默认值,直接赋值定义的值
) - 解析(Resolution)
把类、接口、字段和方法的符号引用转为直接引用。 (例如:常量池中的字面量引用,将字面量的对象加载到内存后,就有真实的内存地址了,就可以引用真实的内容地址)
初始化(Initialization):
给类的静态变量赋予定义的值。就是执行<clinit>()方法 (如果有static修饰的类变量或者static静态代码块,jvm才会自动生成一个<clinit>()方法 父类的static块优先级高于子类;如果[静态变量没有显示赋值],或者[final修饰并且是常量]就不会生成<clinit>()
)
Class.forName("")和Class.getClassLoader().loadClass("")区别是?
Class.forName会Loading和Initialization。 Class.getClassLoader().loadClass只会Loading
类加载器的分类:
启动类加载器(Bootstrap ClassLoader):
- C/C++实现,用来加载java核心库:JAVA_HOME/jre/lib/rt.jar
- 只加载java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader):
- java实现,继承ClassLoader类
- 负责加载JAVA_HOME/jre/lib/ext或java.ext.dirs路径类库
应用类加载器(Application ClassLoader):
- java实现,继承ClassLoader类
- 负责加载java.class.path路径类库
- 它是应用中默认的类加载器
双亲委派机制:
避免了类的重复加载,防止核心类库被篡改。
子类加载器可以访问父类加载器的Class,反之不行; 只有类路径和类名一致并且类加载器一致,才能保证类唯一
运行时内存结构:
java -XX:+PrintFlagsFinal -version:查看所有JVM的最终值
HotSpot VM参数文档
- 程序计数器:存储将要执行下一条指令的地址,防止CPU时间片切换后重复执行。它是线程私有的
- 栈:
先进后出的特点
,栈里面存放着一个个栈帧,一个栈帧对应一个java的方法。由于栈分配的内存比较小,所以它主要负责运行指令集,存放一些基本数据类型,通过引用进行创建/修改堆空间的数据。
栈不存在GC,但存在OOM
-Xss1m:设置栈大小
栈帧:对应一个java的方法
1.局部变量表:定义为一个数组,用于存储方法参数和方法体内的局部变量;如果是非static修饰的方法,索引[0]下标保存的就是this;
double和long占用两个slot,也就是索引下标占两个
2.操作数栈:用于保存计算过程的中间结果以及计算过程中变量的临时存放
- 堆:
负责对象的存储,所有的对象实例以及数组都应当在堆上
。 可以处在物理上不连续的内存空间中;堆中Eden区会有一块TLAB区域,它是线程私有的
堆存在GC,存在OOM
-Xms512m:设置堆空间起始大小(默认物理内存的1/64)
-Xmx512m:设置堆空间最大大小(默认物理内存的1/4)
通常-Xms和-Xmx配置相同的值,避免清理完堆后不需要重新计算堆区大小。
年轻代:它又分为Eden区和S0(from)、S1(to)区;
存放一些生命周期比较短的对象,创建和消亡都非常迅速。
-Xmn512m:设置年轻代大小
老年代:生命周期非常长的对象
-XX:NewRatio=2:设置新生代与老年代的占比
-XX:SurvivorRatio=8:设置Eden与S0、S1区占比
- 元空间(1.8):使用堆外直接内存,存放.Class类结构信息、方法信息、常量、JIT热点代码缓存
-XX:MetaspaceSize=1024m:设置元空间初始大小
-XX:MaxMetaspaceSize=-1:设置元空间最大大小
为什么要分老年代年轻代?
年轻代对象特点是生命周期很短,老年代对象特点是生命周期很长,如果混到一起,每次触发GC,大对象寻找又很慢,对象又很多,每次都会浪费一部分不需要GC的对象寻找时间。GC会暂停用户线程,造成响应慢
对象分配过程:
1.new对象会先放到年轻代的Eden区,如果Eden满了,放不下了,会触发YGC进行垃圾回收。通过可达性分析,找出还是引用的对象,放到to区,并且年龄=1,如果from区还有对象,也会找出还在引用的对象,放到to区,并且年龄+1;
2.当S区的存活对象反复计数,达到设定的年龄阈值(默认是15),就会将对象放到老年代中;或者对象太大,Eden区YGC后还是放不下,就会直接放到老年代;如果老年代也放不下,就会触发FGC,最后还是放不下,直接抛出OOM异常
-XX:MaxTenuringThreshold=15:设置对象S区到老年代的年龄阈值【最大就15,因为对象头中分代年龄就4bit,最大就1111,转换十进制就是15】
对象实例创建过程:
例如通过代码:Object obj = new Object();
对应的字节码指令:
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
8 return
对象实例结构组成:
HotSpot JVM默认使用混合模式(解释器+JIT及时编译器)
解释器:逐行解释字节码指令成为可执行的机器指令
JIT编译器:寻找热点代码,将对应的机器指令缓存到元空间中,减少重复的解释耗时,提升性能;
哪些代码才符合热点代码?
当调用某个方法的次数超过阈值
-XX:CompileThreshold=10000:设置JIT缓存方法调用次数阈值(-client模式默认1500次,-server模式默认10000次)
垃圾回收器(GC):
哪些对象需要回收?怎么判断这个对象是垃圾?
就是在运行中没有任何引用指向这个对象,没有人用这个对象,就应该被回收掉。
-
可达性分析:通过根Roots节点对象寻找引用的对象,避免了循环引用,内存泄漏。为了保证一致性,GC进行中会STW,暂停用户线程,防止产生新的Root和引用
Roots节点对象有哪些?
1.栈中引用的对象
2.类的静态属性引用的对象
3.元空间中常量引用的对象
4.被同步锁synchronized持有的对象
5.jvm内部的引用对象
可达性分析.jpg
存活对象找到后,怎么回收垃圾对象呢?
-
标记-清除算法:通过可达性分析,将存活对象进行标记,再次全堆遍历未标记的垃圾对象记录到空闲内存地址列表中
缺点:效率低,遍历两次;内存空间不连续,碎片化
标记-清理.png -
复制算法:通过可达性分析,将存活对象按序直接复制到另一块内存空间,不会出现碎片化问题
缺点:需要两倍内存空间,引用地址的变动也要联动,对象不能太多,越多越耗时。年轻代S0和S1区就是用的复制算法
复制算法.png -
标记-整理算法:通过可达性分析进行标记,将所有存活的对象从内存的一端按顺序移动,之后清理另一端所有;避免了两倍内存的开销。通常老年代用的就是此算法,所以要尽可能减少FGC
缺点:效率低于复制算法
标记-整理.png
对象的引用类型:
- 强引用(普通对象默认强引用):只要还被引用着,对象GC不回收,直到OOM
- 软引用(SoftReference):内存不足,发生OOM之前,会将软引用对象回收
- 弱引用(WeakReference):只要触发GC就会回收掉
- 虚引用:对象回收跟踪
- 终结器引用:用于实现对象的finalize();
内存溢出(OOM):分配一个超大对象,JVM判定GC后也不可能放下,就直接OOM或者GC回收后,还是没有可分配的内存放置创建的对象,也会出现内存溢出。
内存泄漏:这里的泄漏并不是说可达性分析没找到而导致的,而是本身应该视为垃圾,但在逻辑中引用着,无法释放
有哪些垃圾回收器?特点是什么?
JDK8默认使用Parallel Scavenge GC和Parallel Old GC
JDK9默认使用G1 GC
Serial GC:使用复制算法,串行回收,暂停所有用户线程。
Serial Old GC:使用标记-整理算法,串行回收,暂停所有用户线程。
适用场景:比如运行在客户端模式client,配置不算高,单个CPU还省去了线程交互的开销
-XX:+UseSerialGC:设置年轻代老年代串行垃圾回收器ParNew GC:使用复制算法,并行回收,暂停所有用户线程。在多核配置下,大大提升了吞吐量。
-XX:+UseParNewGC:设置年轻代并行垃圾回收器
-XX:ParallelGCThreads:设置并行线程数量Paraller Scavenge GC:使用复制算法,并行回收,暂停所有用户线程。在多核配置下,大大提升了吞吐量。
Paraller Old GC:使用标记-整理算法,并行回收,暂停所有用户线程。
-XX:+UseParallerGC:设置年轻代并行垃圾回收器
-XX:+UseParallerOldGC:设置老年代并行垃圾回收器
-XX:GCTimeRatio:垃圾收集时间占总时间的比例 (1/(N+1)) ;默认99,垃圾回收的时间不超过1%。
-XX:+UseAdaptiveSizePolicy:开启自适应调节,年轻代Eden/S0/S1比例自动调节,以及S区年龄阈值自动调节等。
CMS GC:使用标记-清除算法,并发回收,与用户线程交替执行,主打低延迟。
初始标记(STW):只标记与GC Roots直接关联的对象。会暂停用户线程
并发标记:从GC Roots直接关联的对象寻找间接关联的对象
重新标记(STW):修正并发标记阶段,用户线程并发执行,导致数据不一致性
并发清理:清理未标记的对象;因为并发标记和并发清理阶段,所以无法清理浮动垃圾.
-XX:+UseConcMarkSweepGC:设置老年代代并发垃圾回收器
-XX:CMSInitiatingOccupanyFraction:设置堆内存使用率阈值,CMS并不是放不下了对象的时候才触发垃圾回收。
-XX:+UseCMSCompactAtFullCollection:CMS GC后,开启内存碎片整理,但是会STW。
-XX:CMSFullGCsBeforeCompaction:多少次FGC后,才对内存碎片进行整理。
G1 GC:将堆空间分成若干个区域(Region),逻辑上区分年轻代和老年代。侧重点回收垃圾最大量的区域(Region),后台维护一个区域(Region)优先级列表;Region之间采用复制算法,对整堆进行位置整理。
-XX:+UseG1GC:设置年轻代和老年代代垃圾回收器。
-XX:G1HeapRegionSize:设置每个Region大小。2的幂
-XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间,但不能完全保证,默认200ms
-XX:InitiatingHeapOccupancyPercent:堆的使用率超过比例,触发GC,默认45
-XX:ConcGCThreads:并发标记的线程数
GC日志监控与分析:
-XX:+PrintGCDetails:打印GC回收时堆和元空间的详细信息
-XX:+PrintGCDateStamps:输出发生GC的时间
-Xloggc:/data/gc.log:把GC日志输出到指定文件中
-XX:+UseGCLogFileRotation:打开GC日志文件滚动覆盖
-XX:NumberOfGCLogFiles:设置滚动日志文件的个数
-XX:GCLogFileSize:设置滚动日志文件的大小
例如:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1m
jstat -gc <pid> 1000 20 (通过jdk自带命令工具每隔1秒打印一次gc情况,总共打印20次)
arthas在线分析 官网
java -jar arthas-boot.jar <pid>
还可以导出dump文件进行离线分析:
- 手动导出方式:
jmap -dump:live,format=b,file=<name.hprof> <pid>
- 自动导出方式(出现OOM后):
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<name.hprof>
使用工具进行分析dump和展示图表:
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true){
Object obj = new Object();
list.add(obj);
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- jvisualvm(jdk自带的):
-
Eclipse Memory Analyzer
eclipse疑似泄漏分析.png
eclipse泄漏分析详情.png
eclipse线程.png
如何合理的设置堆空间大小?
设置过大,就会堆积很多对象,GC时间就比较长;设置过小,就会频繁触发GC
空间 | 命令 | 推荐值 |
---|---|---|
整堆(heap) | -Xms和-Xmx | 设置为老年代FGC后存活对象大小的3-4倍 |
元空间(metaSpace) | -XX:MetaspaceSize和-XX:MaxMetaspaceSize | 设置为老年代FGC后存活对象大小的1.2-1.5倍 |
年轻代(young) | -Xmn | 设置为老年代FGC后存活对象大小的1-1.5倍 |
获取老年代FGC后存活对象大小:打开GC日志,通过FGC后的空间大小,取多次的平均值作为参考值。
手动触发FGC:jmap -dump:live,format=b,file=<name.hprof> <pid>或者jmap -histo:live <pid>
CPU飙高如何排查?
-
通过top命令根据cpu排序,看看哪个java进程占用较高
或通过ps aux | grep java 查找跟java相关的进程
最终得到进程ID(pid)
top命令.png
psaux.png -
top -Hp <pid> :查看进程的飙高的线程id
异常线程id.png - 将飙高的线程id十进制——>>十六进制
2482 -> 0x9b2 -
jstack <进程的pid> | grep -A20 0x9b2
jstack线程信息.png
定位到可能存在问题的代码位置或者是垃圾回收线程占用的问题