1 前言
虽然在《死磕JVM》的博文中已经写了关于内存回收的内容,但是最近看了在复习基础时,看到了关于引用的一些内容,对之前的学习是很大的补充。我们都知道根据可达性垃圾回收算法,对于那些引用不可达的对象,会进行回收。这么说是没错,但是本文不对这部分内容过多赘述,主要是补充一下关于引用的内容和避免内存泄漏的方法。
2 引用
2.1 对象的状态
当一个对象在堆内存中运行时,根据可达性算法,可以将对象状态分成如下3种:
-
可达状态:当一个对象被创建后,有一个以上的引用变量引用它,那它就处于可达状态,程序可通过引用变量来调用该对象的属性和方法。
-
可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时是不可达的。在这个状态下,系统在回收该对象之前,会调用可恢复状态的对象的finalize方法进行资源清理,如果系统在调用finalize方法重新让引用变量引用该对象,则这个对象会再次变为可达状态。
-
不可达状态:对于不可达对象,系统调用finalize方法依然没有使该对象变成可达状态,那系统才会真正回收该对象所占有的资源。
2.1 引用
强引用
程序创建一个对象,并把这个对象赋给一个引用变量,则这个引用变量就是强引用。如:
A a = new A();
强引用在处于可达状态时,是绝对不会被垃圾回收的,因此创建太多的强引用对象,也是java造成内存泄漏的主要原因之一。
软引用
软引用通常通过SoftReference类来实现,当一个对象只有软引用时,就算是可达状态,也是有可能被回收的。其回收时机是在内存空间不足是时,会触发回收,充足时和强引用相同。
如:
public class SoftReferenceTest {
public static void main(String[] args) {
SoftReference<Person>[] people = new SoftReference[100];
for(int i = 0; i < people.length; i++){
people[i] = new SoftReference<Person>(new Person("名字" + i,(i + 1) * 4%100));
}
System.out.println(people[2].get());
System.out.println(people[4].get());
System.gc();
System.runFinalization();
System.out.println(people[2].get());
System.out.println(people[4].get());
}
}
class Person{
String name;
int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
在内存充足时,创建100个SoftReferencr对象,即使进行垃圾回收,也不会回收这些对象,和强引用完全一样。
但是将执行参数修改为:
java -Xmx2m -Xms2m SoftReference
运行结果就都为null了,如果这里用强引用,则会报OOM。可见在适合的时候使用软引用可以在一定程度减少OOM。
弱引用
弱引用通过WeakReference实现,与软引用类似,只是弱引用级别更低,在垃圾回收时,不管内存空间是否足够都会被回收。
如:
public class WeakReferenceTest {
public static void main(String[] args) {
String str = new String("JAVA");
WeakReference<String> wr = new WeakReference<String>(str);
str = null;
System.out.println(wr.get());
System.gc();
System.runFinalization();
System.out.println(wr.get());
}
}
在执行垃圾回收之后,输出为null,说明已经被回收了。
虚引用
软引用和弱引用可以单独使用,但是虚引用不能单独使用,因为单独使用没有太大意义。虚引用的主要作用是根据对象的垃圾回收状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用对象是否被回收。
虚引用通过 PhantomReference类实现。如果一个对象只有一个虚引用,那它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列( Reference Queue)联合使用。
如:引用和引用队列结合使用,可以看到,被虚引用所引用对象被垃圾回收后,虚引用将被添加到引用队列中。
public static void main(String[] args) {
String str = new String("JAVA");
ReferenceQueue<String> rq = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(str, rq);
str = null;
System.out.println(pr.get()); // null
// so domething to pr
System.gc();
System.runFinalization();
System.out.println(rq.poll() == pr); // true
}
在垃圾回收时,只有虚引用引用的字符串对象会被回收,回收后,将该引用添加到关联引用队列。
在被回收之前可以左一些操作,但是做什么操作,才疏学浅,俺也不知道,因为没用,反正知道有这玩意儿就行了。
3 内存泄漏
程序运行时,那些不再使用的空间无法被回收,就成为内存泄漏,过多的内存泄漏会造成内存溢出。
什么情况下比较容易出现内存泄漏呢,比如:
public void remove(int index){
String[] oldValue =new String[index];
int size = oldValue.length;
int moved = size - index - 1;
if(moved > 0){
System.arraycopy(oldValue, index + 1, oldValue, index ,moved);
}
oldValue[size-1] = null; // 1
}
数组将size-1即可删除最后一位数据。但是如果没有1 这一步,数组原来最后一位的引用还在,则如下图所示:
数组的最后会造成内存泄漏,该元素虽然一直可达,但是size已经减小一位,这一位永远都不会用到。因此需要设置为null,解除引用。
4 内存管理技巧
尽量使用直接量
如:我们在创建一个字符串时:
String aa = "aa";
String bb = new String("bb");
采取第一种方式,JVM会在常量池中创建“aa”字符串,但是在如果用第二种方式,除了在常量池中创建“aa”字符串外,还会在内存堆中创建一个String 实例对象,并且这个String对象的底层包含了一个char[]数组,多占用了内存和引用。
使用StringBuilder和StringBuffer
StringBuilder和StringBuffer都可代表字符串,且都是字符序列可变的字符串,但是String是不可变的,一旦对其用“+”拼接,都会创建一个新的String对象,或者在常量池中创建新的字符串,造成一种可变的假象,如果"+"拼接过多,会运行时会生成大量临时字符串,影响性能。
尽早释放无用对象的引用
以下场景:
public void info(){
Object obj = new Object();
System.out.println(obj.hashCode());
System.out.println(obj.toString());
obj = null;
// 耗时 耗内存
}
当 obj = null;是方法最后一步,则没有必要显示的设置为null,但是如果不是最后一步,且后续步骤非常消耗资源,则最好显示的设置为null。
尽量少用静态变量
静态变量的生命周期和类同步,类的垃圾回收比较,条件比较苛刻,因此很少会被垃圾回收释放内存。过多的使用静态变量会占用更多的内存。
避免在循环中创建java对象
如果在一个for循环中,每一次循环都new一个对象,这显然是很不合理的。
缓存经常使用的对象
经常使用的对象,通过一个对象,缓存在内存中,比如连接池,hashmap,或者缓存工具,如redis等。
考虑使用softReference
根据之前的介绍,当内存充足是和强引用没什么区别,当内存不足时,可以释放内存,但是前提是在合适的场景下使用,在使用之前,要先判断是否为null。