avatar

Java内存优化

Java代码中的内存泄漏

1
2
3
1.内存泄漏的原因是对象上有强引用
2.虽然Java程序运行完后,内存会回收,但应让对象尽早被回收
3.出现内存泄漏后,会导致Stop The World和OOM异常

finalize方法

1
2
3
1.Object类中的方法,任何类都可以重写该方法
2.当JVM通过根可达算法,判断某对象可以被回收后,会判断该类是否重写了finalize方法,如果没有,将直接回收;如果重写了finalize方法,会将该对象放入F-Queue队列中,有线程专门遍历并执行该类的finalize方法,执行后再判断该类是否可被回收,如是则将该对象回收
3.可以在finalize方法中编写类回收前所需的动作,但是如果编写不当,会造成对象无法回收,从而引发内存泄漏问题,所以如果没有特殊理由,不建议重写finalize方法

代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MemoryOptimizationTest {

private static MemoryOptimizationTest memoryOptimizationTest;

@Override
protected void finalize() throws Throwable {
System.out.println("进入finalize方法中");
/* 加强引用 */
memoryOptimizationTest = this;
}

public static void main(String[] args) {

memoryOptimizationTest = new MemoryOptimizationTest();

/* 去除强引用 */
memoryOptimizationTest = null;

/* 垃圾回收 */
System.gc();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (memoryOptimizationTest == null) System.out.println("失去");
else System.out.println("活着");

}
}

打印展示

finalize方法

String

1
2
3
4
5
1.String指向的内存空间中的值是不可变的,当String变量存储的值被改变后,并不会更改内存中存储的值,而是指向新的内存空间,由此就会产生内存碎片
2.频繁的对String进行修改,会造成大量的内存碎片,增加JVM的负担;建议使用StringBuffer和StringBuilder进行操作
String: 字符串常量
StringBuffer: 线程安全的字符串变量
StringBuilder: 非线程安全的字符串变量

观察String的不可变特性

代码展示

hashcode方法返回的是对象的哈希码值,通过哈希码值可以快速的查找到对象所在的内存地址,而String的值是不可变的,所以当String被重新赋值时,哈希码值也应该被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StringTest {
public static void main(String[] args) {

String a = "123";

System.out.println("存储123的变量a的hash值: " + a.hashCode());

a = "456";

System.out.println("修改变量a的值为456后的hash值: " + a.hashCode());

a = "456";

System.out.println("在变量a的值为456的情况下再次将变量啊的值修改成456后的hash值: " + a.hashCode());

StringBuilder sb = new StringBuilder();

sb.append("123456789");

System.out.println("第一次拼接字符串时sb的hash值: " + sb.hashCode());

sb.append("asd");

System.out.println("第二次拼接字符串时sb的hash值: " + sb.hashCode());

}
}

图片展示

hashcode

通过String观察常量和变量

1
2
3
4
5
1.equals比较值, ==比较地址值
2.new创建的是变量
3.常量可以共享内存,变量不会
4.常量连接是常量,有变量参与是变量
5.被final修饰的变量会成为常量

代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class StringTest {
public static void main(String[] args) {

final String s = "a";

String a = "abc",
b = "abc",
c = new String("abc"),
d = "a" + "bc",
e = "a",
f = e + "bc",
g = s + "bc";

System.out.println("----- 常量判断 -----");

System.out.println("a.equals(b) : " + a.equals(b));

System.out.println("a == b : " + (a == b));

System.out.println("----- new对象判断 -----");

System.out.println("a.equals(c) : " + a.equals(c));

System.out.println("a == c : " + (a == c));

System.out.println("----- 拼接常量判断 -----");

System.out.println("a.equals(d) : " + a.equals(d));

System.out.println("a == d : " + (a == d));

System.out.println("----- 常量变量拼接判断 -----");

System.out.println("a.equals(f) : " + a.equals(f));

System.out.println("a == f : " + (a == f));

System.out.println("----- 拼接final修饰判断 -----");

System.out.println("a.equals(g) : " + a.equals(g));

System.out.println("a == g : " + (a == g));

}
}

打印展示

常量变量判断

常量与变量的存储位置

1
2
3
4
1.new创建的变量存储在堆中
2.常量存储在常量池中,相同的值可以共享内存
3.被final修饰的变量是常量,存储在常量池中
4.JVM内存优化,是针对堆区

虚拟机结构

虚拟机结构

1
2
3
4
1.类装载子系统将class文件装载,被执行引擎运行,在运行时会用到运行时数据区中的数据
2.JVM栈是线程私有的
3.堆区中存放new创建的对象,JDK1.8中,常量池和静态变量存放在堆区中
4.元数据区在本地内存中存放

垃圾回收时的分代管理

年轻代和年老代

1
2
3
JDK1.8中,元数据区替代持久代
2.类的信息和编译后的代码数据等放在元数据区
3.JVM内存调优是针对堆区,再进一步是针对年轻代和年老代

内存回收流程

1
new对象时先在Eden区申请内存,如果申请不到,对Eden区进行Minor GC,如果再申请不到,会将Eden区中存活的内存移到幸存区,并对Eden区再次进行Minor GC,如果Eden区分配空间再不满足,就把幸存区中的内容移动到年老区,如果年老区内容也满了就会进行Full GC,再分配不到,报OOM异常

Minor GC和Full GC

1
2
3
4
1.Minor GC: 轻量级GC
2.Full GC: 重量级GC
2.System.gc方法会触发Full GC,System.gc方法是告诉JVM可以进行回收,一般会在之后很短时间内执行
3.能通过java命令分配堆空间的运行策略,比如能设置年轻代和年老代的比例,如果JVM监控到上次GC后,运行策略发生变化,会触发Full GC

判断对象可以被回收的标准

1
2
1.对象上没有强引用就可以被回收,new创建的叫强引用,若对象是软引用或弱引用就可能会被回收
2.JDK早期版本用的是引用计数法,当一个对象加一个强引用时,引用计数加1,去掉一个强引用时,引用计数减1,对象上没有引用计数时,就能够被回收;但是引用计数法会导致循环依赖的对象无法被回收,后来就将使用的引用计数法改为了根可达算法

可作为根可达算法的对象

1
2
3
1.JVM栈中引用的对象
2.本地方法栈中引用的对象
3.静态变量和常量所引用的对象

强引用、弱引用、软引用

定义强引用、软引用和弱引用的方式

1
2
3
1.new出来的是强引用
2.通过SoftReference对象可以定义软引用
3.通过WeakReference对象可以定义弱引用,WeakHashMap就是基于弱引用的Map集合

软引用和弱引用的回收时机

1
2
3
1.对象上有强引用,则不会被回收
2.如果一个对象只有软引用,且当前虚拟机堆内存空间足够,不会回收,反之会回收
3.JVM一旦发现某块内存上只有弱引用,不管当前内存空间是否足够,都会回收

代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

public class MemoryOptimizationTest {

public static void main(String[] args) {

/* 强引用 */
String s = new String("123");

/* 软引用 */
SoftReference<String> stringSoftReference = new SoftReference<>(s);

s = null /* 去掉强引用 */;

System.gc() /* 垃圾回收器进行回收 */;

System.out.println(stringSoftReference.get());

s = new String("123");

/* 弱引用 */
WeakReference<String> weakReference = new WeakReference<>(s);

s = null /* 去掉强引用 */;

System.gc() /* 垃圾回收器进行回收 */;

System.out.println(weakReference.get());

}
}

WeakHashMap

打印展示

软引用和弱引用的回收时机

代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.Map;
import java.util.HashMap;
import java.util.WeakHashMap;


public class MemoryOptimizationTest {

public static void main(String[] args) {

String a = new String("a"),
b = new String("b");

Map<String, String> map = new HashMap<>(),
weakHashMap = new WeakHashMap<>();


map.put(a, "aaa") /* a上有String和HashMap的强引用 */;
map.put(b, "bbb") /* b上有String和HashMap的强引用 */;

weakHashMap.put(a, "aaa") /* 再给a加上弱引用 */;
weakHashMap.put(b, "bbb") /* 再给b加上弱引用 */;

map.remove(a) /* 撤销a上HashMap的强引用 */;

a = null /* 撤销a上String的强引用,a上只剩weakHashMap的弱引用 */;
b = null /* 撤销b上String的强引用,b上还剩HashMap的强引用和weakHashMap的弱引用 */;

/* a上没有强引用,只有弱引用,会被自动回收 */
System.gc() /* 垃圾回收*/;

for (Map.Entry<String, String> next : map.entrySet()) System.out.println("HashMap \t" + next.getKey() + " : " + next.getValue());

for (Map.Entry<String, String> next : weakHashMap.entrySet()) System.out.println("WeakHashMap " + next.getKey() + " : " + next.getValue());

}
}

打印展示

强弱引用

内部匿名类形参

JDK1.7及之前版本

1
2
3
1.内部匿名类,形参要加final,否则会报语法错误
2.内部类和外部类是平行的,没有隶属关系
3.外部类有可能会先于内部类回收,如果不加final,参数会被回收,从而导致内部类无法使用;如果加final,会以常量的方式存储,而不是存在堆中,所以外部类回收后,改参数照样会存在

JDK1.8版本

隐式内部类

1
不需要显式地加final,但依然会被当常量管理,无法将其指向新的内存空间

优化措施

代码中的优化措施

1
2
3
4
5
6
1.没有特殊理由,不要重写finalize方法
2.不要频繁的操作String对象,尤其是在循环与多线程中;如需频繁修改,可以使用StringBuilder和StringBuffer
3.在finally从句中释放IO、数据库或网络连接等的物理对象以确保对象能在各种场景中正确地关闭
4.集合对象要及时clean
5.对象用完后可以在finally从句中设置成null
6.在适当的场景中合理的使用软引用和弱引用

命令的优化措施

命令调优

1
合理调整JVM参数,但是调整参数需要慎重,且优先考虑代码级别的优化措施
文章作者: 123
文章链接: https://gao5805123.github.io/123/2021/06/29/Java%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 123
打赏
  • 微信
    微信
  • 支付宝
    支付宝