技术问答集录(一)(JVM,OOM,线程池,JDBC insert)
问题:
- 如何排查Java应用程序OOM、使用CPU高的问题?
- 开发同学发现使用JDBC批量insert数据还是很慢,怎么排查?
- Java的线程池有哪些重要参数?如何实现激进的线程池?
一.如何排查Java应用程序OOM、使用CPU高的问题?
排查Java应用程序OOM
1.什么是OOM?为什么会发生OOM?
Out Of Memory,来源于java.lang.OutOfMemoryError。当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个异常。
2.什么情况下会导致OOM?
在JVM的运行时数据区域,除了"程序计数器"外,均有可能发生OutOfMemoryError的情况。
Java服务OOM,最常见的原因为:
①有可能是内存分配确实过小,而正常业务使用了大量内存
②内存泄露,某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽
③内存溢出,某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接
3.发生了如何定位?
①确认是不是内存本身就分配过小
方法:jmap -heap [pid]
可以通过命令jmap -heap pid 或 通过grafana查看新生代,老生代堆内存的分配大小以及使用情况,看是否本身分配过小。
②找到最耗内存的对象
方法:jmap -histo:live [pid]| more
③确认是否是资源耗尽
工具:pstree 或 netstat
查看进程创建的线程数,以及网络连接数,如果资源耗尽,也可能出现OOM。
使用工具:arthas
检查是否有死锁,大对象等
CPU高
定位方法:
1.使用top 定位到占用CPU高的进程PID
top
2.获取线程信息,并找到占用CPU高的线程
ps -mp [pid] -o THREAD,tid,time | sort -rn
3.将需要的线程ID转换为16进制格式
printf "%x\n" [tid]
4.打印线程的堆栈信息
jstack [pid]|grep [tid] -A 30
通过堆栈信息可定位到占用cpu的方法
cpu消耗高的可能原因
- 使用了复杂的算法,比如加密、解密
- 压缩、解压、序列化等操作
- 代码bug,比如死循环
二. 开发同学发现使用JDBC批量insert数据还是很慢,怎么排查?
排查:
- 检查数据库连接是否配置rewriteBatchedStatements=true
- 比较单条插入与批量插入的时间是否有差异,验证批量插入是否生效
- SHOW FULL PROCESSLIST命令查看当前表是否存在大量线程竞争表的锁
- 检查是否有锁表情况
- 检查是否有过多的索引
- MySQL批量新增executeBatch()方法默认会忽视批量更新语句,会把我们期望批量执行的SQL语句拆分,然后一行一行执行,需要在JDBC的连接的url加上参数rewriteBatchedStatements=true才会批量执行。
三. Java的线程池有哪些重要参数?如何实现激进的线程池?
Java的线程池有哪些重要:7个
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
corePoolSize: 队列未满时,核心线程数; 当新创建线程小于核心线程数时,线程池创建新线程执行任务
maximumPoolSize:队列满后线程能够到达的最大并发数;当新创建线程,线程池数量corePoolSize+ workQueue.size()>maximumPoolSize会执行handler
keepAliveTime: 线程池中超过corePoolSize数目的空闲线程最大存活时间;
TimeUnit:存活时间单位
workQueue :队列,大于corePoolSize时,新加入线程放入队列
handler:当队列和最大线程池都满了之后的饱和策略,RejectedExecutionHandler handler //拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
拒绝策略包含以下 4 种:
①CallerRunsPolicy:提交任务的线程自己去执行该任务。
②AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
③DiscardPolicy:直接丢弃任务,没有任何异常抛出。
④DiscardOldestPolicy:丢弃最老的任务,把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
实现激进的线程池:
线程池默认的工作方式是:
当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中。当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止。而实现激进的线程池可以通过:先把最大线程数用完,然后再提交任务到队列里面去。
例如 Tomcat线程池的实现思路:
①重写任务队列的 offer 方法。使线程池在提交任务时判断:若当前正在运行的线程数少于最大线程数时,就创建新线程。
②实现一个自定义的拒绝策略处理程序。如果队列满了,则会等待指定时间后再次放入队列。如果再次放入队列的时候还是满的,则抛出拒绝异常。
(1) 无线程池的工作队列是无界阻塞队列,由于线程数量是有限的,而队列的容量是无限的,当线程的处理速度远小于任务被提交到工作队列的速度时,会导致工作队列积压大量的任务,这些任务是
无法被GC,极端情况下会导致OOM
(2) Executors.newCachedThreadPool(),这个方法会创建一个maximumPoolSize = Integer.MAX_VALUE ,这个线程池会导致每当有任务被提交到线程池时,就会创建一个线程,当任务有几千上万个时,
就会创建成千上万个线程,每个线程都有栈内存,需要占据一定的内存空间,这样就导致服务器的内存快速耗尽,最终服务器崩溃,即使没有崩溃,大量的线程会导致频繁的上下文切换,CPU负载会非
常的高,系统越来越慢,最终挂掉
设置线程池数量:线程数= Ncpu(CPU核心线程数)*(1+w/c)
之前看到的一遍文章https://mp.weixin.qq.com/s/9siq9gsBa-OuzOSc62y1lw