以下代码中,将实体对象做为map的key:
public class HeapDemo1 {
public static void main(String[] args) throws Exception{
int count = 0;
HashMap<Students, String> map = new HashMap<>();
while (true) {
//被100整除时休眠10ms,是让一下CPU时间片给Visual VM,不然一直while true,Visual分不到CPU时间片,会监控不到,处于一种假死状态
if(count++ % 100 == 0){
Thread.sleep(10);
}
Students students = new Students();
//学生的ID相同,按理说是同一个对象
students.setId(1L);
students.setName("甲");
map.put(students,"test");
}
}
}
而这个实体类没有正确重写equals+hashcode:
@Setter
@Getter
public class Students {
private Long id;
private String name;
private byte[] info = new byte[1024 * 1024];
}
运行,OOM,Visual VM观察堆内存走向:
原因分析:
哈希表的数据结构是数组+单向链表的组合体,上面map.put,底层则是先调用key的hashcode找位置,然后再调key的equals看是否覆盖这个位置已有的key。因此,对上面这个以实体类为key的写法:
因此,最后就会有大量对象id相同的学生对象(同一个对象)被挂在hashmap里,最终OOM。
修改实体类,重写equals和hashcode:
@Setter
@Getter
public class Students {
private Long id;
private String name;
private byte[] info = new byte[1024 * 1024]; //byte属性让对象大一点,好测试
/**
* 只比id属性即可,ID相同,则同一个对象
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Students students = (Students) o;
return Objects.equals(id, students.id);
}
/**
* id属性
*/
@Override
public int hashCode() {
return Objects.hash(id);
}
}
此时,Visual VM观察堆内存走向:
注意避坑:
public class Outer {
private byte[] bytes = new byte[1024 * 100];
private String name = "test";
class Inner {
private String name;
//创建内部类对象时,获取外部类的name属性,赋值给内部类的name属性
public Inner() {
this.name = Outer.this.name;
}
}
public static void main(String[] args) throws Exception{
int count = 0;
ArrayList<Inner> innerArrayList = new ArrayList<>();
while (true) {
//同上,给Visual VM一点CPU时间片
if (count++ % 100 == 0) {
Thread.sleep(10);
}
innerArrayList.add(new Outer().new Inner());
}
}
}
Visual查看,Inner对象放集合了,可达,没被回收是合理的,但Outer对象不用了,属于泄漏:
原因分析:
非静态的内部类(static),默认持有外部类,创建完的内部类对象会关联到外部类,导致外部类对象即使代码中不用了,也不可被回收。Dubug验证:
解决办法:改为静态内部类
public class Outer {
private byte[] bytes = new byte[1024 * 1024];
public List<String> newList(){
List<String> list = new ArrayList<String>(){{ //匿名内部类在非静态方法中被创建
add("1");
add("2");
}};
return list;
}
public static void main(String[] args) throws Exception{
int count = 0;
ArrayList<Object> innerArrayList = new ArrayList<>();
while (true) {
System.out.println(count++);
innerArrayList.add(new Outer().newList());
}
}
}
运行,OOM
原因:匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。
Outer对象不能被回收:
解决办法:使用静态方法,可以避免匿名内部类持有调用者对象
如果是new的线程,不remove也行,因为当线程被回收时,ThreadLocal也同样被回收。
public class HeapDemo2 {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws Exception{
while (true) {
new Thread(() -> {
threadLocal.set(new byte[1024 * 1024 * 100]);
}).start();
Thread.sleep(10); //歇会儿,别加太快,以至于GC都C不及
}
}
}
换成线程池,线程被复用,不remove则内存泄漏:
public class HeapDemo2 {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE, 0, TimeUnit.DAYS, new SynchronousQueue<>());
int count = 1;
while (true) {
System.out.println(count++);
pool.execute(() -> {
threadLocal.set(new byte[1024 * 1024 * 100]);
//remove后解决了内存泄漏
//threadLocal.remove();
});
Thread.sleep(10);
}
}
}
解决:线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象
大量把字符串手动加到字符串常量池,导致OOM。JDK6下,字符串常量池在永久代,调小永久代,以便快速看到效果,JDK8,字符串常量池在堆里,同理可调小堆:
-XX:MaxPermSize=15M
-Xmx10M -Xms10M
Demo:
public class HeapLeak {
public static void main(String[] args) {
int i = 0;
ArrayList<String> list = new ArrayList<>();
while (true) {
//String.valueOf(i++).intern();
list.add(String.valueOf(i++).intern());
}
}
}
建议:不要将随机生成的字符串加入字符串常量
静态变量的生命周期长(类的生命周期),因此将对象保存在静态变量中,长时间不被释放,后面又不再用的话,白占地方,内存泄漏。
优化点:
同类型的注意点,还有:
如下,设置-Xmx500m,一个Bean,懒加载时微服务启动不受影响
@Lazy
@Component
public class TestLazy{
private byte[] bytes = new byte[1024 * 1024 * 1024];
}
ConfigurableApplicationContext run = SpringApplication.run(SystemApplication.class,args);
//如果程序运行过程中,这个类一直没被用到,那这个对象就不会被创建
//这里获取,就OOM
TestLaxy bean = run.geteBean(TestLazy.class);
给大对象设置过期时间:
tay-catch-finally,没有关闭资源,不一定就内存泄漏,但:
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS)){
//....
}