Java常见问题分析

https://yq.aliyun.com/articles/514418?spm=a2c41.11123433.0.0

摘要: 一、JVM简介1.JVM内存模型实际占用内存大小:-XX:MaxPermSize + -Xmx + -Xss + -XX:MaxDirectMemorySize 如图一: 主要分为:非堆内存+堆内存+栈内存+堆外内存 JVM主要管理两种类型的内存:堆和非堆。

一、JVM简介
1.JVM内存模型
实际占用内存大小:-XX:MaxPermSize + -Xmx + -Xss + -XX:MaxDirectMemorySize
如图一:

主要分为:非堆内存+堆内存+栈内存+堆外内存
JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的
在JVM中堆之外的内存称为非堆内存(Non-heap memory)。
Java虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在Java虚拟机启动时创建的。
堆外内存:DirectMemory是java nio引入的,直接以native的方式分配内存,不受jvm管理。这种方式是为了提高网络和文件IO的效率,避免多余的内存拷贝而出现的。
栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。

2.堆内存分三代
共划分为:年轻代(Young Generation)、年老代(old generation tenured)和持久代(Permanent Generation)。
持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代:[Eden/Survisor/Survisor]
    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
    年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:
    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:
    用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
新生代和老年代都是堆内存空间
堆的内存模型:
[Eden|from|to]-[old]
\__young____/--\old/
默认的Edem:from:to=8:1:1(可以通过参数–XX:SurvivorRatio来设定),即:Eden=8/10的新生代(young)空间大小,from=to=1/10 的新生代空间大小。
新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young )

3.GC
Scavenge GC
    一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
    新生代通常存活时间较短,因此基于复制算法来进行回收,所谓复制算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和其中一个Survivor,复制到另一个之间Survivor空间中,然后清理掉原来就是在Eden和其中一个Survivor中的对象。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到 survivor,最后到老年代。
Full GC
    对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
    旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。
年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用
上一次GC之后Heap的各域分配策略动态变化

4.JVM提供的GC方式
JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)
1)串行GC
    在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定
2)并行回收GC
    在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数
3)并行GC
    与旧生代的并发GC配合使用
如图二:

二、java常见问题处理
1.进程异常退出
=======================================
可能的原因:
1)系统OOM Killer //grep kill /var/log/messages,查看kill时对应的内存占用total-vm,anon-rss,file-rss
2)人为的kill  //history |grep -i kill
3)代码代用system.exit() //反查代码
4)JVM自身bug //DirectMemory 的默认大小是64M,而JDK6之前和JDK6的某些版本的SUN JVM,存在一个BUG,在用-Xmx设定堆空间大小的时候,也设置了DirectMemory的大小。加入设置了-Xmx2048m,那么jvm最终可分配的内存大小为4G多一些,是预期的两倍。
解决方式是设置jvm参数-XX:MaxDirectMemorySize=128m,指定DirectMemory的大小。
5)内存问题    //内存不足,比如申请一个大的对象的时间。不能及时gc
6)native stack溢出导致 //不受jvm控制,但是被java占用的
致命错误出现的时候,JVM生成了hs_err_pid.log这样的文件,其中往往包含了虚拟机崩溃原因的重要信息
默认创建在工作目录:可以结合find -name hs_err_pid* 
hs_err_pid.log文件内容

1
2
3
4
5
6
7
8
9
1)触发致命错误的操作异常或者信号
2)版本和配置信息
3)触发致命异常的线程详细信息和线程栈
4)当前运行的线程列表和它们的状态
5)堆的总括信息
6)加载的本地库
7)命令行参数
8)环境变量
9)OS的CPU信息

2.OOM
=======================================
1)Java heap space/GC overhead limit exceeded
    dump分析:启动参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=  或者jmap -dump:format=b,file=文件名 [pid] 
    使用mat工具分析heapdump
    占用内存较大代码优化
    如果内存占用不多:可能是创建了一个大对象导致,根据日志分析创建大对象的时间/jstack分析是否存在死循环
2)PermGen space
    调大PermSize
    是否动态加载Groovy脚本
    是否有动态生成类逻辑,比如使用cglib大量动态生成类
3)Direct buffer memory 
    默认占用-Xmx相同的内存 //-XX:MaxDirectMemorySize=1G调整
    网络通信使用Netty但是未限流
    分析代码中是否使用DirectyBuffer未合理控制
4)java.lang.StackOverflowError
    调小-Xss使每个线程栈的内存占用减小 //设置每个线程的堆栈大小
    调小-Xmx,给栈更多的内存空间
    分析代码中是否存在不合理的递归
5)request bytes for Out of swap space 
    地址空间不够 //64bitos
    物理内存不够:jmap -histo:live pid ,如果内存明显减少,说明是directbuffer问题,通过-XX:MaxDirectMemorySize设定
    btrace Inflater/Deflater
6)unable to create new native thread 
    ulimit -a //vim /etc/security/limits.conf添加
    * soft noproc 11000
    * hard noproc 11000
    * soft nofile 5000  //修改限制
    * hard nofile 5000  //修改限制
/proc/sys/kernel/pid_max 操作系统线程数限制
/proc/sys/vm/max_map_count 单进程mmap的限制会影响
/proc/sys/kernel/thread-max
/proc/sys/vm/max_map_count
max_user_process(ulimit -u)

7)Map failed 
如图三:

3.CPU过高
=======================================
基本命令:top,vmstat,mpstat,sar,tsar 
us高:用户进程消耗的CPU时间多
    原因:full gc,CMS gc,代码死循环,整体消耗CPU多等
    方案:查看gc.log ;jstat -gcutil [pid] //https://github.com/oldratlee/useful-scripts/blob/master/show-busy-javathreads.sh
sy高:内核消耗的CPU时间多
    原因:锁竞争激烈,线程主动切换频繁
    方案:jstack查看是否有锁,或者是否是线程切换频繁。//修改为无锁结构,线程切换频繁改为通知机制
        btrace ConditionObject.awaitNanos 是否存在很小值,最好是ms级别
wa高:等待IO的CPU时间多,随机IO太多或者磁盘性能问题
    原因:io读写频繁
    方案:iostat,iotop,lsof//增加缓存,同步改为异步,随机写入改为顺序写

4.应用无响应
=======================================
CPU高
OOM
死锁     jstack -l 查看对应死锁的线程 //去掉死锁,
线程池满    //增大线程池,减少耗时

5.环境变量异常
=======================================
时区错误/变量错误/编码方式错误
解决方案:
    jinfo 查看具体的启动参数

6.调用超时    
=======================================
服务端慢/服务端或调用端gc/服务端或调用端CPU高/大对象序列化慢/网络问题,丢包

=======================================
三、案例分析
案例一:"PermGen space"
java.lang.OutOfMemoryError: PermGen space
Exception in thread "http-bio-17788-exec-75" 
明显可以看出是老年代的内存溢出,说明在容器下的静态文件过多,比如编译的字节码,jsp编译成servlet,或者jar包。
解决此问题,修改jvm的参数 permsize即可,permsize初始默认为64m。

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配


你可能感兴趣的:(Java,java)