Java内存管理的几个小技巧
1.尽量使用直接量
当需要使用字符串,还有Byte、Short、Integer、Long、Float、Double、Boolean、Character包装类的实例时,程序不应该采用new的方式来创建对象,而应该直接采用直接量来创建他们。
例如,程序需要“hello”字符串,应该采用如下代码:
String str = "hello";
上面方式会创建一个“hello”字符串,而且JVM的字符串缓存池还会缓存这个字符串,但是程序如果使用如下代码:
String str = new String("hello");
此时程序同样会创建一个缓存在字符串缓存池中的“hello”字符串。初次之外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组里依次存放了h、e、l、l、o等字符。
2.使用StringBuilder和StringBuffer进行字符串连接
String、StringBuilder、StringBuffer都可代表字符串,其中String代表字符序列不可变的字符串,而StringBuilder和StringBuffer都可代表字符序列可变的字符串。
如果程序使用多个String对象进行字符串连接运算,在运行时将生成大量的临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
3.尽早释放无用对象的引用
大部分时候,方法的局部引用变量所引用的对象会随着方法的结束而变成垃圾,因为局部变量的生存期限很短,当方法运行结束时,该方法的局部变量就结束了生存期限。因此,大部分时候程序无需将局部引用显式设为null.
例如,下面的info()方法
public void info(){
Object obj = new Object();
System.out.println(obj.toString());
System.out.println(obj.hashCode());
obj = null;//此处将其显式设为null
}
上面程序中的info()方法里定义了一个obj变量,随着info()方法执行完成,程序中的obj引用变量的作用域就结束了,原来的obj所引用的对象就会变成垃圾。因此,上面程序中粗体字代码是没有必要的。
但换一种情况来看,如果上面程序中的info()方法改为如下形式。
public void info(){
Object obj = new Object();
System.out.println(obj.toString());
System.out.println(obj.hashCode());
obj = null;
//执行耗时、耗内存操作
//或者调用耗时、耗内存的方法
...
}
对于上面的所示的info()方法,如果在粗体字之后还需要执行耗时、耗内存操作,或者还需要调用耗时、耗内存的方法,那么程序中的粗体字代码就是有必要的:可以尽早释放对Object对象的引用。可能的情况是,程序在执行粗体字代码之后的耗时、耗内存操作时,obj之前所引用的Object对象可能被垃圾回收了。
4.尽量少用静态变量
从理论上来说,Java对象何时被回收由垃圾回收机制决定,对程序员来说是不确定的。由于垃圾回收机制判断一个对象是否是垃圾的唯一标准是该对象是否有引用变量引用它,因此最好尽早释放对象的引用。
最坏的情况是,某个对象被static变量所引用,那么垃圾回收机制通常是不会回收这个对象所占用的内存的。实例如下。
class Person{
static Object obj = new Object();
}
对于上面的Object对象而言,只要obj变量还引用到它,它就不会被垃圾回收机制所回收。
obj变量是Person类的静态变量,因此它的生命周期和Person类同步。在Person类不被卸载的情况下,Person类对应的Class对象会常驻内存,直到程序运行结束。因此,obj所引用的Object对象一旦被创建,也会常驻内存,直到程序运行结束。
注:根据分代回收机制,JVM会将程序中Person类的信息存入Permanent代。也就是说,Person类、obj引用变量都将存在Permanent代里,这将导致obj对象一直有效,从而使obj所引用的Object得不到回收。
5.避免在经常调用的方法、循环中创建多个Java对象
经常调用的方法和循环有一个共同特征:这些代码会被多次重复调用。实例如下。
public class Test{
public static void main(String[] args){
for(int i = 0; i < 10; i++){
Object obj = new Object();
//执行其他操作...
}
}
}
上面代码在循环中创建了10个Object对象,虽然上面程序中的obj变量都是代码块中的局部变量,当循环执行结束时这些局部变量都会失效,但由于这段循环导致Object对象会被创建10次,因此系统需要不断地为这10个对象分配内存空间,执行初始化操作。这10个对象的生存时间并不长,接下里系统又需要回收它们所占用的内存空间。在这种不断的分配、回收垃圾操作的工作中,程序的性能会受到巨大的影响。
6.缓存经常使用的对象
如果有些对象需要被经常使用,则可以考虑将这些对象用缓存池保存起来,这样当下次需要时就可直接拿出这些对象来用。典型的缓存就是数据连接池,数据连接池里缓存了大量的数据库连接,每次程序需要访问数据库时都可直接取出数据库连接。
除此之外,如果系统中还有一些常用的基础信息,比如信息化信息里包含的员工信息、物料信息等,也考虑对它们进行缓存。实现缓存时通常两种方式。
-
使用HashMap进行缓存。
- 直接使用某些开源的缓存项目。
如果直接使用HashMap进行缓存,程序员需要手动控制HashMap容器里的key-value对不至于太多,因为当key-value对太多时将导致HashMap占用过大的内存,从而导致系统性能降低。
注:缓存设计本省就是一种以牺牲系统空间来换取运行时间的技术,不管是哪种缓存实现,都会使用容器来保存已用过的对象,方便下次再用。而这个保存对象的容器将占用一块不算太小的内存,如何控制该容器占用的内存不至于过大,而该容器又能保留大部分已用过的对象,这才是缓存设计的关键。
7.尽量不要使用finalize方法
在一个对象失去引用之后,垃圾回收器准备回收该对象之前,垃圾回收机制会先调用该对象的finalize()方法来进行资源管理。
实际上,将资源清理放在finalize()方法中完成时非常不好的选择。由于垃圾回收机制的工作量已经比较大了,尤其是回收Young代内存时,大都会引起应用程序暂停,使得用户难以忍受。
在垃圾回收器本身已经严重制约应用程序性能的情况下,如果再选择使用finalize()方法进行资源清理,无疑是火上浇油的行为,这将导致垃圾回收器的负担更大,导致程序运行效率更差。
8.考虑使用SoftReference
当程序需要创建长度很大的数组时,可以考虑使用SoftReference来包装数组元素,而不是直接让数组元素来引用对象。
SoftReference是一个很好的选择:当内存足够时,它的功能等同于普通引用;当内存不够时,它会牺牲自己,释放软引用所引用的对象。实例如下。
public class SoftReferenceTest {
public static void main(String[] args){
SoftReference<Person>[] person = new SoftReference[100000];
for(int i = 0; i < person.length;i++){
person[i] = new SoftReference<Person>(new Person("名字" + i,(i + 1) * 4 % 100));
}
System.out.println(person[2].get());
System.out.println(person[4].get());
//通知系统进行垃圾回收
System.gc();
System.runFinalization();
//再次输出垃圾回收之后的SoftReference数组里的元素不变
System.out.println(person[2].get());
System.out.println(person[4].get());
}
}
class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name: " + name + "," + "age: " + age ;
}
}上面的程序创建了一个长度为100000的Person数组,如果直接使用强引用数组,这个Person数组将会导致程序内存溢出;如果程序改为创建长度为100000的软引用数组,程序将可以正常运行。当系统内存紧张时,系统会自动释放软引用所引用的对象,这样就能保证程序继续运行。
使用软引用引用对象时不要忘记了软引用的不确定性。程序通过软引用所获得的对象有可能为null。当系统内存紧张时,SoftReference所引用的Java对象将被释放。由于通过SoftReference获取的对象可能为null,因此应用程序取出SoftReference所引用的Java对象之后,应该是显式判断该对象是否为null;当该对象为null时,应重建该对象。