1.1 cpu占用过高
记下这个16进制的数字,下面我们要用
(4)用jstack工具查看线程栈情况
[root@localhost ~]# jstack 7268 | grep 1c77 -A 10
"http-nio-8080-exec-2" #16 daemon prio=5 os_prio=0 tid=0x00007fb66ce81000
nid=0x1c77 runnable [0x00007fb639ab9000]
java.lang.Thread.State: RUNNABLE
at
com.spareyaya.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19)
at
com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.jav
a:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(Invocable
HandlerMethod.java:190)
at
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(I
nvocableHandlerMethod.java:138)
at
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMet
hod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
通过jstack工具输出现在的线程栈,再通过grep命令结合上一步拿到的线程16进制的id定位到这个线程
的运行情况,其中jstack后面的7268是第(1)步定位到的进程号,grep后面的是(2)、(3)步定位
到的线程号。
从输出结果可以看到这个线程处于运行状态,在执行
com.spareyaya.jvm.service.EndlessLoopService.service 这个方法,代码行号是19行,这样就
可以去到代码的19行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。
1.2 死锁
死锁并没有第一种场景那么明显,web应用肯定是多线程的程序,它服务于多个请求,程序发生死锁后,死锁的线程处于等待状态(WAITING或TIMED_WAITING),等待状态的线程不占用cpu,消耗的内存也很有限,而表现上可能是请求没法进行,最后超时了。在死锁情况不多的时候,这种情况不容易被发现。可以使用jstack工具来查看
由于web应用往往会有很多工作线程,特别是在高并发的情况下线程数更多,于是这个命令的输出内容会十分多。jstack最大的好处就是会把产生死锁的信息(包含是什么线程产生的)输出到最后,所以我们只需要看最后的内容就行了
Java stack information for the threads listed above:
===================================================
"Thread-4":
at
com.spareyaya.jvm.service.DeadLockService.service2(DeadLockService.java:35)
- waiting to lock <0x00000000f5035ae0> (a java.lang.Object)
- locked <0x00000000f5035af0> (a java.lang.Object)
at
com.spareyaya.jvm.controller.JVMController.lambda$deadLock$1(JVMController.java:
41)
at
com.spareyaya.jvm.controller.JVMController$$Lambda$457/1776922136.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-3":
at
com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)
- waiting to lock <0x00000000f5035af0> (a java.lang.Object)
- locked <0x00000000f5035ae0> (a java.lang.Object)
at
com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:
37)
at
com.spareyaya.jvm.controller.JVMController$$Lambda$456/474286897.run(Unknown
Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
发现了一个死锁,原因也一目了然。
1.3 内存泄漏
我们都知道,java和c++的最大区别是前者会自动收回不再使用的内存,后者需要程序员手动释放。在
c++中,如果我们忘记释放内存就会发生内存泄漏。但是,不要以为jvm帮我们回收了内存就不会出现内
存泄漏。程序发生内存泄漏后,进程的可用内存会慢慢变少,最后的结果就是抛出OOM错误。发生OOM错误后可能会想到是内存不够大,于是把-Xmx参数调大,然后重启应用。这么做的结果就是,过了一段时间后,OOM依然会出现。最后无法再调大最大堆内存了,结果就是只能每隔一段时间重启一下应用。内存泄漏的另一个可能的表现是请求的响应时间变长了。这是因为频繁发生的GC会暂停其它所有线程(Stop The World)造成的。
为了模拟这个场景,使用了以下的程序
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
Main main = new Main();
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
main.run();
}
}
private void run() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
// do something...
});
}
}
}
运行参数是 -Xms20m -Xmx20m -XX:+PrintGC ,把可用内存调小一点,并且在发生gc时输出信息,运行
结果如下
...
[GC (Allocation Failure) 12776K->10840K(18432K), 0.0309510 secs]
[GC (Allocation Failure) 13400K->11520K(18432K), 0.0333385 secs]
[GC (Allocation Failure) 14080K->12168K(18432K), 0.0332409 secs]
[GC (Allocation Failure) 14728K->12832K(18432K), 0.0370435 secs]
[Full GC (Ergonomics) 12832K->12363K(18432K), 0.1942141 secs]
[Full GC (Ergonomics) 14923K->12951K(18432K), 0.1607221 secs]
[Full GC (Ergonomics) 15511K->13542K(18432K), 0.1956311 secs]
...
[Full GC (Ergonomics) 16382K->16381K(18432K), 0.1734902 secs]
[Full GC (Ergonomics) 16383K->16383K(18432K), 0.1922607 secs]
[Full GC (Ergonomics) 16383K->16383K(18432K), 0.1824278 secs]
[Full GC (Allocation Failure) 16383K->16383K(18432K), 0.1710382 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1829138 secs]
[Full GC (Ergonomics) Exception in thread "main" 16383K->16382K(18432K),
0.1406222 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1392928 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1546243 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1755271 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1699080 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1697982 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1851136 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1655088 secs]
java.lang.OutOfMemoryError: Java heap space
类的编译优化
2.2 编译优化
类的加载过程
JVM内存空间
JVM堆内存分配机制
JVM垃圾回收机制
垃圾回收算法
几种进入老年代的方式,我们看看这些是怎么处理的:
大对象直接进入老年代:这个没办法优化,总不能调大对象大小吧,那这些大对象在新生代的复制
就会很慢了。
长期存活的对象将进入老年代:年龄的增加,是每次Minor GC的时候,所以我们可以减少Minor
GC(这个方法上面提到了),这样年龄就不会一直增加。
动态年龄判断:有一个重要的条件就是在Survivor空间中相同年龄所有对象大小的总和大于Survivor
空间的一半,所以要加大新生代的内存空间。
空间分配担保:这里面有一个条件是Minor GC后,Survivor空间放不下的就存放到老年代,为了让
存活不到老年代,我们可以加大Survivor空间。
上面的方法,既有加大新生代的内存空间,也有加大Survivor空间,实时上,怎么优化,需要根据我们的实际场景来看,JVM的优化并没有银弹。
JVM - CMS垃圾收集器
空间碎片
标记-清除算法会产生空间碎片的,如果连续的内存空间不够存放
即将进入老年代的对象,此时就会触发Full GC。
为了避免这种情况,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection,默认打开的,当Full GC完成后,他会STW,进行内存整理,把存活的对象紧密的靠在一起,腾出连续空间。如果每次都要重新内存,那都会STW,所以CMS还提供了-XX:CMSFullGCsBeforeCompaction参数,默认是0,他的意思是进行了多少次Full GC后才整理内存。
JVM - G1垃圾收集器
10.1 Region
使用G1收集器时,java堆的内存会划分为多个大小相等的独立区域(Region),他也有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合.如下图所示:粉色的代表新生代,没有字母的是eden,有s的是survivor ,老年代是浅蓝的O,还有一个H是humongous,也是老年代,大对象直接进入老年代,这个humongous就是存储大对象的,也就是说如果对象内存大小大于Region的一半大小,那就会给一个专门的Region存放,如果对象大于一个Region的大小,那就用多个Region存放。
10.3 垃圾回收
10.3.1 新生代回收
上面提过,新生代的内存空间最多占用60%,当60%的空间用完的时候,就会触发新生代的回收。新生代的回收是用复制算法的,与之前不同的是,他会考虑到停顿时间。
10.3.2 老年代回收
老年代回收分为:初始标记、并发标记、最终标记、混合回收。
前面三个阶段跟CMS垃圾收集的前面三个类似。
混合回收,是说他并不会仅仅回收老年代的垃圾,也会回收新生代的垃圾,他会根据停顿时间,尽可能的多回收Region。由于在停顿时间内回收的垃圾可能不会很多,所以这个阶段会进行多次的混合回收,默认是8次,可以通过 -XX:G1MixedGCCountTarget 设置。
如果混合回收的时候,发现Region仅占有5%了,那他就会停止回收,不会一直回收8次。
混合回收的基于复制算法的,所以大对象的复制会比较耗时,如果某个老年代的Region超过85%的对象是存活的,那他不会被回收,通过 -XX:G1MixedGCLiveThresholdPercent 设置。
JVM各种参数配置
11.6.5 G1收集器
官方文档:https://docs.oracle.com/en/java/javase/13/docs/specs/man/java.html
-XX:+UseG1GC 开启G1收集器。
-XX:G1HeapRegionSize 设置每个Region的大小,值是2的N次幂,范围1m ~ 32m,目标是根据最小的
Java堆划分出月2048个区域。
-XX:MaxGCPauseMillis 设置期望到达的最大GC停顿时间,默认值是200ms。
-XX:ParallelGCThreads 设置STW时GC线程数,最大值为8。
-XX:ConcGCThreads 设置并发标记的线程数,参考值:ParallelGCThreads 的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC的堆占用率阀值,默认值为45%。
-XX:G1NewSizePercent -XX:G1MaxNewSizePercent 设置新生代占用整堆内存的最小百分比(默认
5%)、最大百分比(默认60%)。
-XX:G1ReservePercent 保留内存区域,防止survivor中的to区溢出,默认值10%。