JVM内存溢出分析总结

开个文总结下我遇到的内存溢出问题,放个JVM的图以供参考:

pzqqqs.jpg

从内存溢出分析的角度来看,内存主要分为栈内存、堆内存、直接内存和MetaSpace(java8之前是堆内的永久代)。
下面主要讲下这几种内存溢出的场景:

1. StackOverflowError 栈内存溢出

这里stack指的是线程的独立内存空间,主要保存方法执行信息,每个方法调用时都会产生一个栈帧,当栈深度超过虚拟机分配给线程的栈大小时就会出现StackOverflowError。
出现这个问题有以下几种可能:

  • 分配的栈内存太小,这种不太常见 JVM默认栈内存-Xss1024k,足够一般线程使用。
  • 方法循环调用,A方法调用B,B调用C,C又来调用A,就会出现。大部分栈溢出都是这种情况。
  • 方法迭代,比如分页查询,总数1W,每页查一个,查到hasNext为true就继续查,迭代1W次,也有可能出现溢出。

问题出现后,根据stackTrace错误堆栈基本上就可以定位到问题位置。

PS:高并发应用创建过多线程也可能引起栈内存OOM,有开源、节流两种思路应对:

  • 开源:提升栈内存总量
    • 扩大服务器内存
    • 减少堆内存-Xms -Xmx
  • 节流
    • 减少每个线程分配的栈内存 -Xss512k或256k
    • 通过负载均衡、拒绝&延迟响应等策略减少线程并发数

2. OutOfMemoryError: Java heap space 堆内存溢出

堆内存主要用来存放java对象,堆内存不够了就会报错。
问题有两种可能的原因:

  1. 堆内存设置太小,无法满足应用正常运行的需要(包括基本运行以及承载高峰期的内存压力);
  2. 应用存在内存泄漏,部分对象无法回收,导致占用内存越来越多。

遇到这种情况,可以

  • 先把最大堆内存-Xmx改大一些
  • 进行JVisualVM内存抽样或者MAT等工具dump分析,查看内存中对象分步情况。重点观察内存占比较大的实例和线程。
  • 还可以监视应用的堆内存,看一下内存的增长是否有规律。

也可以使用jmap等java命令来进行dump分析,详情参考深入解析OutOfMemoryError

另外,如果多次报错时打出的strackTrace都在同一个位置,建议着重看一下。如果是某些框架内部报错,先去官网或者Google一下。

3. OutOfMemoryError: PermGen space / MetaSpace 永久代或Metaspace溢出

这里主要讲类信息导致的内存溢出,类信息在java8之前存放在永久代,java8之后存在Metaspace中。
应用加载的类信息主要分两种: class文件和运行过程中动态生成的类。除非特地限制-XX:MaxMetaspaceSize或-XX:MaxPermSize大小,一般该问题都是由动态类引起的。
可以通过JVisualVM监视MetaSpace和类加载数,MetaSpace的增长与类加载的关系。

可以通过以下方式定位问题原因:

  • 从错误堆栈找到问题抛出点
  • 本地调试抛出点代码,加上JVM参数 -verbose:class,打印类加载信息
  • 找到动态加载类的代码,分析如何解决

之前解决过的案例是由JAXB解析XML数据引起的,JAXBContext.newInstance()会生成一些动态类和方法,在JDK的JAXB工具类中通过WeakReference< Cache >来缓存JAXBContext,但是容易被回收,而且不能存放多个JAXBContext,如果需要解析多种格式的XML数据,还是会经常调用JAXBContext.newInstance()。

1
2
3
4
5
6
7
8
9
private static final class Cache {
final Class type;
final JAXBContext context;

public Cache(Class type) throws JAXBException {
this.type = type;
this.context = JAXBContext.newInstance(type);
}
}

最后采用Map来存放JAXBContext。

1
private static Map<Class<?>, JAXBContext> contextMap = new ConcurrentHashMap<>();

还有一种类似的情况,使用Apache-CXF生成的Webservice-client,每次初始化也会生成一些动态类,应该使用单例模式避免重复初始化。
另外,CGLib字节码增强实现的动态代理以及Groovy等动态语言、动态产生的JSP文件等都会动态加载类信息,使用时需要注意。

4. 堆外内存溢出

java NIO通信时默认会使用ByteBuffer缓存数据,如果用完忘记调用ByteBuffer.release()方法,容易导致内存泄漏。
Tips:

  • JVisualVm 装上Buffer Pools插件就可以监视java进程堆外内存的使用情况了,分别显示Direct、Mapped
  • 使用了Netty应用在启动时可以用-Dio.netty.leakDetection.level=PARANOID 检测一下是否有内存泄漏情况,根据错误堆栈可以定位到泄漏点

WinRAR文件分卷压缩

分类Tips就是记录一些小技巧,很简单,可能很多人都知道,但是不知道的时候就比较容易捉急,比如想上传个文件,但是超过单个文件最大限制,那怎么传呢?

  • 压缩文件,可是压完了还是太大,咋办呢?
  • 文件要是能切成几块就好了,怎么切呢?

用WinRAR分卷压缩:
pqyWY4.jpg

刚知道的时候,感觉像是突破了知见障

  • 原来WinRAR还有这功能,基本天天用却视而不见
  • 原来这么简单,白瞎了我到处找办法

踩坑记2—SpringDataJPA合集

lombok@Data注解 pojo – 存在外键关联

  • toString()导致StackOverFlow
  • 作为DTO输出,JSON序列化报错 (属性添加@JsonIgnore解决)

依靠数据库unique防业务字段重复

  • 示例中,由于saveDevice被事务控制,若device与已有设备冲突,saveDevice()执行完后提交事务时才会报错,此时

已调用外部接口上报了新增设备信息,本系统回滚save操作,就会导致数据不一致。

1
2
3
4
5
@Transactional
public void saveDevice(Device device){
deviceDao.save(device);
outerService.reportDeviceAdded(device);
}

for循环内调用@Transactional方法,偶现异常

Spring - No EntityManager with actual transaction available for current thread - cannot reliably process ‘persist’ call
将外部方法也标记@Transactional,循环内部捕获异常,内部改为@Transactional(propagation = Propagation.REQUIRES_NEW)

外键关联对象先后save时偶现

  • 错误问题:
    org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.xxx.DeviceChannel with id 12;
  • 代码:
    1
    2
    deviceChannelDao.save(channels);
    deviceDao.save(device);

deviceChannel与device外键关联,修改后代码:

1
2
device.setDeviceChannels(deviceChannelDao.save(channels));
deviceDao.save(device);

踩坑记1

1. 接口返回慎用Http 500

500 Internal Server Error 代表服务故障,功能完全无法使用
接口遇到鉴权失败、业务异常等返回相应错误码,或者返回200并在报文体中返回失败信息

Http状态码:

200~299 成功

300~399 重定向到Header Location地址

400~499 客户端错误码

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 405 Method Not Allowed

    500~599 服务器错误码

  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable

2. java常量修改

  • check所有引用
  • 修改后项目target清空重新编译。

PS: 常量会被编译到引用类的class文件中,但是java文件没修改,class不会被rebuild

3. synchronized(obj) obj不能为空

4. Spring的BeanUtils.copyProperties

  • 属性类型要一致
  • source和target切忌填反

5. 参数int的方法传入空Integer值会抛出NPE

6. Stream 不能重复消费

Linux修改文件权限

三位二进制树表示权限值:

  1. 可读
  2. 可写
  3. 可执行

例如:101 =5,表示可读可执行,不可写


每个文件分3组权限,
1.所属人权限
2.同组用户权限
3.其他组用户权限

使用以下形式修改文件权限。

1
chmod -R 权限值 文件名

-R表示递归
对文件夹及其所有内容开放所有权限:

1
chmod -R 777 foldname

PS:查看文件权限命令

1
ls -l

授权

1
sudo -s