目录
视频教程链接:
一、系统内存泄露该怎么处理-闲聊_哔哩哔哩_bilibili
JVM内存图
一、内存泄露场景
什么是内存泄漏
VM在运行时会存在大量的对象,一部分对象是长久使用的,一部分对象只会短暂使用
JVM会通过可达性分析算法和一些条件判断对象是否再使用,当对象不再使用时,通过GC将这些对象进行回收,避免资源被用尽
内存泄漏:当不再需要使用的对象,因为不正确使用时,可能导致GC无法回收这些对象
举例内存泄漏
对象生命周期变长引发内存泄漏
静态集合类
public class StaticClass {
private static final List<Object> list = new ArrayList<>();
/**
* 尽管这个局部变量Object生命周期非常短
* 但是它被生命周期非常长的静态列表引用
* 所以不会被GC回收 发生内存溢出
*/
public void addObject(){
Object o = new Object();
list.add(o);
}
}
生命周期基本与JVM一样长
静态集合引用局部对象,使得局部对象生命周期变长,发生内存泄漏
饿汉式单例模式
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){
if (INSTANCE!=null){
throw new RuntimeException("not create instance");
}
}
public static Singleton getInstance(){
return INSTANCE;
}
}
饿汉式的单例模式也是被静态变量引用,即时不需要使用这个单例对象,GC也不会回收
非静态内部类
非静态内部类会有一个指针指向外部类
public class InnerClassTest {
class InnerClass {
}
public InnerClass getInnerInstance() {
return this.new InnerClass();
}
public static void main(String[] args) {
InnerClass innerInstance = null;
{
InnerClassTest innerClassTest = new InnerClassTest();
innerInstance = innerClassTest.getInnerInstance();
System.out.println("===================外部实例对象内存布局==========================");
System.out.println(ClassLayout.parseInstance(innerClassTest).toPrintable());
System.out.println("===================内部实例对象内存布局===========================");
System.out.println(ClassLayout.parseInstance(innerInstance).toPrintable());
}
//省略很多代码.....
}
}
当调用外部类实例方法通过外部实例对象返回一个内部实例对象时(调用代码中的getInnerInstance方法)
外部实例对象不需要使用了,但内部实例对象被长期使用,会导致这个外部实例对象生命周期变长
因为内部实例对象隐藏了一个指针指向(引用)创建它的外部实例对象
实例变量作用域不合理
如果只需要一个变量作为局部变量,在方法结束就不使用它了,但是把他设置为实例变量,此时如果该类的实例对象生命周期很长也会导致该变量无法回收发生内存泄漏(因为实例对象引用了它)变量作用域设置的不合理会导致内存泄漏
隐式内存泄漏
动态数组ArrayList中remove操作会改变size的同时将删除位置置空,从而不再引用元素,避免内存泄漏
移除指定位置元素,该位置后面的元素全部往前移动一位。最后一个元素设置为null(让gc清除掉)
不关闭资源引发内存泄漏
各种连接: 数据库连接、网络连接、IO连接在使用后忘记关闭,GC无法回收它们,会发生内存泄漏
所以使用连接时要使用 try-with-resource 自动关闭连接
改变对象哈希值引发内存泄漏
一般认为对象逻辑相等,只要对象关键域相等即可
一个对象加入到散列表是通过计算该对象的哈希值,通过哈希算法得到放入到散列表哪个索引中
如果将对象存入散列表后,修改了该对象的关键域,就会改变对象哈希值,导致后续要在散列表中删除该对象,会找错索引从而找不到该对象导致删除失败(极小概率找得到)
public class HashCodeTest {
/**
* 假设该对象实例变量a,d是关键域
* a,d分别相等的对象逻辑相等
*/
private int a;
private double d;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashCodeTest that = (HashCodeTest) o;
return a == that.a &&
Double.compare(that.d, d) == 0;
}
@Override
public int hashCode() {
return Objects.hash(a, d);
}
public HashCodeTest(int a, double d) {
this.a = a;
this.d = d;
}
public HashCodeTest() {
}
@Override
public String toString() {
return "HashCodeTest{" +
"a=" + a +
", d=" + d +
'}';
}
public static void main(String[] args) {
HashMap<HashCodeTest, Integer> map = new HashMap<>();
HashCodeTest h1 = new HashCodeTest(1, 1.5);
map.put(h1, 100);
map.put(new HashCodeTest(2, 2.5), 200);
//修改关键域 导致改变哈希值
h1.a=100;
System.out.println(map.remove(h1));//null
Set<Map.Entry<HashCodeTest, Integer>> entrySet = map.entrySet();
for (Map.Entry<HashCodeTest, Integer> entry : entrySet) {
System.out.println(entry);
}
//HashCodeTest{a=100, d=1.5}=100
//HashCodeTest{a=2, d=2.5}=200
}
}
缓存引发内存泄漏
当缓存充当散列表的Key时,如果不再使用该缓存,就要手动在散列表中删除,否则会发生内存泄漏
如果使用的是WeakHashMap,它内部的Entry是弱引用,当它的Key不再使用时,下次垃圾回收就会回收掉,不会发生内存泄漏
public class CacheTest {
private static Map<String, String> weakHashMap = new WeakHashMap<>();
private static Map<String, String> map = new HashMap<>();
public static void main(String[] args) {
//模拟要缓存的对象
String s1 = new String("O1");
String s2 = new String("O2");
weakHashMap.put(s1,"S1");
map.put(s2,"S2");
//模拟不再使用缓存
s1=null;
s2=null;
//垃圾回收WeakHashMap中存的弱引用
System.gc();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍历各个散列表
System.out.println("============HashMap===========");
traverseMaps(map);
System.out.println();
System.out.println("============WeakHashMap===========");
traverseMaps(weakHashMap);
}
private static void traverseMaps(Map<String, String> map){
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry);
}
}
}
使用 ThreadLocal 造成内存泄露
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程的安全,但是使用不当,就会引起内存泄露。
一旦线程不在存在,ThreadLocal 就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到 ThreadLocal 来保留线程池中线程的变量副本时,ThreadLocal 没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。
解决办法是不在使用 ThreadLocal 时,调用 remove() 方法,该方法删除了此变量的当前线程值。不要使用 ThreadLocal.set(null),它只是查找与当前线程关联的 Map 并将键值对设置为当前线程为 null。
try {
threadLocal.set(System.nanoTime());
}
finally {
threadLocal.remove();
}
finalize方法
之所以会造成内存泄漏,是因为在垃圾回收的时候,如果重写了finalize方法而且该对象的finalize方法没有被执行过,但是进入队列之后一直没被调用就会一直占用内存空间
二、创建内存泄露案例
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@RestController
@RequestMapping(value = "/demo")
@Slf4j
public class DemoController {
private static List<Object> list = new LinkedList<>();
@GetMapping("/leak")
public String leak(){
for (int index =0;index<1000; index++){
UserInfo userInfo = new UserInfo();
list.add(userInfo);
}
return "success";
}
@Data
class UserInfo{
private String username;
}
}
后台启动命令
nohup java -Dserver.port=8080 -jar demo.jar >> log.txt
三、排查流程
动态监控内存
# 每隔3秒执行一次
vmstat 3
排查发现出现内容泄露
排查jvm内存分配情况(内存分配合理)
jmap -heap 进程id
排查是否存在异常未清除类
jmap -histo:live <pid>|sort -k 2 -g -r|less
对象实例的数量。
对象的占用空间大小(以字节为单位)。
对象类的全名。
排查回收对象情况
jmap -finalizerinfo 进程id
下载堆内存
jmap -dump:live,format=b,file=myjmapfile.hprof 进程id
堆分析工具
虚拟机堆转储快照分析工具
使用jdk的 jvisualvm工具(建议采用):
- 打开命令行或终端窗口。
- 输入
jvisualvm 并按 Enter 键启动
- 在 jvisualvm 中,选择 "File" -> "Load...",然后选择要打开的 HPROF 文件。
- 点击 "Open" 或 "OK" 来加载并分析 HPROF 文件。
使用eclipse memory analyzer分析内存类依赖关系