垃圾收集器分类
串行收集器->Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停适用于'内存比较小的嵌入式设备。
并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于'科学计算、后台处理等若交互场景 。
并发收集器[停顿时间优先]->CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的 时候不会停顿用户线程的运行。 适用于'相对时间有要求的场景,比如Web 。
- 优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选 如果允许停顿时间超过1秒,选择并行或JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器
常见问题
1. 吞吐量和停顿时间
吞吐量和停顿时间
停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
小结 :这两个指标也是评价垃圾回收器好处的标准。
2. 如何选择合适的垃圾收集器
官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器
case:G1 GC 默认最小内存是 2G,稳定运行内存是6G
如果服务器是4C8G 且对响应时间要求不是特别高,可用使用G1;
如果是CPU特别好且内存较小(PS:核数和内存占比较高,如8C4G),对响应时间要求最高,使用CMS。
-
内存泄漏与内存溢出的区别
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。 -
young gc会有stw吗?
不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关 系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时 间相对来说就小很多了。
-
major gc和full gc的区别
Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触 发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大 时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7 永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。
-
什么是直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通 常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内 存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是 有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
-
垃圾判断的方式
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没 有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有 一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。GC ROOT:
Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需 要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好 是其它形式的GC root;
Thread 线程,激活状态的线程;
Stack Local 栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;
JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定 义
JNI Global JNI中的全局变量引用的对象;同上
Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。
Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些类,JVM也没有过多的信息。
-
不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真 正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行 一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个 对象建立关联,否则就会被真的回收。
-
为什么要区分新生代和老年代?
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不 同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合 适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制 成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分 配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
-
G1与CMS的区别是什么
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的 产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
-
方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面 3 个条件才能算是 “无用的类” :
a. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b. 加载该类的 ClassLoader 已经被回收。
c. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM性能优化指南
JVM的性能优化可以分为代码层面和非代码层面。 在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码,提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。 在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身 已经做了很多的内部优化操作,大家要注意的 是不要为了调优和调优。
JVM编译
JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式, 追求更高的运行效率。
解释器
Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。
刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就 会把这些代码认定为"热点代码"。
即使编译器(JIT)-- JDK7各层次的优化。
Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。
即时编译器会把这些"热点代码"编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存 中。
执行引擎
热点代码:被多次调用的方法、被多次执行的循环体。说明:如果是循环体的话,会对其所在的方法进行即时编译 -- OSR.
如何判断是否为热点代码? -- 热点探测。
热点探测分为两种:
基于采样分析-- 无法解决线程阻塞造成方法一直在栈中的问题。
基于计数器的热点探测:
方法调用计数器 : C1模式下 默认1500次;C2模式是10000次 可通过参数:-XX:CompileThreadHold来设置
解释:统计的是一定时间内方法的调用次数,如果一定时间内调用次数小于设置的次数如1500,那么下个周期,调用次数设置为700次 -- 此算法为热度衰减算法 可通过参数:-XX:CounterHalfLifeTime来设置
回边计数器::统计的是循环体调用的次数,此处和方法调用计数器的区别是:统计的是绝对次数。
JVM编译期自我优化--方法内联:如果static、final 修饰的方法a调用了方法b且方法b不是太大的情况下JVM在编译阶段会将方法b的内容直接合并进方法a中,方法b大小限制:热点方法:325字节;,非热点方法35字节。
JVM编译期自我优化--逃逸分析:确定我们的对象会不会被外部访问到,如果一个对象仅仅是在方法中,那么可以直接将对象分配到栈(栈帧是线程私有的)中,分析过程就是逃逸分析,逃逸分析的目的是:分析对象的作用域,减少对象创建、回收的时间,同步锁的锁消除。
逃逸分析命令:-XX:+DoEscapeAnalysis , 系统默认开启。
C1 -- C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序; 适用于:带界面的方法调用。
C2 -- C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序;适用于:服务层面的方法调用。
开启分层编译的命令:
-XX:+TieredCompilation 默认开启,关闭直接走C2;
-XX:TieredStopAtLevel = 1表示只走C1;
内存
内存分配
正常情况下不需要设置,那如果是促销或者秒杀的场景呢? 每台机器配置2c4G,以每秒3000笔订单为例,整个过程持续60秒
内存溢出(OOM)
一般会有两个原因: (1)大并发情况下 (2)内存泄露导致内存溢出
大并发[秒杀]
浏览器缓存、本地缓存、验证码
CDN静态资源服务器
集群+负载均衡 动静态资源分离、限流[基于令牌桶、漏桶算法] 应用级别缓存、接口防刷限流、队列、Tomcat性能优化 异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁 5分钟之内没有支付,取消订单、恢复库存等
内存泄露导致内存溢出
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"; } }
排查流程:
启动
使用jmeter模拟10000次并发
top命令查看
top
top -Hp PIDjstack查看线程情况,发现没有死锁或者IO阻塞的情况
jstack 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
GC
此处以G1垃圾收集器调优为例
是否选用G1
官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间比较长
G1调优
使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count 99.16% 0.00016s 0.0137s 0.00559s 12
调整内存大小再获取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
调整最大停顿时间
-XX:MaxGCPauseMillis=200 设置最大GC停顿时间指标
比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间
Throughput Min Pause Max Pause Avg Pause GC count 98.96% 0.00015s 0.01737s 0.00574s 12
启动并发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
G1调优最佳实战
官网:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
- 不要手动设置新生代和老年代的大小,只要设置整个堆的大小
G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标 如果手动设置了大小就意味着放弃了G1的自动调优 参考:https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
- 不断调优暂停时间目标
一般情况下这个值设置到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。
CPU占用率高
排查流程:
- top
- top -Hp PID 查看进程中占用CPU高的线程id,即tid
- jstack PID | grep tid
JVM常用命令
jps --查看java进程
jinfo
实时查看和调整JVM配置参数
查看用法
jinfo -flag name PID 查看某个java进程的name属性的值
jinfo -flag MaxHeapSize PID jinfo -flag UseG1GC PID
修改
注意:参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID jinfo -flag <name>=<value> PID
查看曾经赋过值的一些参数
jinfo -flags PID
jstat
查看虚拟机性能统计信息
查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
查看垃圾收集信息
jstat -gc PID 1000 10
jstack
查看线程堆栈信息
用法
jstack PID
排查死锁案例
//运行主类 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() + "---获得obj1锁"); synchronized (MyLock.obj2) { System.out.println(Thread.currentThread().getName() + "---获得obj1锁"); } } } } else { while (true) { synchronized (MyLock.obj2) { System.out.println(Thread.currentThread().getName() + "---获得obj2锁"); synchronized (MyLock.obj1) { System.out.println(Thread.currentThread().getName() + "---获得obj1锁"); } } } } } }
- 运行结果:
jstack分析
把打印信息拉到最后可以发现
(2).png)
jmap
打印出堆内存相关信息
dump出堆内存相关信息
jmap -heap PID jinfo -flag UsePSAdaptiveSurvivorSizePolicy 35352 -XX:SurvivorRatio=8
内存溢出自动dump出该文件。
jmap -dump:format=b,file=heap.hprof PID
要是在发生堆内存溢出的时候,能自动dump出该文件就好了
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
关于dump下来的文件
一般dump下来的文件可以结合工具来分析,如内存分析工具--MAT
JVM工具
- 工具层面,会有非常多的场景,这时候首选不要嵌入的工具 -- 影响CPU算例,占用服务器资源,
提醒:Tprofiler可以使用一下,原因是:1. 可以随时开关,2. 可以精确分析创建了多少对象、哪行代码出现了问题。
内存分析工具--MAT
Java堆分析器,用于查找内存泄漏
Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照
它在触发快照的时候保存了很多信息:Java对象和类信息。
通常在写Heap Dump文件前会触发一次Full GC。
下载地址 :https://www.eclipse.org/mat/downloads.php
获取dump文件
手动
jmap -dump:format=b,file=heap.hprof 44808
自动
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
Dump的信息
- All Objects
Class, fields, primitive values and references- All Classes
Classloader, name, super class, static fields- Garbage Collection Roots
Objects defined to be reachable by the JVM- Thread Stacks and Local Variables
The call-stacks of threads at the moment of the snapshot, and per-frame information about local
objects使用
Histogram:可以列出内存中的对象,对象的个数及其大小
Class Name:类名称,java类名 Objects:类的对象的数量,这个对象被创建了多少个
Shallow Heap:一个对象内存的消耗大小,不包含对其他对象的引用
Retained Heap:是shallow Heap的总和,即该对象被GC之后所能回收到内存的总和右击类名--->List Objects--->with incoming references--->列出该类的实例
右击Java对象名--->Merge Shortest Paths to GC Roots--->exclude all ...--->找到GC Root以及原因
Leak Suspects:查找并分析内存泄漏的可能原因
Reports--->Leak Suspects--->Details
Top Consumers:列出大对象