JVM高频知识点总结【3】
1 JVM常用参数
1.1 标准参数
-version
-help
-server
-cp
1.2 -X参数(非标准)
非标准参数,也就是在JDK各个版本中可能会变动
-Xint 解释执行
-Xcomp 编译执行(第一次使用就编译成本地代码)
-Xmixed 混合模式,JVM自己来决定
使用时需要加上-version,之前说过java是编译+解释执行的语言,此处可以调整
1.3 -XX参数(非标准,重要)
使用得最多得参数
非标准化参数,相对不稳定,主要用于JVM调优和Debug
a.Boolean类型
格式: -XX:[+-]<name> +或-表示启动或禁用name属性
比如: -XX:+UseConcMarkSweepGC 表示启用CMS垃圾收集器
-XX:+UseG1GC 表示启用G1类型得垃圾收集器
b.非Boolean类型"直接设数值"
格式:-XX<name>=<value> 表示name属性得值是value
比如:-XX:MaxGCPauseMillis=500
1.4 其他参数(简写)
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xms1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
所以这块也相当于-XX类型得参数
1.5 查看参数
java -XX:+PrintFlagsFinal -version
java -XX:+PrintFlagsFinal -version > flags.txt --导出到flags文件(linux环境下)
需要注意的是"=“表示默认值,”:="表示被用户或JVM修改后得值
如果我们想要查看某个进程具体参数得值,可以使用jinfo命令
一般来说,我们设置参数之前,可以先查看一下当前参数是什么,然后进行修改
1.6 设置参数的常见方式
- 开发工具中设置,如IDEA、Eclipse
- 运行jar包的时候:java -XX:+UseG1GC xxx.jar
- web容器比如tomcat,可以在脚本中进行设置
- 通过jinfo实时调整某个进程的参数(参数只有被标记为manageable的flags才可以被实时修改)
1.7 常用参数合集
2 JVM常见命令
2.1 jps(查看java进程)
查看java进程
2.2 jinfo(查看、修改属性值)
(1)实时查看和调整JVM配置参数
(2)查看用法
jinfo -flag name PID 查看进程号为PID的某个java进程name属性的值
例:
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID
(3)修改
参数只有被标记为manageable的flags才可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag <name>=<value> PID
(4)查看曾经赋值过的一些参数
jinfo -flags PID
2.3 jstat(查看垃圾收集信息、类装载信息)
- 查看虚拟机性能统计信息
- 查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,一共输出10次
- 查看垃圾收集信息
jstat -gc PID 1000 10
2.4 jstack(查看堆栈信息、排查死锁)
查看线程堆栈信息
jstack PID
- 排查死锁(DeadLockDemo)
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
//定义锁对象
class MyLock{
public static Object obj1 = new Object();
public static Object obj2 = new Object();
}
//死锁代码
class DeadLock implements Runnable{
private boolean flag;
DeadLock(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
while(true){
synchronized (MyLock.obj1){
System.out.println(Thread.currentThread().getName() + "----if获得obj1锁");
synchronized (MyLock.obj2){
System.out.println(Thread.currentThread().getName() + "----if获得obj2锁");
}
}
}
} else {
while (true){
synchronized (MyLock.obj2){
System.out.println(Thread.currentThread().getName() + "----否则获得obj2锁");
synchronized (MyLock.obj1){
System.out.println(Thread.currentThread().getName() + "----否则获得obj1锁");
}
}
}
}
}
}
将打印信息拉到最后
2.5 jmap(查看堆内存信息)
- 生成堆转储快照
- 打印出堆内存相关信息
jmap -heap PID
3. dump出堆内存相关信息
jmap -dump:format=b,file=heap.hprof PID
这个时候我们会想,要是在发生堆内存溢出得时候,能够自动dump出该文件就好了
- 一般我们在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
3 JVM基础性能优化
JVM的性能优化可以分为代码层面和非代码层面
- 在代码层面,我们可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要再重复执行这些代码了。
- 在非代码层面,一般情况下,可以从内存、gc及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身
已经做了很多的内部优化操作。
那今天我就从内存、gc以及cpu这3个方面和大家一起探讨一下JVM的优化,但是我们需要注意的是不要为了调优和调优。
3.1 内存
3.1.1 内存分配(Young:Old,促销)
正常情况下不需要设置,但是如果是促销或秒杀场景呢?
每台机器配置2c4G(2核4G),以每秒3000笔订单为例,整个过程持续60s
上面的图是大致估算,在有限的设备下,如何让我们的服务器能够承受住并发【调节Young:Old区比例】
3.1.2 内存溢出(OOM)
一般来说内存溢出都有两大原因:
- 大并发情况下
- 内存泄漏导致内存溢出(对象是可达的,GC Root能够找到它,但是该对象却在后续业务代码中没有使用)
3.1.2.1 大并发[秒杀]
常用手段
浏览器缓存、本地缓存、验证码
CDN静态资源服务器
集群+负载均衡
动静资源分离、限流[基于令牌桶、漏桶算法]
应用级别缓存、接口防刷限流、队列、Tomcat性能优化
异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁
5分钟之内没有支付,取消订单、恢复库存等
3.1.2.2 内存泄漏导致内存溢出
ThreadLocal引起的内存泄漏,最终导致内存溢出
public class TLController {
@RequestMapping(value = "/tl")
public String tl(HttpServletRequest request) {
ThreadLocal<Byte[]> tl = new ThreadLocal<Byte[]>();
// 1MB
tl.set(new Byte[1024*1024]);
return "ok";
}
}
①上传到阿里云服务器
jvm-case-0.0.1-SNAPSHOT.jar
②启动
java -jar -Xms1000M -Xmx1000M -XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=jvm.hprof jvm-case-0.0.1-SNAPSHOT.jar
③使用jmeter模拟1000次并发
39.100.39.63:8080/tl
④使用top命令查看
top
top -Hp PID
⑤使用jstack查看线程情况,发现没有死锁或IO阻塞的情况
jstak PID
java -jar arthas.jar ---> thread
⑥查看堆内存的使用,发现堆内存使用率已经高达88.95%
jmap -heap PID
java -jar arthas.jar ---> dashboard
⑦此时可以大体判断出来,发生了内存泄漏从而导致的内存溢出,怎么排查呢?
jmap -histo:live PID | more
读取到jvm.hprof文件,上传到指定的工具分析,比如heaphero.io
3.2 是否选用G1收集器
官网 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
(1)50%以上的对被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
3.3 G1调优
(1)使用G1:-XX:+UseG1GC
修改配置参数,获得到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
99.16% 0.00016s 0.0137s 0.00559s 12
(2)调整内存大小再获取gc日志分析
-XX:MetaspaceSize=100M
-Xms300M
-Xmx300M
比如设置堆内存的大小,获得到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.89% 0.00021s 0.01531s 0.00538s 12
(3)调整最大停顿时间
-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.96% 0.00015s 0.01737s 0.00574s 12
(4)启动并发GC时堆内存占用百分比
-XX:InitiatingHeapOccupancyPercent=45
G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。
值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
比如设置该百分比参数,获得到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count
98.11% 0.00406s 0.00532s 0.00469s 12
3.4 G1调优实战
官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#r
ecommendations
- 不要手动设置新生代和老年代的大小,只设置则整个堆的大小
why
:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
G1收集器在运行过程中会调整新生代和老年代的大小
其实是通过adapt代的大小来调整对象的晋升速度和年龄,从而达到为收集器设置的暂停时间目标
如果手动设置了大小,就意味着放弃了G1的自动调优
- 不断调优暂停时间目标
一般情况下,这个值设置到100ms或200ms都是可以的(不同情况会不一样),但如果设置成50ms就
不太合理。
-- 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化为Full GC。
所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能
总是得到满足。
- 使用-XX:ConcGCThreads=n来增加标记线程的数量
IHOP阈值过高:有转移失败风险,比如对象进行转移时空间不足。如果阈值过低,会使标记周期运
行过于频繁,并且有可能混合收集器回收不到空间。
IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,
调高ConcGCThreads
- MixedGC调优
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
- 适当增加堆内存大小
- 不正常的Full GC
有时候会发现系统刚刚启动的时候,就会发生一次Full GC,但是老年代空间比较充足,一般是
由Metaspace区域引起的。可以通过适当增加MetaspaceSize大小,比如256M。
4 总结
4.1 JVM性能优化脑图
4.2 常见问题
- 内存泄漏与内存溢出区别?
内存泄漏:不再使用的对象无法得到及时的回收,持续占用内存空间
内存溢出:内存泄漏很容易导致内存溢出,但内存溢出不一定都是内存泄漏导致的
- Young GC会由STW吗?
会,不管什么GC,都会由STW(stop the world),区别是持续时间的长短。这个时间的长短与
垃圾收集器由关,Serial、ParNew、Parallel Scavenge收集器无论是并行还是串行,都会挂起
用户线程,而CMS和G1在并发标记时候,是不会挂起用户线程的,但其他阶段一样会挂起,不过相比
其他垃圾收集器而言STW时间相对来说会小很多。【ZGC:串行】
- Major GC与Full GC区别?
1.Major GC在很多参考资料中是等价于Full GC的,我们也可以发现在很多性能检测工具中只有
Minor GC和Full GC。
2.一般情况下:一次Full GC会将年轻代、老年代、元空间以及对外内存进行垃圾回收。
3.Full GC原因;
【1】年轻代晋升到老年代,老年代空间不足
【2】老年代使用率超过阈值
【3】元空间不足时(JDK1.7,永久代不足)
【4】调用System.gc();
- 什么是直接内存
服务器的内存;
Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的,直接向系统申请的内存空间
,通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直
接内存。由于直接内存在Java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系
统内存是有限的,Java堆和直接内存的总和依然受限于OS能够给出的最大内存。
- 垃圾判断的方式
1.引用计数法:循环引用问题
2.根可达性算法(引用链):GC Root
- 不可达的对象一定要被回收吗?
不是。【finalize】
即使在可达性分析法中不可达的对象,也不是"非死不可"的,这个时候他们暂处于"缓刑"阶段,要
真正宣告一个对象的死亡,至少要经历两次标记过程:可达性分析算法中不可达的对象被第一次标
记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖
finalize方法或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象的时候会被放在一个队列中进行第二次标记,除非这个对象与引用链上的
任何一个对象建立关联,否则就真的会被回收。
- 为什么要区分新生代和老年代?
根据各个区域中对象的特点选用不同的垃圾收集算法,垃圾收集器
"新生代":复制
"老年代":标记-清除或标记-整理
- G1与CMS的区别
CMS:集中在老年代的回收
G1:集中在分代回收,包括年轻代与老年代
-- G1使用Region方式对堆内存进行了划分,且基于标记-整理算法实现
- 方法区中无用类的回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的呢?
【1】该类的所有实例都已经被回收
【2】加载该类的ClassLoader已经被回收
【3】该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该
类的方法
5 拓展
JVM重要知识点:
- 类加载
装载 - 链接 - 初始化 - 使用 - 卸载
类加载器 、双亲委派、 三大特性及打破双亲委派、 SPI
- 运行时数据区
栈帧(要将运行时数据区与其他知识结合起来)
- 对象的内存布局
指针压缩问题
- 内存模型及对象已死问题
面试的时候问题对象的生命周期即可
第三方图片的缓存
图片缓存导致内存泄漏
- 垃圾回收算法
动态分配规则
常规的三大垃圾回收算法
整理算法
分代回收三大假说
- 垃圾收集器
【1】CMS
- CMS的两种模式与一种特殊策略
- 可中止的预处理
- 三色标记
- CMS的参数调优
【2】G1
- Rest三大数据类型
- G1特性
- G1参数
- 三大回收流程
【3】ZGC
- ZGC三大核心技术
- ZGC垃圾回收触发时机
- JVM常见命令及执行引擎
JVM的分层编译5大级别
code cache
- JVM性能优化
案例优化 自己找一个高并发的案例来分析
- JVM常见工具使用
监控工具
内存分析
日志分析