你好,最近用WPS总会会遇到未知的问题,突然闪退,没有保存的东西全部都没有了
1172
2022-05-29
为了加深自己理解及方便日后复习,将学到的一些Java内存相关知识总结一下,如有错误,欢迎大家指正
1.JAVA内存基础
1.1 Linux与进程内存模型
硬件层面
从硬件层面上看,Linux系统的内存空间由两部分构成:物理内存和磁盘SWAP分区。物理内存是Linu使用的主要内存区域,当物理内存不足时,Linux会把一部分相对冷内存数据放到磁盘的SWAP分区,以便腾出更多的可用内存空间;而当命中数据处于SWAP分区中时,就会将其置换回物理内存中
系统层面
从Linux系统层面上看,除了引导系统的BIN区,整个内存空间被分为两个部分:内核态内存(Kernel Space)、用户态内存(User Space)。内核内存是Linux自身使用的内存空间,主要提供给程序调度、内存分配、硬件资源驱动等程序逻辑使用。用户态内存是提供给各进程的主要内存空间,Linux使用虚拟内存技术给各个进程提供相同的虚拟内存空间,这机制确保进程之间相互独立、互不干扰。
进程层面
从进程的角度,进程能直接访问的用户态内存(虚拟内存空间)被划分为5个部分
代码区:存放了应用程序的机器代码,运行过程中代码不能被修改,具有只读和固定大小的特点。
数据区:存放应用程序中的全局数据,静态数据和常量字符串等,其大小也是固定的。
堆区:运行时程序动态申请的空间,程序运行时动态申请、释放的内存资源。
未使用区:分配新内存空间的预备区域。
栈区:存放函数的传入参数、临时变量、返回地址等数据。
1.2 Java8 JVM内存模型
内存模型大致可以分为五个部分
程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,是线程私有的区域,各条线程之间的计数器互不影响,独立存储。在虚拟机概念模型里,字节码解释器通过改变这个计数器的值来执行不同的字节码指令,分支、跳转、循环、异常处理、线程恢复等基础操作都会依赖这个计数器来完成。
虚拟机栈(VM Stack)
JVM栈是线程私有的内存区域。它描述的是java方法的内存,每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个栈帧从入栈到出栈就代表着一个方法从调用到返回的过程。
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈类似,不同的是其为Native方法服务,而虚拟机栈为java方法服务。是规范中指明的概念,但是实现并无规定,如HotSpot直接将本地方法栈和虚拟机栈合并实现。
堆(Heap)
堆区是用来分配实例对象的空间,各个线程共享使用,由垃圾收集器自动回收管理。
元空间(MetaSpace)
用来存放类的元数据,也就是数据结构信息,如类常量,字符串常量,类定义等数据。java8之前,是由永久代进行存储。
2.JAVA OOM常见场景
2.1 内存溢出
2.1.1 堆内存溢出
问题现象为,创建对象失败,提示异常为java.lang.OutOfMemoryError:Java heap space
其导致原因为
代码BUG,使用的静态容器类(List等)没有及时清除对象
读入数据过大,比如在读取数据库表时,一次性捞出全部数据
数据积压,在生产者消费者模型中,生产很快,消费很慢,数据来不及处理,队列又没有限制,长时间运行导致内存溢出
解决方法为
确保代码没BUG时,通过压测调整-Xms参数,-Xmx参数
大量数据如大文件加载,大批量数据查询时分批处理
队列添加限制,提高执行效率,加速垃圾回收,避免并发高时无足够内存空间
import java.util.ArrayList; import java.util.List; /** * 堆内存溢出 * —Xmx10M * * @since 2022-04-15 */ public class HeapOom { public static void main(String[] args) { List
2.1.2 MetaSpace内存溢出
问题现象为 程序运行时报java.lang.OutOfMemoryError:Metaspace
该问题原因为
应用代码过多
引用三方库多
动态生成加载类
解决方法为
调整-XX:MaxMetaSpaceSize参数
不需要的三方库及时删除
动态生成类时做好压力测试
import java.util.ArrayList; import java.util.List; import javassist.CannotCompileException; import javassist.ClassPool; /** * 元数据溢出 * -XX:MaxMetaspaceSize=10m -XX:-UseCompressedClassPointers * * @since 2022-04-15 */ public class MetaspaceOom { public static ClassPool pool = ClassPool.getDefault(); public static void main(String[] args) throws CannotCompileException { List
2.1.3 堆外内存溢出
问题现象为,程序报java.lang.OutOfMemoryError … Native Method
问题原因就是采用了很多NIO相关操作,没有及时释放
解决方法则是减少长生命周期的堆外内存引用,及时释放空间
import java.lang.reflect.Field; import sun.misc.Unsafe; /** * 堆外内存溢出 * -Xmx10m * * @since 2022-04-15 */ public class DirectMemoryOomError { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); while (true) { unsafe.allocateMemory(1024 * 1024); } } }
2.1.4 栈内存溢出
问题现象为,应用抛出异常java.lang.StackOverflowError
问题原因则是
方法调用太深,导致栈中内存被消耗殆尽,主要是递归操作
问题解决方法是
合理设置-Xss值
递归操作注意终止条件,关注递归层数
/** * 栈溢出 * * @since 2022-04-15 */ public class StackOverflowOom { public static void testStack(){ Byte[] temp = new Byte[1024*1024]; testStack(); } public static void main(String[] args) { testStack(); } }
2.1.5 本地线程内存溢出
问题现象为,应用抛出异常java.lang.OutOfMemoryError:unable to create new native thread error.
问题原因为
线程数过多,在新创建线程时,剩余内存无法满足线程创建需求
线程数量超过操作系统限制
问题解决方法为
检查操作系统线程限制,如Linux,可以用ulimit -a查看
应用明确最大线程数,使用线程池,不随意创建线程及线程池
import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * 本地线程内存溢出 * -XX:ThreadStackSize=1G * * @since 2022-04-15 */ public class UnableCreateNativeThreadError { public static void main(String[] args) { while (true) { Executor pool = Executors.newCachedThreadPool(); pool.execute(()-> System.out.println("Test")); } } }
2.1.6 数组超限内存溢出
问题现象为,应用抛出异常java.lang.OutOfMemoryError: Requested array size exceeds VM limit
问题原因为
试图分配超出该平台下JVM最大数组限制大小的数组
解决办法
避免分配大数组
/** * 数组内存超限制 * * @since 2022-04-15 */ public class ArrayLimitOom { public static void main(String[] args) { int[] arr = new int[Integer.MAX_VALUE-1]; } }
2.1.7 超出交换分区
问题现象为,启动时抛出java.lang.OutOfMemoryError:Out of swap space
问题原因是
jvm启动时,按照-Xms及其他参数申请内存,当请求内存大于可用内存时,进程请求分配内存失败
解决方法是
清理环境内存,腾出空间
2.1.8 OS Killer进程
问题现象为,java进程突然消失了,日志未见明显异常,用dmesg发现Out of memory: Kill process ** (java) score ** or sacrifice child
问题原因为
Out-Of-Memory killer机制监控机器的内存资源,当发现内存临近耗尽,就会扫描所有进程(按照一定规则计算,内存占用、时间等),然后kill掉最高得分的进程,保护机器运行环境
2.1.8 GC回收超时
问题现象为,应用响应慢,抛出java.lang.OutOfMemoryError:GC overhead limit exceeded
问题原因为:
应用在内存不足时进行GC操作回收内存,而JVM花费大量时间进行GC却只回收了些微内存,超过一定次数便会触发该错误
解决方法:
减少对象生命周期,尽量做到朝生夕灭
可分析Stop World停顿日志周围GC情况
import java.util.HashMap; import java.util.Map; import java.util.Random; /** * GC回收超时 * -Xmx10m * * @since 2022-04-15 */ public class OverHeadLimitOom { public static void main(String[] args) { Map
2.2 内存泄漏
问题现象为:实例数不断增加
问题原因为
从GC ROOT对象往下,无用对象依然是可达的
实际就是一些不用的,无引用的,应该被GC回收到的对象,无法被回收,一直保留,导致内存泄漏,一般常见于HASH容器中,修改了KEY的值,导致后续删除操作实际上无法删除,一直存留。
/** * 内存泄漏 * -Xmx20m * * @since 2022-04-15 */ public class MemoryLeakOom { static class Email { private String address; public Email(String address) { this.address = address; } public int hashCode() { return address.hashCode(); } } public static void main(String[] args) { HashSet
3 问题定位
问题场景:应用运行一段时间后,悄无声息结束了,或者容器重启了
定位方法;
步骤1: dmesg|grep -i kill 观察是否是OS kill了进程
步骤2: JVM启动参数查看
jinfo -flags $PID
步骤3: 堆内存分析
jmap -heap $PID 查看堆内存
jmap -histo $PID 查看堆内对象
步骤4: 线程情况分析
jstack -m $PID 查看线程
内存占用分析
在启动时加入启动参数-XX:NativeMemoryTracking=summary
创建基线 jcmd $pid VM.native_memory baseline
查看内存变化 jcmd $pid VM.native_memory summary.diff
需要注意的是NativeMemoryTracking功能大约会有5%-10%的性能损耗,测试环境添加尚可。
Java
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。