JVM相关总结
https://www.cnblogs.com/jiangym/p/15885161.html
JVM内存模型(JMM)
根据代码画出下面的JVM内存模型
public class Math { public static final int initData = 666; public static User user = new User(); public int compute(){ int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); } } class User{ }
字节码class文件被“类装载子系统”,加载到运行时数据区,通过“字节码执行引擎”运行方法代码。
main方法开始运行后,就会有一个线程;每有一个线程,都会开辟一个线程特有的“虚拟机栈”(最左面),
compute()和main()在虚拟机栈里都会有一个自己的栈帧,方法对应的栈帧会在方法结束时被释放。
存栈帧的顺序也是按栈的先进后出来存和销毁。嵌套调用
(左上二图)局部变量,a,b都会放到局部变量里。
操作数栈里也是栈结构,栈顶会弹出ab然后操作,乘积为30然后放入操作数栈。在字节码文件执行时有程序计数器,计数器的数也是“字节码执行引擎”来控制的
在局部变量分配c的空间,然后从操作数里放到局部变量里。
User对象是静态的,在方法区存栈帧,指向的对象在堆中。
垃圾收集是“字节码执行引擎”开辟的一个线程,会根据可达性分析,以及三色标记算法,来处理。
分代年龄是在对象头里存,对象头第一个字宽(1字宽=4字节=32bit)存markword,(32位机)前25位是hashcode,之后四位是分代标识,
所以只能最多到1111即0-15次,就要去老年代。剩下三位与synchronized有关,存偏向锁和锁标志位。
JVM参数
- 标准参数
- -help
- -server
- -version
- -cp
- X参数,非标准化参数。各个版本不一定相同。
- -Xint 解释执行
- -Xcomp 第一次使用就编译成本地代码
- XX参数(常用)非标准化参数
- 布尔类型 -XX[+-]<name> +-分别标识启用和禁用;name为目标属性
- -XX:+UseConcMarkSweepGC 启用cms垃圾收集器
- -XX:+UseG1GC 启用G1垃圾收集器
- key-value类型 -XX:<name>=<value> name的属性值是value
- -XX:name=10
- -Xms -Xmx 最小最大内存 是简写。
- -Xss 设置堆栈的 ThreadStackSize
- 布尔类型 -XX[+-]<name> +-分别标识启用和禁用;name为目标属性
查看JVM运行时参数:
项目中的配置
java -XX:+UseContainerSupport
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/nginx/logs/java/activity/logs/$ACTIVITY_NAME.gc.log
-Xms500m -Xmx500m
-XX:+UseConcMarkSweepGC
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSScavengeBeforeRemark
-jar /app.jar --spring.profiles.active=$(echo ${active})
--logFileName=$(echo ${ACTIVITY_NAME})
GC调优
官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
G1调优
- 不要设置yong和old区的大小,会覆盖暂停时间目标
- 暂停时间不要设置太严格,影响吞吐量
- 调优MixGC
-XX:MetaspaceSize=64M 触发fullGC的阈值 (old总计118M)
MaxGCPauseMillis=100 stw时间
这几个常用的就行了。
三色标记算法
是为了在每次垃圾回收时,提升扫面效率,把已经扫过的节点忽略。
把每个对象分为黑灰白,未扫过的为白,扫过但是子节点未扫全的为灰,子节点都扫全的为黑。
扫描的线程有的时候会中断,会存在一个黑指向白的问题,导致下一次扫描的时候,白色扫不到,内存泄漏。
CMS解决的办法是,把这种场景的黑变为灰,但是还不能彻底解决这个问题,且扫完之后整体再扫一遍。
G1的解决办法是单独记录黑指向白的线,后续统一处理。
线程间通信
为提高性能,编译器和处理器常常会对指令重排序。
happens -before原则
//todo
垃圾收集器
CMS(老年代 oldGC)+parNew
jdk1.8之前常使用,
一般CMS设置fullGC的阈值是内存到65%,如果等到内存满了会使用Serial-old,这个stw会很长时间
回收阶段
- 初始标记
- 标记根对象直接关联的对象 短暂stw
- 并发标记
- 找出所有与GCRoot能关联的对象,同时执行 不stw
- 并发预清理(重新标记)不一定执行
- 减少5工作量
- 并发可终止的预清理 不一定执行
- 重新标记
- 用户是并发的,怕期间修改,存在stw
- 并发清除
- 并发清除垃圾, 因为并发清除,所以不进行整理。
- 并发重置
- 清除CMS上下文信息
G1
jdk1.8之后默认是G1收集器
把内存划分为若干个region,每个region在同一时刻,只属于一个代。
- Eden
- survivor
- old
- humongous 存大对象,连续的region
YongGC
mixedGC
这个是设计巧妙的部分,老年代占比到阈值(一般45%),会回收所有yongRegion同时回收部分oldRegion区的。
回收阶段
- 初始标记
- 标记根对象直接关联的对象 短暂stw
- 并发标记
- 找出所有与GCRoot能关联的对象,同时执行 不stw
- 最终标记 stw
- 筛选回收
- 根据用户期望,对回收成本进行排序,制定回收计划,并选择region回收
回收过程
region放到一个回收集(类似Set);把region存活对象放到空region里,删除region ---- 复制算法
fullGC
减少rullGC的办法:
- 增加预留内存
- 更早地垃圾回收
- 增加并发阶段使用的线程数
>jdk8 一般都不用CMS了,用G1。
ZGC
升级到jdk17之后,一般可以使用zgc。
todo
垃圾回收算法
- 标记-清除
- 存在内存碎片
- 复制
- 2快内存,性能好,无碎片,内存利用率低。
- 标记-整理
- 标记清除之后,整理碎片。
- 分代收集
可达性分析
对象是否有引用,可以用引用计数法和可达性分析,java使用可达性分析
引用计数法,引用+1,释放-1,来标记,存在循环引用的问题。
可达性分析:没有到gcroot的引用,即可回收。
finalize关键字
一旦垃圾回收器准备释放对象所占的内存空间, 如果对象覆盖了finalize()并且函数体内不能是空的, 就会首先调用对象的finalize(), 然后在下一次垃圾回收动作发生的时候真正收回对象所占的空间.
finalize代码块里加东西要慎重,避免内存泄漏。
最好使用try。catch。finally
类加载机制
类加载过程
类加载的时候是按需加载的
通过运行类时添加参数TraceClassLoading参数,获取类加载的时间线
1.首先读取rt的jar包
2.加载hello类
3.加载hello类的main方法里的uesr类
类加载过程(生命周期)
加载;(验证;准备;解析) => 链接;初始化;使用;卸载
- 加载:查找并加载类的二进制数据。
- (把类的.class文件的二进制数据读入内存,存放在运行时数据区的方法区;类加载的最终结果是产生 堆区中描述对应类的Class对象);
- 链接:包括验证、准备和解析三个子阶段;
- 验证:验证class文件的二进制是否符合规范
- 准备:在准备阶段就赋值了。
- 解析: 把符号引用翻译成直接引用。
- 初始化:给类中的静态变量赋予正确的初始值;
- 创建类(new、反射、克隆、反序列化)
- 使用静态方法、非静态变量
- Class.forName("ATest"); 获取描述类的Class对象;
- 类初始化顺序,是一个面试点
- 使用
- 卸载
- 场景:
- 该类所有实例都被GC
- 加载该类的ClassLoader已被GC
- 该类的Class对象没有被引用,也没有通过反射访问该类的方法。
注意:使用能在编译期能得知的final static修饰的常量,不会导致类的初始化;
public static final int a = 2*3;//编译时常量,不会导致类初始化;
public static final int a b = (int)(Math.random()*10)/10+1; // 不是编译时常量,会初始化;
子类父类初始化过程:
先对这个类进行加载和连接-> 如果有直接父类,先加载连接初始化这个父类->重复以上步骤直到所有父类初始化,初始化当前类;
(先加载连接当前类,再加载连接初始化父类,再初始化当前类)
初始化阶段
变量给真正的值。把准备阶段给的默认值替换掉。
静态代码块执行。
创建对象时执行
普通代码块执行
构造器
类加载执行顺序
- 静态常量
- 静态变量
- 静态初始化块
- 变量
- 初始化块
- 构造器
有继承关系时,父子类加载顺序
- 父类--静态变量
- 父类--静态初始化块
- 子类--静态变量
- 子类--静态初始化块
- 父类--变量
- 父类--初始化块
- 父类--构造器
- 子类--变量
- 子类--初始化块
- 子类--构造器
类加载器classLoad
在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”,被成为“类加载器”
除了Java虚拟机自带的根类加载器以外,其余的类加载器有且只有一个父加载器;
Java虚拟机自带加载器:
- 根(Bootstrap)类加载器:没有父类加载器。负责加载虚拟机核心类。
- 扩展(Extension)类加载器:父加载器为根加载器;继承于java.lang.ClassLoader;
- 系统(System)类加载器:也称应用类加载器,父加载器为扩展类加载器;加载classpath路径下指定的类库;继承于java.lang.ClassLoader,也是自定义加载器的默认父类;
双亲委派
类加载过程中,
会先从最顶层加载器(一般是根加载器)开始往下,
先判断父类加载器能不能加载,能加载则往下传递返回加载的类;
不能加载则继续往下判断,如果都不能加载,则会抛出ClassNotFoundException异常;
加载器之间的父子关系实际上是指加载器对象之间的包装关系,而不是类之间的继承关系;
例如:
双亲委派加载例子
String类来加载;app传到Ext传到Boo,Boo发现rt下有类,就直接加载了;
ext下的类加载;app传到Ext传到Boo,Boo发现加载不了;返回下级加载器;Ext发现ext目录下有,就直接加载了;
用户定义User类加载;app传到Ext传到Boo,Boo发现加载不了;返回下级加载器;Ext发现加载不了,返回下级加载器;App加载器发现可以加载就加载了。
若其他类加载不到,则会抛ClassNotFoundException。
所以说双亲委派是对加载器来说的,总是往上层加载器抛,之前理解的不对,之前想的时子类和父类之间的加载关系。
好处
1.确保安全,避免核心类库被加载
2.避免重复加载
3.保证类的唯一性
例如自己写String类
程序会去RT的加载器加载,但是加载到之后发现main方法不存在,就会报错。
调优
执行 TOP 命令后
场景:CPU持续100%
常见原因
- 宿主机CPU超卖
- 内存问题 大量FUllGC
- 代码存在死循环
排查办法:
- 通过TOP命令可以查看到各个进程CPU,记录其PID
- 通过Jstat命令 打出垃圾收集日志,排查是否是内存溢出:
- Jstat -gcutil 12345 5000
- 查看YGC和FGC的次数和耗时。
- 如果是内存溢出,排查内存溢出位置。
- Jmap -dump :format=b,file = ../../heap.bin PID
- 用mat等工具查看
- 如果不是内存溢出,则有可能是存在死循环。排查死循环位置。
- Top -P 【pid】 -H
- 查看CPU占比高的子线程 1234
- 转成16进制 printf "%x/n" 1234 = abc
- 打印进程堆栈 查看对应abc的具体报错
- sudo -u admin/opt/java/bin/jstack 2066 > a.txt
cat proc/cupinfo 查看CPU的核数
负载高
CPU没满,但负载比较高
等待磁盘IO的进程过多,进程队列过长。
常见场景:
- 磁盘读写请求多
- Mysql有不走索引的语句或死锁
内存异常
mem是内存使用情况
内存飙升常见场景:
- 内存溢出(堆、方法区、栈)
- 堆,fullGC也不起作用了
- 方法区:类加载过多
- 线程栈:递归太多,层级太深。
- 内存泄漏
- jmap -dump format=b file=heap.dump PID
- 用mat等分析
ThreadLocal 相关
java引用分为四种。强软弱虚四种引用。
强引用:
new 一个对象 是强引用
finalize()方法基本上不能重写,避免内存泄漏, 当一个对象被垃圾回收器clear的时候,会调用finalize
重写是为了跟踪回收过程。
System.gc() 建议gc,hotspots 会去回收
软引用:
SoftReference<> m = new SoftReference<>(new byte[1024*1024*10]);
加粗的部分为软引用
byte[] b = new byte[1024*1024*12];
这个是强引用,当执行这句话,空间不够时,上面的软引用会直接回收。
软引用适合缓存的使用,例如缓存图片。
弱引用:
遇到gc就被回收
虚引用:
NIO的零拷贝 这种情况 分配的直接内存 在JVM之外 不能直接被垃圾回收器回收
垃圾回收器里存个虚引用,回收时候有个队列,如果有相关需要删除的直接内存,则再释放。
ThreadLocal
static ThreadLocal<Person> tl = new TheadLocal<>();
作用:
一个线程set一个对象到tl里,另一个线程获取不到
原因要从tl.set()的源码入手:
set是放到当前线程的threadLocalMap里了,所以本质是thread,person对象的一个键值对,放到当前线程的map里。
所以另一个线程获取不到上一个线程存放的数据。key不同。
ThreadLocal缺点
- 不可继承性:子线程中不能读取到父线程的值 InheritableThreadLocal() 不能实现不同线程之间的数据共享
- 脏读(脏数据): 在一个线程中读取到了不属于自己的数据 。
- 线程使用 ThreadLocal 不会出现脏读,每个线程都是用的是自己的变量值和 ThreadLocal
- 线程池使用 ThreadLocal 会出现脏数据,线程池会复用线程,复用线程之后,也会复用线程中的静态属性,从而导致某些方法不能被执行,于是就出现了脏数据的问题
- 解决办法
-
-
- 避免使用静态属性(静态属性在线程池中会复用)
- 使用remove解决
-
- 内存溢出问题(最常出现的问题)
- 内存溢出:当一个线程执行完之后,不会释放这个线程所占用内存,或着释放内存不及时(线程不用了,但线程相关内存还得不到及时释放)
应用:
spring的@transactional注解中,若有两个dao层M1和M2,为了保证M1获取的数据库链接和M相同,
吧M1的connection链接放到ThreadLocal里,M2从ThreadLocal里取,这样就保证了两次连接再一个事务里。
内存泄漏
线程池里的线程如果用到ThreadLocal了,必须要手动清理掉,防止内存泄漏
threadLocalMap的key最初用的强引用,一直也回收不掉,越积累越多,后来变成弱引用了。
用不到当前threadLocal了,要调用tl.remove(); 底层是threadLocalMap.remove(当前线程);
造成泄漏的原因:
由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null
如果当前的情况下在栈中将threadlocal1的引用设置为null,强引用1将会失效,那堆中的threadlocal1对象因为ThreadLocalMap的key对它的引用是弱引用,将会在下一次gc被回收,那就会出现key变成null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于ThreadLocalMap.Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。
hash 冲突
我们都知道 HashMap 在发生冲突的时候会采用拉链法,将冲突的元素链式存储在同一个槽里面。 而ThreadLocalMap 采取了另外一种方式,如果当前槽中已经有元素,那么它试图存入后一个槽中,直到找到可以容纳自己的槽。
脏数据
线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的类静态属性也会被重用。如果在实现线程run() 方法中不显示的调用remove() 清理与线程相关的ThreadLocal 信息。如果先一个线程不调用set() 设置初始值,那么就get() 到重用信息,包括ThreadLocl 所关联线对象的值。
脏数据问题在实际故障中十分常见。比如 用户A下单后没有看到订单记录,而B却看到了A的订单记录。通过排查发现是通过session 优化引起的。在原来的请求中,用户每次请求Server,都需要去缓存里查询用户的session信息,这样做无疑增加了一次调用。因此开发工程师决定采用某框架来缓存每个用户对应的SecurityContext,它封装了session 相关信息。优化后虽然为每一个用户新建了一个session 相关的上下文,但是因为ThreadLoacl 没有再线程结束是及时进行remove() 清理操作,在高并发场景下,线程池中的线程可能会读取到上一个线程缓存的用户信息。
常见的CMS GC问题分析与解决
源自美团公众号: Java中9种常见的CMS GC问题分析与解决
命令行终端
-
标准终端类:jps、jinfo、jstat、jstack、jmap
-
功能整合类:jcmd、vjtools、arthas、greys
可视化界面
-
简易:JConsole、JVisualvm、HA、GCHisto、GCViewer
-
进阶:MAT、JProfiler
命令行推荐 arthas ,可视化界面推荐 JProfiler
判断 GC 有没有问题
- 延迟
- STW时间,越短越好。
- 吞吐量
内存分配
https://www.cnblogs.com/tiancai/p/11699566.html
分配过程
https://www.cnblogs.com/wyf0518/p/11461944.html
方法内联
热点方法小于325字节,A调用B的场景,JVM会直接写成A里+B里的,减少压栈和出栈的操作,方法体尽量小,尽量用final,static修饰。
空间换时间。CodeCache容易溢出。
逃逸分析
- 全局变量逃逸
- 局部变量赋值给类变量,作用域放大了。
- 方法返回值逃逸
- 方法内的作用域放大到方法外了。
- 实例引用逃逸
- this应用,当前实例,放大到返回值外了。
- 线程逃逸
JVM会对对象进行逃逸标记,
- 全局级别逃逸
- 对象作为方法的返回值
- 对象作为静态字段,或者成员变量
- 重写了finalize()方法,那这个类都会被标记,并且会放到堆内存中。
- 参数级别逃逸
- 实例引用逃逸
- 无逃逸
标量替换
- 标量: 不能被进一步分解的: 基础类型,对象引用
- 聚合量:可以进一步分解:字符串。
替换
栈上分配
通过逃逸分析,对象不会被外部访问,就直接在栈上分配。
基于逃逸分析,可以开启锁消除
TLAB
https://www.cnblogs.com/straybirds/p/8529924.html
线程本地分配缓存区。
是线程专用的内存分配区域,JVM会为每一个线程分配一块TLAB区域,占用Eden区
为什么要TLAB?
加速内存分配。对象一般都在堆内存中分配的,堆是线程共享的,有并发问题,要同步处理。
JVM分配时,底层用CASE和失败重试,保证分配不出问题。为了效率。
局限性
大对象不能用,直接在堆内存分配
分配时独享,使用时共享。
内存泄漏场景
造成泄漏的原因
相关资料
https://www.bilibili.com/video/BV14K411F7S4?p=1
https://www.bilibili.com/video/BV12b4y167Mb?p=2
https://www.bilibili.com/video/BV12b4y167Mb?p=12
https://www.bilibili.com/video/BV1s44y167Yj?from=search&seid=18316383145675461585&spm_id_from=333.337.0.0
《Java面象对象编程》
https://www.cnblogs.com/nje19951205/p/17151531.html
https://www.cnblogs.com/WangJinYang/p/10264400.html