看的一些网址

一些资料

Java线程池七个参数详解

深度好文:JVM调优

对于调优,首先还是要进行架构调优、代码调优

1
2
3
lsof -i tcp:8082 # 查看端口的进程
lsof -i -P | grep -i "listen" # 查看本机监听的端口进程列表
ps -M <pid> # 查看进程的线程列表

1 黑马程序员,栈内存溢出(java.lang.StackOverflowError)

栈:线程运行需要的内存空间。

栈桢:每个方法运行需要的内存。

栈不需要垃圾回收的,因为栈是一次次方法调用产生栈桢,用完弹出

栈内存肯定也不是越大越好,栈内存越大,(物理内存一定,然后每一个线程有一个栈,栈越大,那么同时运行的线程数目越少,递归可以更深一点)

1
2
3
栈桢过多导致栈内存溢出
栈桢过大导致栈内存溢出
-Xss:设置栈内存大小

比如第三方,一个类中引用另外一个类,另一个类中引用该类,那么json解析的时候,也会出现无限递归,可以给其中一个类的那个属性设置成@JsonIgnore

2 黑马程序员,堆内存溢出(java.OutOfMemoryError: Java heap space)

1
2
3
4
5
6
7
8
9
10
11
// 黑马举的例子,
String a = "hello";
List<String> list = new ArrayList<>();
while(true) {
list.add(a);
a = a + a;
}
// 导致 java.OutOfMemoryError: Java heap space
-Xmx 8m // 修改堆空间大小
-Xmx 指定应用程序可用的最大堆大小
-Xms 指定应用程序可用的最小堆大小

黑马程序员、堆内存诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 比如上面的代码跑出outofmemory,那么使用下面的一些调试方法

# 1 查看当前系统中有哪些java进程
jps -l -v
# 2 查看当前堆内存的占用情况
jmap -heap 38353
jhsdb jmap --heap --pid 38353

# 3 jconsole工具,图形界面,还有其他CPU等的监测,可以连续监测
jconsole # 然后连接就行,之前的jstack查看线程死锁也是有的

# 4 jvisualvm更好用
# mac里面下载了,打开即可,所以说前面的jps、jmap什么的其实可以不用
# 可以直接在 监视 这个tab里面点击堆Dump查看当前的dump快照,也有 执行垃圾回收 这样的按钮

案例

1
2
3
4
5
# 这个进程在垃圾回收之后,内存占用还是很高
# jconsole的memory里面有一个执行GC(代码中system.gc()也一样的效果),但是堆内存从250M,到200+M,降低了不多,为啥呢?
编程错误吗?
可以用jvisualvm,更好用
使用jvisualvm,点击 堆dump查看当前快照,然后查看当前最大的几个实例对象,发现Students这个list中一直在加student对象,由于list是在最外面的强饮用,所以居高不下

3 黑马程序员、方法区内存溢出(java.OutOfMemoryError: Metaspace/PermGen space)

1.8以前导致永久代内存溢出

1.8之后导致元空间内存溢出

-XX:MaxMetaspaceSize-8m

1.8以前,永久代。

jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。

元空间

类加载器加载类的二进制字节码的时候,存在方法区中,1.8以后是在操作系统内存中去了

所以默认没有上限,它的案例中用-XX:MaxMetaspaceSize-8m变小,然后就出现了。

3JIC9MBbju5fVhD

defineClass也只能触发类的加载,不会有链接和初始化。

永久代

还有1.8以前的(1.6)也演示一下

GLZsY7NrkyU3MxH

实际场景中

spring、mybatis用到一些字节码技术,比如都会用到cglib这些技术。

spring用它生成一些代理类,这些代理类是Spring中AOP的核心。

mybatis中可以用cglib产生mapper接口的实现类。

运行期间动态生成类的二进制字节码,完成动态的类加载。代理技术中广泛的运用了字节码的动态生成技术。

所以在运行期间,会生成大量的在运行期间生成的类,所以1.8以前很容易导致永久代的溢出。

4 针对线程的诊断

案例1: CPU占用居高不下

1 top命令可以定位到哪一个线程,cpu占用很高

2 ps H -eo pid,tid,%cpu | grep pid (可以查看当前进程内的线程对CPU的占用情况,H线程,macOS下不用H,但是tid出不来)

3 jsack pid 就可以看到某个线程tid的一些日志,这里面的tid是16进制的,需要转换,然后里面可以定位到行

​ 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

案例2: 运行很长时间,迟迟没有得到结果,可能死锁

1 top

2 ps H

3 jstack pid

​ 最后会有一些输出,Found one Java-level deadlock,会定位到行号,当然也可以直接用jvisualvm查看死锁的。

感觉上面的案例中,除了要定位到某个线程的行号,jvisualvm用不了,其他的都能用它解决。

GC

AV32JFHUZjd6S5T

HEKXYojBCMOiGc9

G1是JDK9默认,取代了之前的CMS。

nPhtcSiZ7CL98Rg

tyzhang JVM: 垃圾回收

调优领域

1 内存

2 锁竞争

3 cpu占用

4 io

  • 低延迟还是高吞吐量,选择合适的回收器(科学运算追求高吞吐量,互联网项目追求响应时间)

  • CMS,G1,ZZGC

  • ParallelGC

  • Zing(一个虚拟机),几乎没有STW的时间

-Xmn // 新生代,越小,minorGC越频繁,很多到了老年代,老年代GC也频繁;越大,老年代的空间紧张,那么再出发GC的时候就可能是FullGC,时间更长。所以新生代一般堆的1/4以上,1/2以下。

老年代调优的时候,先尝试不要调优。即使发生FullGC,也要先看看新生代的调优。

以CMS为例,CMS老年代的内存越大越好,并发的,其他线程也能 并发,但是初始标记和重新标记的时候,会STW。

垃圾回收的同时,其他线程也在运行,浮动垃圾产生了,浮动垃圾产生了又导致内存不足,那么就会造成CMS并发失败,那么就会退化成串行的serialold回收器。

-XX: CMSInitiatingOccupancyFraction=precent,就是说老年代使用了多少就FullGC,一般留下20%左右给浮动垃圾。

案例1:FullGC和MinorGC频繁

新生代小,minorGC频繁,导致晋升对象的晋升阈值降低,老年代存了大量短时间的,所以FullGC频繁。

增大新生代。

案例2:请求高峰时期发生FullGC,单次暂停时间也别长(CMS)

要求低延时

分析:查看GC日志,看看哪个阶段时间比较长。GC日志会有每一阶段耗费的时间,

CMS的初始标记和并发标记时间比较快,重新标记比较慢,因为重新标记扫描整个堆内存,不光老年代而且新生代,新生代对象个数比较多,那么扫描时间就会更大,那么是否可以先进行一次新生代的GC,就是在重新标记之前,再进行一次minorGC,使新生代需要标记的更少。

-XX:+CMSScavengeBeforeRemark

案例3: 老年代充裕的情况下,发生FullGC(CMS jdk1.7)

1.7的时候方法区是永久代,在堆中,永久代如果设置小了,内存不足,就会导致FullGC。

jvisualvm

1 GC日志

JVM第三篇:Jvisualvm查看GC流程

GC日志的话,还是需要添加虚拟机参数进行打印的。然后jvisualvm的话,就可以用插件visualGC观看变化(老年代、新生代(E、S1、S0)、方法区(元空间))。

如果为idea,通过如下方式配置打印GC日志:

1
2
3
4
5
6
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log

-verbose:gc : 开启gc日志
-XX:+PrintGCDetails : 打印gc详情
-XX:+PrintGCDateStamps : 打印gc时间戳
-Xloggc:gcc.log : 将日志输出到文件xx(默认位置为桌面) 这个如果写了,那么控制台就不会输出了
1
2
// 查看当前虚拟机下面的GC运行参数
java -XX:+PrintFlagsFinal -version | grep GC

2 堆dump

堆dump,用jmap可以

1
2
3
4
5
6
# 通过设置如下的JVM参数,可以在发生OutOfMemoryError后获取到一份HPROF二进制Heap Dump文件:
-XX:+HeapDumpOnOutOfMemoryError
# 可以为虚拟机设置下面的参数,这样就可以在需要的时候按下CTRL+BREAK组合键随时获取一份heap dump文件:
-XX:+HeapDumpOnCtrlBreak
# JDK自带了一些工具可以帮助我们查看JVM运行的堆内存情况,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。常用的是
jmap、jstat

不过直接用jvisualvm里面选择堆,然后点击堆dump就能查看GCroots个数等,也能查看当前的对象大小排列。也就是查看了当前的GC里面的数据了。

但是查看老年代、新生代、元空间的大小,除了上面的GC日志和VisualGC外,需要使用命令

1
2
 jmap -heap <pid>            # 现实当前的年轻代、老年代等
jstat -gcutil 58648 1000 10 # 实时查看某个进程,1000ms打印一次,打印10次停止

oaN9vRHPy8BF7Ti

3 死锁

jstack能看

jconsole也能看,jvisualvm也能看,参照下面thread dump最后会有。

4 查看线程

做法1: jps查找进程,ps H -eo pid,tid,%cpu | grep pid,jstack pid

做法2: jvisualvm,右键进程,有一个thread dump(里面的线程tab的下面也有),就可以看到里面有关进程的结果,跟jstack pid一样的效果。

一些参数

1
2
3
4
5
6
7
8
9
-Xms | 堆的初始Size | 物理内存的1/64|-Xms128m
-Xmx | 堆的最大Size,在生产环境,通常与-Xms设置成相同的值 | 物理内存的1/4|-Xmx128m
-Xmn |年轻代的初始Size和最大Size(Eden+From Survior + To Survivor ) | -Xmn256m
-Xss|线程的堆栈大小|-Xss1m
-XX:NewSize=size | 年轻代的初始size | -XX:NewSize=256m
-XX:MaxNewSize=size | 年轻代的最大size | -XX:MaxNewSize=256m
-XX:NewRatio=ration|年轻代(包括Eden和两个Survivor区)和老年代的比值|2|-XX:NewRatio=4表示年轻代和年老代的占比是1:4,年轻代占整个堆的1/5。设置-Xms、-Xmx与-Xmn时,该参数不需要设置
-XX:SurvivorRatio|Eden区与Survivor区的比值|8|
-Xloggc:filename|Sets the file to which verbose GC events information should be redirected for logging.|-Xloggc:garbage-collection.log

Author: Jcwang

Permalink: http://example.com/2022/08/06/%E5%85%AB%E8%82%A1%E7%9C%8B%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BD%91%E5%9D%80/