JVM知识体系
#查看class
- jclasslib 插件 安装 使用
- classfile 构成
u4 maigc(四位)
u2 minor_version(两位)
u2 major_version(两位)
u2 contants_pool_count 常量个体数(两位)
cp_info constant_pool[contants_pool_count-1]
class 加载过程(硬盘到内存)
1.Loading(加载到内存中 双亲委派)
类加载器
- Class 通过Loader 到内存 一块内存放二进制文件,一块生成该Class类的对象,这个对象,指向存放二进制文件地址
- method Area(方法七)
– perm G(1.8之前)
– metaspace(1.8之后)
类加载器加载层次
- Bootstrap 加载 jdk 核心的Class lib/rt.jar等核心类 由C或C++实现
- Extension 加载扩展包 jre/lib/ext//*.jar
- APP 加载classpath指定内容
- CustomClassLoader 自定义加载器
- Bootstrap负责加载其他的ClassLoader 让其他ClassLoader 对应的Class,例如程序最后两行,加载ClassLoader 的ClassLoader 是BootStrap
一个class文件被加载的过程(双亲委派)
class 文件通过自己的loadclass 方法到内存,尝试 CustomClassLoad 加载 CustomClassLoad 发现自己的缓存中没,就问AppClassLoader是否加载进来了,一直向上询问,直到Bootstrap,然后向下委托,麻烦下级加载,直到可以加载的类加载器加载上
为什么要搞双亲委派
-
为了安全
-
避免重复
-
除了bootstrap 加载器,其他加载器都是bootstrap加载来的
-
父加载器,只是当前加载器中parent变量中保存的加载器
-
如下 bootstrap 是null
-
类加载器范围
-
sun.misc.Launcher$ExtClassLoader
-
sun.misc.Launcher$AppClassLoader
- BootStrap 加载
- extClassLoader
- appClassLoader
自定义类加载器
- getClassLoader()方法得到的是AppClassLoader 调用loadClass 加载指定的java文件编译后的class文件到内存,并生成与之对应的Class对象
- ClassLoader是反射的基石 通过操作Class 类完成反射
- 自己找调用父类classLoader ,最后自己调动findClass方法
- 继承ClassLoader 通过重写findClass方法实现自定义类加载器
com.xj.ios.HelloWorld 类加载过程 (双亲委派)
首先自定义加载器 调用自己的loadclass方法 问appclassLoader 加载过没有,appclassLoader没有,找exclassloader 加载过没有,没有 再去找bootstarp 没有加载过,bootstarp尝试在自己的区域加载,没有告诉exclassloader 去加载,没有告诉AppclassLoader,还是没有,就会告诉自定义加载器加载,加载的时候调用findclass,找到对应的class类,加载到内存
自定义classloader 的parnet 是appclassloader
自定义classloader 的加载器是appclassloader
-
自定义classLoader 可以置顶parent(扩展)
-
通过classLoader 写加密方法
-
-加密
- 解密
- 类加载采用懒加载的方式,用到的时候加载
混合模式
- 解释器
-
- bytecode intepreter
- JIT
-
- just in-time compiler
- just in-time compiler
2.Linking
2.1verfication(校验)
- 校验class是否符合JVM规范
2.2preparation(静态变量赋默认)
- 给静态成员变量赋默认值
2.3resolution (常量池用到的符号引用转换为内存引用)
- 类 方法 属性等符号引用解析为直接引用
- 常量池中各种符号引用解析为指针,偏移量等内存地址的直接引用
3.Initialzing
- 静态变量赋值为初始值
面试题:
- count 输出几 2
解析:T.count load T.class到内存中,linking 校验、静态变量赋初始值 Tt = null,count=0, initialing t = new T() 执行构造count++ 为1 count 复制为2
- count 输出几 3
T load到内存 到linking 阶段 静态变量赋 默认值 count=0 T t= null , initiling count=2 t = new T() count 为3
面试题 volatile 双重校验单例 为什么要用volatile
- new 对象分为两步 T t = new T() 申请内存-赋默认值-赋初始值
1.申请内存
2.内存中,成员变量默认值
3.调用构造 赋初始值
由于new 会分为申请内存 给成员变量赋默认值 再赋初始值, 这个赋完默认值 其实Instance其实就不为空了 ,这时候另一个线程拿到的INSTANCE 中的成员变量,的值是默认值 ,并不是初始化后的值,加volatile防止指令重排序
- 1new 是申请空间
- invokespecial 调用构造方法
- astore 将内存地址复制给变量
- 可能出现指令重排序问题,所以用voletial 防止指令重排序
- loading过程
JMM(java 内存模型)
硬件层数据一致性
- 离CPU越近,更小,更快
- 老的CPU数据同步方案,为了防止两个cpu 数据不一致 ,用的总线锁,第一个Cpu操作x的时候,其他cpu无法用,所以效率低下
缓存行 (一块连续的内存)
读取缓存以cache line为基本,度为64个字节
-
- 对象创建过程
class loading linking (检验 静态变量赋默认值 符号引用化) 静态变量初始化 -》 申请内存对象-》成员变量赋默认值-》 调用构造方法-》成员变量赋初始值,执行构造方法语句
-
观察虚拟机配置命令
-
-XX:InitialHeapSize 初始堆大小
-
-XX:MaxHeapSize 最大堆大小
-
-XX:+UseCompressedClassPointers
-
2.对象在内存中的存储布局
- 普通对象
- 1.对象头 markdown 8个字节
- 2.ClassPointer 指向class对象
- 3.实例数据
- 4.Padding 对齐 8字节
- 数组对象
- 1.对象头 markdown 8个字节
- 2.ClassPointer 指向class对象
- 3.数组长度:4字节
- 4. 数组数据
- 5.Padding 对齐 8字节
- 对象头具体包括什么(没懂)
- 对象怎么定位(没懂)
- 对象怎么分配(没懂)
JAVA 运行时数据区域和JVM指令集
- run-time data areas
- ProgramCounter 存放指令的位置 下一条指令
- Heap 堆
- JVM static 每个线程对应一个栈 每个方法对应一个栈帧
- native method stacks 对用jni 调C C++
- Direct Memory 直接内存
- MethodArea 方法区
- 1.Perm Space(<1.8)
- 2.Meta Space(>1.8)
栈帧 Frame(重点)
- 一个线程在JVM中是一个JVM栈,每个方法是一个栈帧, 每一个栈帧有自己的操作数栈和动态链接
局部变量表(Local Variable Table)
操作数栈(Operand Stack)
动态链接(Dynamic Linking)
指向class文件中常量池的符号链接 (方法叫什么名字,方法的返回值类型等等)
a()中调用b()方法 指向class文件的常量池中b 方法的描述信息,就是动态链接
返回地址(return address)
a() 调用 b() ,方法a调用方法b, b 方法的返回值放在什么地方 存储这个返回值的地址 就是栈帧的返回地址
一个线程一个jvm 栈,每个方法对应一个栈帧,栈帧包括四个 局部变量表,操作数栈,动态链接,返回地址
- 局部变量表 (方法用到的局部变量)
- 操作数栈
通过 jclasslib 可以看到 main方法栈帧包括两张表,LineNumberTable(行号表),LocalVariableTable (用到的局部变量)
用到变量名字,cp_info_#15 对用ContantsPool 中
- 0 binpush 8 (8 当成byte 扩展成int入操作数栈)
- 2 istore_1 (把栈顶出栈 复制下标值为1的局部变量中 给8赋值给i)
- 3 iload_1 (把局部变量表下标为1的数,入栈)
- 4 iinc 1 by 1 (把局部变量表下标为1的数+1 ) 此时栈中是8 局部变量表中是9
- 7 istore_1((把栈顶出栈 8 赋值给下标值为1的局部变量中 从9 变到8 )
- 。。。
- 11 iload_1 (把局部变量表下标为1的数,入栈)
- 13 return
结果为9
- 0 binpush 8 (8 当成byte 扩展成int入操作数栈)
- 2 istore_1 (把栈顶出栈 赋值下标值为1的局部变量中 给8赋值给i)
- 3 iinc 1 by 1 (把局部变量表下标为1的数+1 ) 局部变量8 -》9
- 6 iload_1 (把局部变量表下标为1的数,入栈) 9 入栈
- 7 istore_1((把栈顶出栈 9 赋值给下标值为1的局部变量中 )
- 11 iload_1 (把局部变量表下标为1的数,入栈) 9 入栈
- return
1.一个方法对应一个栈帧,会和堆还有方法区打交道
非静态方法,默认局部变量表中,第0个是this
Hello_02 h = new Hello_02()
1. new xxxxx
new Hello_02() 申请内存空间 赋初始值 将内存地址压栈
2. dup
复制对象内存地址,再次压栈
3.invokespecial----<init>
弹栈、给对象初始化,调用构造方法
4.astore_1 弹栈 将地址复制给h
5.aload_1 将局部变量表下标为1的h 压栈
6.invokespecial----<m> 调用m方法
7.return
m 方法
1. sipush 200
2.istore_1
3.return
DCL 为什么使用volatile
- 因为new对象过程,会出现申请空间,默认值,压栈,出栈,赋初始值,过程中会出现异步问题,所以用volatile
return 100
将100压栈 放在main 栈帧栈顶
弹栈
return 100
将100压栈
istore_2 100 弹栈 赋值给局部变量表的下标为2的变量
垃圾回收
ROOT Searching
jvm 栈 、 本地方法栈 、常量池、方法区静态变量
垃圾回收算法
- 标记删除 (两次扫描,容易产生碎片)
- 标记压缩 (两次扫描,效率比较低)
- 复制清除 (浪费空间,需要对象调整)
堆内存 逻辑分代模型(青年代 老年代)
1.eden(伊甸区)(新生代) (copy)
2.servivor (幸存区1)(新生代)回收一次 到这里(copy)
3.servivor (幸存区2)(新生代)回收一次 到这里(copy)
4.老年代(mark comact mark sweep)
一个独享从出生到消亡的过程
对象产生后,尝试在栈上分配,如果分配不下,进入伊甸区,被回收一次进入s1,再回收一次,进入s2,在回收进入s1 ,如此重复,知道年龄达标,进入old区域
GC概念(YGC FULLGC)
- 年轻代空间耗尽,触发YGC
- 老年代空间耗尽,触发FULLGC (年轻代和老年代同时回收)
- 栈上分配
- 线程本地分配 TLAB
去掉逃逸分析
去掉标量替换
去掉TLAB
一个对象产生,是否可以入栈,如果不可以,判断对象是否大,通过固定的参数配置,如果够大直接进入old 区,触发(FULLGC)才会消亡,
如果不够大,进入伊甸区(TLAB),伊甸区进行YGC 清除,对象进入s1,在清除,如果年龄够了,进入old,如果不够进入s2 ,直到年龄达标,进入old区域。
(永久代 Perm Generation(1.7)、元数据区(1.8)Metaspace)
- MethodArea
- Perm Generation(<1.8)
- Metaspace(1.8以后)
class 的源信息
代码的编译信息
jni 产生的中间jit
等等
java 虚拟机参数
常见的垃圾回收器
1.Serial (单线程 )
2.Parallel(多线程)
Serial+SerialOld
Parallel Scavenge+Parallel Old
ParNew+CMS
- 1.Seria
- ATW stop-the-work 线程停止 进行垃圾回收
- 2.Seria Old
- 老年代的Seria
2.Parallel Scavenge - 停止线程,多线程回收垃圾
parallel old
PS+PO
-
ParNew+CMS
-
ParNew(parallel new)
在 ps 的基础上增加了对CMS的支持 -
CMS (1.4 版本引入 CMS是里程碑式的 并发回收 CMS毛病较多 没有任何一个jdk 版本默认是CMS 但是很重要)
concurrent mark sweep
并发标记清除
- 初始标记 标记ROOT对象
- 并发标记
- 重新标记
- 重新标记就是标记并发标记过程中新产生的对象
- 并发清理
CMS 缺点
- 由于浮动垃圾问题,预制在50%-68% ,保证有足够的空间可以产生浮动垃圾 ,
- 默认值 -XX:CMSInitiatiingOCCpancyFraction=92% 由于必须有预留移动的空间给浮动垃圾,所以阈值为92% 阈值达到92% 就会触发FGC,将阈值降低到50%-68%,让它有足够的空间产生浮动垃圾以及
GC 和GC Tuning
垃圾回收器组合参数的设定
JVM调优第一步,了解JVM常用命令行参数
- HostSport 参数分类
标准:-开头 所有的HotSpot
非标准: -X开头,特定版本HotSpot支持特定命令
不稳定定: -XX 开头 下一版本可能取消
- Java 虚拟机-XX 所有的参数
java -XX:+PrintFlagsFinal -version |grep CMS
总参数700-800
- 1.概念区分:内存泄漏 memory leak ,内存溢出 out of memory
- 2.java -XX:+PrintCommandLineFlags HelloGC
-XX:InitialHeapSize = 16070592 初始虚拟机堆大小
-XX:MaxHeapSize=257129472 最大堆大小
-XX:+UseComperssedClassPointers -XX:+UseCompressedOops
指针压缩
-
- java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
-Xms40M -Xmx60M 最小堆大小 最大堆大小 一般两个值一样
-Xmn 新生代大小
-
- java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- 5.java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
cms 回收会更频繁
初始化标记
[GC (CMS Initial Mark) 72771K(126720K), 0.0009446 secs]
并发标记
重新标记
并发清除
GC 日志详解
- java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
[GC (Allocation Failure) [ParNew: 38579K->4100K(39296K), 0.0284189 secs] 1420303K->1419617K(1455924K), 0.0284476 secs] [Times: user=0.03 sys=0.02, real=0.03 secs]
GC 哪种回收器
Allocation Failure 产生原因
ParNew: 38579K->4100K(39296K), 0.0284189 secs] 年轻代 (回收前大小->回收后大小)(年轻代总大小) 时间
1420303K->1419617K(1455924K) 回收前堆大小-回收后堆大小(堆总大小) 时间
[Times: user=0.03 sys=0.02, real=0.03 secs] 执行的时候 用户状 内核态 总多少
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at HelloGC.main(HelloGC.java:9)
Heap
par new generation total 306688K, used 306433K [0x0000000740000000, 0x0000000754cc0000, 0x0000000754cc0000)
eden space 272640K, 100% used [0x0000000740000000, 0x0000000750a40000, 0x0000000750a40000)
from space 34048K, 99% used [0x0000000750a40000, 0x0000000752b40598, 0x0000000752b80000)
to space 34048K, 0% used [0x0000000752b80000, 0x0000000752b80000, 0x0000000754cc0000)
concurrent mark-sweep generation total 1756416K, used 1755477K [0x0000000754cc0000, 0x00000007c0000000, 0x00000007c0000000)
Metaspace used 2687K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 290K, capacity 386K, committed 512K, reserved 1048576K
起始内存地址 使用空间结束地址 整体空间结束地址
total = eden+ 1个survivor
调优前基础概念
- 1.吞吐量 用户代码执行时间/(用户代码执行时间+垃圾回收时间)
- 2.响应时间: STW越短 相应时间越好
所为调优,首先确定追求啥,吞吐量优先,还是响应时间优先 还是在满足一定相应时间的情况下,要求达到多大的吞吐量
问题:
科学计算 吞吐量优先 --数据挖掘 一般采用 (PS+PO)
相应时间优先:网站,代界面的程序 (1.8 G1 或 ParNew+CMS)
什么是调优
- 1.根据需求进行JVM预调优
- 2.优化运行JVM运行环境
- 3.解决JVM出现的各种问题(OOM)
调优,从规划开始
QPS per second (1000并发 好几十万用户在线)
TPS
PPS
- 调优 : 根据业务场景
- 无监控,不调优 (压力测试)
- 步骤:
- 熟悉业务场景(没有最好的垃圾回收器,只有合适的)
- 响应时间、停顿时间【CMS G1 ZGC】
- 吞吐量 = 用户时间/(用户时间+GC时间)[PS]
2.选择垃圾回收器组合
3.计算内存需求
4.选定CPU
5.设定年代大小,升级年龄
6.设定日志参数
如下:5个日志文件,每个20M,循环插入
7.观察日志情况
1.系统硬件升级反而卡顿
为什么慢 很多数据load到内存,内存不足频繁GC,相应时间变慢
为什么更卡顿,内存越大, FGC时间越长
咋办:
ps+po 修改为 ProNew+CMS 或者G1
2.系统CPU经常100% 如何调优
1.找出那个进程(top mingling)
2.进程中那个线程高(top -Hp )
3.到处该线程的堆栈(jstack)
4查找那个方法(栈帧)耗费时间(js)
5.工作线程占比高 还是 垃圾回收线程占比高
3.系统内存飙高
1. 到处堆内存(jmap)
2. 分析(jhat jvisualvm jprofiler)
4.如何监控jvm
jstat jvisualvm arthas top ...
1. java -Xms200M -Xmx200M -XX:+PrintGC com.xj.jvm.gc.T01_FullGC
堆内存大小200M 打印GC信息
2.一般是运维团队收到报警信息(内存 或者CPU)
3.top 命令 查看cpu 占比比较高的进程 如果是java 进程 记住pid(进程id)
4.top -Hp 1364(pid) 打印java进程的线程占比 观察那个线程cpu 和内存占比高
5.jstack 1364 (pid)
-
查看线程 top
-
查看java 进程 top -Hp 8390
-
jps 定位具体进程
-
jstack 8390
-
多个程序等待抢一个对象 同一个锁
-
jinfo 8568
- jconsole
- jstat -gc
- java VisualVM 图像化界面 可以查看内存使用情况 定位问题
- 面试官问怎么定位OOM问题的
不要说是图形化界面 因为生产环境下不允许 只有测试的时候用 测试的时候才会用
已经上线的系统用什么 cmdline arthas
- jmap 解决oom 查看对象占用空间
- jmap -histo 8568| head -20 线上内存特别大 不如100G 就不适合了
1.设定HeapDump参数 oom时候会自动产生堆转储文件
2.很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
3.在线定位
- arthas 在线监测jvm
下载 https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
挂载到8568
-
常用命令
-
jvm 查看jvm信息 相当于jinfo
-
thread 那些线程线程的使用情况
-
heapdump 相当于 jmap 到处堆内存情况
-
heapdump /home/wupeng/11111.hprof
分析堆内存 dump文件分析
- 类似于 jmap
- 如果服务器已经挂掉怎么倒出堆内存
- jmap 或者参数
- jad 反编译
- 动态代理生成的类 或者 是否是被使用了的代码
- 按理汇总
G1
复习CMS
- PerNew+CMS
- 缺点
1.初始标记
2.并发标记
3.重新标记
4.并发清除
G1 逻辑分代 物理不分代
主要用于服务器,多核、大内存
压缩空闲时间不会延长GC的暂停时间
G1的内存区域不是固定的eden 或者old区域
- G1基本概念
- 1.card table
- RSet RememberedSet
- 3.CSet Collection Set
- G1 有FullGC吗? 如果产生怎么处理
- 当年轻代的空间,使用超多一定值 45% 以后,就会触发MixedGC 就相当于cms
并发标记算法
三色标记 (cms G1 都在使用)
颜色指针
- 通过三个字节标识对象引用是否改变 ,回收的时候扫描变化过的
csm 日志
- java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.xj.jvm.gc.T01_FullGC
G1 日志
-
java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseG1 com.xj.jvm.gc.T01_FullGC
-
G1 由于region 组成 里面有 年轻代 幸存 老年 还有大对象
-
GC 指定暂停时间 比如20ms 回收年轻代如果比20Ms大 那就调节年轻代大小
实战参数