JVM中可以生成的最大数量由JVM的堆内存大小、Thread的Stack内存大小、系统最大可创建的线程数量(Java线程的实现是基于底层系统的线程机制来实现的,系统可以生成的最大线程数:/proc/sys/kernel/threads-max,系统可创建的最大pid数:/proc/sys/kernel/pid_max)三个方面影响。
增大堆内存(-Xms,-Xmx)会减少可创建的线程数量(栈区=可用内存-堆-直接内存),增大线程栈内存(-Xss,32位系统中此参数值最小为60K)也会减少可创建的线程数量(每个现象独立栈,栈区空间一定下,单个线程栈占据内存越小,可创建线程越多)。
查看GCRoot:dump出堆文件,使用visualvm查看。
通过top,显示进程运行信息列表,得到高CPU占用的Java进程。找到Java进程PID:jps -mlv
:PID+main方法参数+主类包名+JVM参数。
找到高CUP线程:top -H p java-pid
可以查看该进程下各个线程的cpu使用情况,一般超过80%就是比较高的,80%左右是合理情况,将线程PID转化为16进制,用于之后在堆快照根据nid找到高cpu占用的线程。
获取线程栈:jstack pid >filename.log
,线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,得到线程状态(TIMED_WATING、WATING、BLOCKED、RUNNABLE)、调用关系,CPU执行时间,锁的持有状态(locked),等待锁的状态(waiting to lock),等待事件(Wait on condition )。通过分析高cpu线程的栈快照,以定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU;如果该线程是 VMThread
,则一般是频繁触发GC导致cpu占用高,可以导出系统当前的内存数据,分析得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码。
jstack
查看堆栈信息,找到阻塞点;2,如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以隔一段事件多次导出 jstack
日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程;3,如果通过 jstack
可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。https://tech.meituan.com/2017/12/29/jvm-optimize.html
jstat -gc pid interval count
查看 GC 情况。eden、survior0/1、老年代、元空间容量及使用情况,young/full GC 次数和耗时。
分区大小:活跃数据的大小是指,是Full GC后堆中老年代占用空间的大小。
总大小 3-4 倍活跃数据的大小 新生代 1-1.5 活跃数据的大小 老年代 2-3 倍活跃数据的大小
full gc 触发条件一般是 老年代空间不足,大量对象频繁进入老年代 + 老年代空间释放不掉。
1,系统并发高、或者数据量过大,导致 young gc频繁,且gc后存活对象太多,动态年龄判断:每次gc存货对象年龄加一,当survior区中某个年龄对象占据内存大小占survior区容量的一半,将自动更新晋升老年代的阈值,降低进入老年代门槛,下次GC时将年龄超过阈值的对象被晋升到老年代,内存担保:survivor 区存放不下存活对象,直接放入老年代,老年代迅速堆满。调整内存分配比例 eden,survior0,survior1默认8:1:1,增大survior区占比。
2,程序一次性加载过多对象到内存 (大对象:内存担保),导致频繁有大对象进入老年代 造成full gc。
3,gc频率持续上涨,而且full gc后回收效果不好,可能存在内存溢出的情况,老年代驻留了大量释放不掉的对象, 只要有一点点对象进入老年代就触发 full gc。 dump 排查具体原因。
4,年轻代和老年代的内存都比较低,可能是元数据区加载了太多类,或者大量使用cglib工具生成大量代理类,触发full gc。
5,老年代内存不高,频繁发生full gc,手动System.gc()。
young GC、minor GC:通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率。避免经常发生内存担保和动态晋升年龄计算,对象在新生代得到充分回收,只有生命周期长的对象才进入老年代,这样老年代增速变慢,full GC频率自然也会降低,同时因为单次 Young GC 时间更多取决于存活对象的数量,而非 Eden 区的大小,增大新生空间不会明显增大young GC 耗时。如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。
MaxTenuringThreshold 要尽量合理,不能设置太大,否则有些长寿对象在每次 GC 时都会在两个 Survivor 区之间来回复制,无疑是增加了复制阶段的耗时,新生代不断增长直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。设置过小使对象过早晋升,即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的full GC。
https://blog.51cto.com/u_15257216/2861461
OOM heap space:java堆内存溢出,一般由于内存泄露或者堆的大小设置不当导致内存溢出引起。内存泄露就是某些对象不再被应用程序使用,而GC无法回收的情况,这些对象会无限期保留在Java堆空间中,最终堆积触发(使用了 File 等资源没有回收,ThreadLocal用完未remove),需要通过内存监控软件查找程序中的泄露代码;;堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
jmap -dump:format=b,file=file_name pid_java
实时导出堆信息,对Dump出来的堆转储快照进行分析,哪里发生了内存泄露,哪些对象占据了堆的大部分,这些对象在代码中的位置。确认内存中的对象是否是必要的,也先分是内存泄漏还是内存溢出。如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,这样就能够找到泄漏的对象是通过怎么样的路径与GC Roots相关联的导致垃圾回收机制无法将其回收,掌握了泄漏对象的类信息和GC Roots引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果不存在泄漏,那么就是内存溢出,内存中的对象确实必须存活着,申请的内存超出了JVM能提供的内存大小。那么此时就需要通过虚拟机的堆参数( -Xmx和-Xms)来适当调大参数;从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。
OOM MetaSpace :元空间溢出,存放了被虚拟机加载的类信息,一般出现于大量Class,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。检查代码中是否存在大量的反射生成的代理类操作。根据dump出的堆找到已加载的类中查找最昂贵的类加载器,从此类加载器中可以提取已加载类,并且按实例对此类进行排序,以使可疑对象排在首位,以此排除造成该OOM问题的程序代码。应用长时间运行,类型数据回收条件苛刻,重启强制回收。通过更改方法区的大小来解决,使用MetaspaceSize、MaxMetaspaceSize来修改。
OOM stack:每个线程拥有独立栈,栈区空间大小一定下,可创建线程总数存在上限,不断创建线程将导致OOM,尝试减少每个线程栈大小-Xss,单个线程栈占据内存越小,可创建线程越多。一台服务器可以创建的线程数依赖于物理配置和平台,如最大pid值、可创建线程总数限制。
SOF:JVM 虚拟机栈是有深度的,在执行方法的时候会伴随着入栈和出栈。程序中存在深度递归调用、方法内声明了海量的局部变量导致栈帧大小增大、执行了大量方法,导致线程栈空间耗尽,栈大小设置太小也会出现此种溢出。修复引发无限递归调用的异常代码;排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常);需要执行大量方法或包含大量局部变量,这时可以通过虚拟机参数-Xss来适当地提高线程栈空间限制。
OOM GC overhead limit:当Java进程绝大部分时间都在进行GC,但是只恢复了很少可用堆空间,连续多次GC都是如此那么就会抛出异常。这个错误是为了避免GC最终消耗了100%的CPU,而实际应用程序无法正常进行任何实际工作。分析是哪个地方在大量消耗内存,并且无法正常回收,通过jmap -heap pid查看堆内存占用,看是否本身分配过小,加大内存。
Out of swap space:swap溢出,当JVM请求的总内存大于可用物理内存时,操作系统会开始把数据从内存中换到硬盘中。当数据将交换空间填满,并且物理空间也被填满之后会报出这个错误。操作系统配置的交换空间不足,有别的进程消耗了所有内存资源。可以增加交换空间的大小,但是对于Java的垃圾回收而言,交换是不希望发生的,因为交换过后的内存分配可能让GC暂停时间增加几个数量级。尽量避免别的进程和主要服务端程序竞争内存资源。也可以进行内存升级以从根本上增加内存大小。
OOM native method:本机方法(native method)分配失败,使用操作系统本地工具进行诊断,难度较大。
OOM Direct buffer memory:NIO使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。由于是分配 OS 本地内存,不属于 GC 管辖范围,如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。
通过启动参数 MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值;通过反射调用 sun.misc.Cleaner的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间。
OOM array size:大数组,物理空间连续,无法找到一块足够大的内存容纳当前对象。JVM 限制了数组的最大长度,JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2。尽量不要用一个特别大的数组去储存,可以分成较小的多个数组批量加载需要的数据)
OOM Kill process:当内核检测到系统内存不足时,OOM killer 被激活,然后选择一个进程杀掉。Linux 内核允许进程请求比系统中可用内存更多的内存,但当大多数应用程序都消耗完自己的内存时,这些应用程序的内存需求加起来超出了物理内存(包括swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。升级内存。