Java知识点小结3:内存回收

文章目录

注:本文是对《疯狂Java面试讲义》的小结。

对象引用

Java通过 new 关键字来创建对象实例,JVM会在堆内存中为对象分配空间。当对象失去引用时,JVM的垃圾回收机制会自动清理对象,回收内存空间。

可以把对象的引用关系理解为有向图。如果某个对象在图中处于不可达状态,则认为该对象不再被引用。

Java的对象引用方式有:

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

下面举例说明各种引用方式。

准备:已知类 Person 定义如下:

java 复制代码
public class Person {
    private String name;
    private int age;
    ......
}

强引用

强引用是最普通、最常见的引用方式。

java 复制代码
        Person person = new Person("Tom", 20);

强引用的对象,一定不会被JVM回收。

对于强引用,当内存占用过多时,就会出现 OutOfMemoryError

java 复制代码
        Person[] arr1 = new Person[80000];

        for (int i = 0; i < arr1.length; i++) {
            arr1[i] = new Person("Tom" + i, i % 20);
            System.out.println(arr1[i]);
        }

正常情况下,内存够用,运行结果OK。为了模拟内存被占满的情况,我们把JVM的内存设置为较低值:

powershell 复制代码
-Xmx8m -Xms8m

如果是使用命令行:

powershell 复制代码
java -Xmx8m -Xms8m Xxxxxx

如果是使用IntelliJ IDEA,右键,More Run/Debug -> Modify Run Configuration...:

在弹出的对话框里,点击"Modify options",在子菜单中确保勾选了"Add VM options",然后填入 -Xmx8m -Xms8m

运行结果如下:

powershell 复制代码
......
Person{name='Tom61694', age=14}
Person{name='Tom61695', age=15}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1375)
	at java.base/java.lang.StringConcatHelper.newArray(StringConcatHelper.java:494)
......

可见,在创建到大约60000个对象的时候,内存就会溢出。

软引用(SoftReference)

软引用的作用是,当内存空间充足时,它不会被系统回收,但是当内存空间不足时,它就会被系统回收。

软引用的用法如下:

java 复制代码
        SoftReference<Person> person = new SoftReference<>(new Person("Tom", 20));
        ......
        person.get(); // 获取所引用的对象,注意可能是null值

把上面强引用的代码稍作修改,如下:

java 复制代码
        SoftReference<Person>[] arr1 = new SoftReference[80000];

        for (int i = 0; i < arr1.length; i++) {
            arr1[i] = new SoftReference<>(new Person("Tom" + i, i % 20));
            System.out.println(arr1[i].get());
        }

        System.out.println(arr1[1].get());

        System.out.println(arr1[3].get());

        System.out.println(arr1[79999].get());

运行结果如下(别忘了设置JVM内存):

powershell 复制代码
......
Person{name='Tom79998', age=18}
Person{name='Tom79999', age=19}
null
null
Person{name='Tom79999', age=19}

可见,80000个对象依次创建成功,内存没有溢出,但是创建完毕后,再访问这些对象时,发现前面一部分对象已经变成null了。这是由于在创建后面的对象时,内存不够,触发了垃圾回收,前面的软引用对象被清除了。

多次运行代码,最后一行,有时也会打印出 null ,这说明每次垃圾回收的时机和回收的对象数量是不一定的。

弱引用(WeakReference)

弱引用和软引用类似,区别在于,不管内存是否充足,只要有垃圾回收,弱引用对象就会被回收。

弱引用的用法如下:

java 复制代码
        WeakReference<Person> person = new WeakReference<>(new Person("Tom", 20));

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

        System.gc();
        System.runFinalization();

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

运行结果如下(无需设置JVM内存):

powershell 复制代码
Person{name='Tom', age=20}
null

可见,垃圾回收时,弱引用对象被回收了。

WeakReference 功能类似的还有 WeakHashMap

对于map里的key,如果其对象没有其它强引用,则在垃圾回收时,会把该对象回收,并把该key值从map里移除。

java 复制代码
        WeakHashMap<Person, String> map = new WeakHashMap<>();
        map.put(new Person("Tom", 20), "aaa");
        map.put(new Person("Jerry", 30), "bbb");

        System.out.println(map);

        System.gc();
        System.runFinalization();

        System.out.println(map);

运行结果如下:

powershell 复制代码
{Person{name='Jerry', age=30}=bbb, Person{name='Tom', age=20}=aaa}
{}

考一考

下面的代码,运行结果是什么?

java 复制代码
        WeakHashMap<Person, String> map = new WeakHashMap<>();
        Person person1 = new Person("Tom", 20);
        map.put(person1, "aaa");
        // person1 = null;
        Person person2 = new Person("Jerry", 30);
        map.put(person2, "bbb");
        // person2 = null;

        System.out.println(map);

        System.gc();
        System.runFinalization();

        System.out.println(map);

乍一看,似乎跟前一个例子没什么太大区别,但运行结果是不同的:

powershell 复制代码
{Person{name='Jerry', age=30}=bbb, Person{name='Tom', age=20}=aaa}
{Person{name='Jerry', age=30}=bbb, Person{name='Tom', age=20}=aaa}

这两个key对象为什么没有被回收掉呢?其实原因很简单,因为person1和person2还在引用它们。这是强引用,所以不会被垃圾回收。要想被垃圾回收,只需解除强引用(参见被注释掉的那两行代码)。

虚引用(PhantomReference)

虚引用的主要作用是跟踪对象被垃圾回收的状态。

虚引用不能单独使用,必须和引用队列(ReferenceQueue)一起使用。引用队列保存了被回收后对象的引用。它和软引用、弱引用等联合使用,这些对象被回收时,会把引用添加到相关联的引用队列中。

java 复制代码
        ReferenceQueue<Person> queue = new ReferenceQueue<>();

        PhantomReference<Person> phantomReference = new PhantomReference<>(new Person("Tom", 20), queue);

        // 虚引用的 get() 方法总是返回null
        System.out.println(phantomReference.get());

        // 还没做垃圾回收,所以队列为空
        System.out.println(queue.poll());

        System.gc();
        System.runFinalization();

        // 垃圾回收后,虚引用对象在队列中
        System.out.println(queue.poll());

运行结果如下:

powershell 复制代码
null
null
java.lang.ref.PhantomReference@36baf30c

既然虚引用都get不到实际对象,那它到底有什么用呢?

看起来,虚引用的用处,就是会触发一个事件。我们可以另起一个线程,对队列进行监听,比如:

  • remove() :阻塞方法
  • poll() :非阻塞方法

这样,当对象被回收时,我们就会得到通知,做相应的处理(比如清理资源)。

总结

强引用 软引用 弱引用
垃圾回收时,不会被回收 Y N N
垃圾回收时,若内存不足,就会被回收 N Y Y
只要有垃圾回收,就会被回收 N N Y

垃圾回收

垃圾回收机制主要做两件事:

  • 跟踪每个Java对象的可达状态,回收不可达对象的内存
  • 清理分配和回收过程中产生的内存碎片

垃圾回收的设计思想:

  • 串行和并行:使用单CPU还是多CPU来做回收
  • 并发和停止(stop-the-world):回收时,应用是否暂停
  • 紧凑和不紧凑:如果只是回收对象,则内存可能会产生很多碎片。把所有活着的对象复制到一起,可以避免内存碎片

回收方式:

  • 复制:把内存分成两个相同的空间A和B,以A空间为例,从根开始,把每个可达对象复制到B空间,最后把整个A空间重置
  • 标记清除(mark-sweep):从根开始,标记每个可达对象,最后回收所有没有标记可达的对象
  • 标记清除紧凑(mark-sweep-compact):从根开始,标记每个可达对象,最后把所有活着的对象搬迁在一起,并回收其它内存空间

分代内存:

  • 新生代(Young):大部分对象存活时间不会很长,处于新生代
  • 老年代(Old):少量对象存活时间很长,处于老年代
  • 永生代(Permanent):主要用于存放Class对象,方法等信息

新生代

新生代又分为Eden(伊甸园)区和Survivor区。

绝大部分对象先分配到Eden区(大对象可能会直接分配到老年代),而Survivor区的对象至少熬过一次垃圾回收。

Survivor区又分为From区和To区,参见上面提到的"复制"回收方式。

垃圾回收时,将Eden区和From区的可达对象复制到To区,然后清空Eden区和From区,最后把From和To互换一下。

老年代

如果一个对象熬过了数次垃圾回收,就可能会被转入老年代。

老年代的垃圾回收频率无需太高,因为老年代的对象都很能熬。

  • 次要回收:新生代的垃圾回收,频率较高
  • 主要回收:新生代和老年代的垃圾回收,频率较低

老年代的垃圾回收通常采用标记清除紧凑算法。

永生代

主要用于存放Class对象,方法等信息,默认为64MB。

内存管理小技巧

尽量使用直接量

java 复制代码
        String str1 = "hello"; // 在字符串缓存池里缓存了hello字符串
        String str2 = new String("hello"); //同上,此外还多创建了一个char[]数组

使用StringBuilder和StringBuffer进行字符串拼接

String字符串是不可变的,如果直接对字符串拼接,将产生大量的临时字符串。

java 复制代码
        String str1 = "hello";
        String str2 = "world";
        String str3 = "!!!";

        String result1 = str1 + str2 + str3; // 产生大量的临时字符串
        System.out.println(result1);

        StringBuilder result2 = new StringBuilder();
        result2.append(str1);
        result2.append(str2);
        result2.append(str3);
        System.out.println(result2.toString());

        StringBuffer result3 = new StringBuffer();
        result3.append(str1);
        result3.append(str2);
        result3.append(str3);
        System.out.println(result3.toString());

注: StringBufferStringBuilder 的区别:

  • StringBuilder :非线程安全,但性能高,适合单线程环境
  • StringBuffer :线程安全,性能稍低,适合多线程环境

尽早释放无用对象的引用

java 复制代码
        Person person = new Person("Tom", 20);
        person.doSomething();
        // person = null;
        ......

本例中,创建并使用完person对象后,最好将其释放(参见注释处代码),否则,person变量在其作用域范围内,会一直持有对象引用,导致对象无法被系统回收。

尽量少用静态变量

java 复制代码
class A {
    static B b = new B();
}

本例中,b是A类的静态变量,其生命周期与A类一致(存入永生代)。

避免在循环中创建对象

java 复制代码
        for (int i = 0; i < 100; i++) {
            Person person = new Person("Tom", 20);
            ......
        }

创建了100个Person对象,它们的生存时间都很短。系统需要不断的分配和回收内存。

缓存经常使用的对象

典型的例子是数据库连接池。

避免使用finalize()方法

垃圾回收的工作量已经很大了,尤其是新生代,对象很多,回收频繁,若再使用finalize()方法清理资源,更加重了垃圾回收的负担。

使用SoftReference

参见前面的介绍。

注意SoftReference和WeakReference的不确定性,在使用对象时,应检查其是否为空。

相关推荐
不见长安见晨雾13 分钟前
将Java程序打包成EXE程序
java·开发语言
逸狼37 分钟前
【JavaEE初阶】多线程(5 单例模式 \ 阻塞队列)
java·开发语言
希忘auto2 小时前
Java之线程篇四
java
Yz98762 小时前
Hadoop里面MapReduce的序列化与Java序列化比较
java·大数据·jvm·hadoop·分布式·mapreduce·big data
凯哥Java2 小时前
优化批处理流程:自定义BatchProcessorUtils的设计与应用
java·数据库·mysql
njnu@liyong2 小时前
AOP-前置原理-怎么判断和拦截?
java·aop·拦截
末央&2 小时前
【C++】内存管理
java·开发语言·c++
心之语歌3 小时前
设计模式 享元模式(Flyweight Pattern)
java·设计模式·享元模式
MTingle3 小时前
【Java EE】文件IO
java·java-ee