减少GC开销 &&可能出现内存泄漏的情况&&两个对象相互引用会不会被GC

如何降低java GC开销,减少GC次数

其他详见:《如何减少垃圾回收的次数 jvm虚拟机》

1.选择一个较好的GC器

Java9在2017年九月发布,G1(“Garbage First”)垃圾回收器 成为 HotSpot 虚拟机默认的垃圾回收器。从 serial 垃圾回收器到CMS 收集器, JVM 见证了许多 GC 实现,而 G1 将成为其下一代垃圾回收器。

随着垃圾收集器的发展,每一代 GC 与其上一代相比,都带来了巨大的进步和改善。parallel GC 与 serial GC 相比,它让垃圾收集器以多线程的方式工作,充分利用了多核计算机的计算能力。CMS(“Concurrent Mark-Sweep”)收集器与 parallel GC 相比,它将回收过程分成了多个阶段,使得应用线程正在运行的时候,收集工作可以并发地完成,大大改善了频繁执行 “stop-the-world” 的情况。G1 对于拥有大量堆内存的 JVM 表现出更好的性能,并且具有更好的可预测和统一的暂停过程

2.预测集合的容量,初始化集合时候提供大小

因为java中的集合,底层都使用的是数组(原生数据类型或者基于对象的类型),数组一旦被分配,其大小就不可变,因此添加元素到集合时,大多数情况下都会导致需要重新申请一个新的大容量的数组替换老的数组。
如果没有提供集合的初始化大小,那么大多数集合的实现都尽量优化重新分配数组的处理并且将其开销平摊到最低。不过,在构造集合的时候就提供大小可以得到最佳的效果。

3.直接处理数据流

当处理数据流时,比如从一个文件读取数据或者从网络中下载数据,下面的代码是非常常见的:

byte[] fileData = readFileToByteArray(new File("myfile.txt"));

所产生的字节数组可能被解析 XML 文档、JSON 对象或者协议缓冲消息,以及一些常见的可选项。

当处理大文件或者文件的大小无法预测时,上面的做法很是不明智的,因为当 JVM 无法分配一个缓冲区来处理真正文件时,就会导致OutOfMemeoryErrors。

即使数据的大小是可管理的,当到垃圾回收时,使用上面的模式依然会造成巨大的开销,因为它在堆中分配了一块非常大的区域来存储文件数据。

一种更加好的处理方式是使用合适的 InputStream (比如在这个例子中使用 FileInputStream)直接传递给解析器,不再一次性将整个文件读取到一个字节数组中。所有主流的开源库都提供相应的 API 来直接接受一个输入流进行处理,比如:

FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

4.使用不可变的对象

不变性有太多的好处。甚至不用我赘述什么。然而,有一个优点会对垃圾回收产生影响,应该关注一下。

一个不可变对象的属性在对象被创建后就不能被修改(在这里的例子使用的是引用数据类型的属性),比如:

public class ObjectPair {
private final Object first;
private final Object second;

public ObjectPair(Object first, Object second) {
    this.first = first;
    this.second = second;
}

public Object getFirst() {
    return first;
}

public Object getSecond() {
    return second;
}

}

将上面的类实例化后会产生一个不可变对象—它的所有属性用 final 修饰,构造完成后就不能改变了。

不可变性意味着所有被一个不可变容器所引用的对象,在容器构造完成前对象就已经被创建。就 GC 而言:这个容器年轻程度至少和其所持有的最年轻的引用一样。这意味着当在年轻代执行垃圾回收的过程中,GC 因为不可变对象处于老年代而跳过它们,直到确定这些不可变对象在老年代中不被任何对象所引用时,才完成对它们的回收。

更少的扫描对象意味着对内存页更少的扫描,越少的扫描内存页就意味着更短的 GC 生命周期,也意味着更短的 GC 暂停和更好的总吞吐量。

5. 小心字符串拼接(用StringBuffer或者StringBuilder代替String)

字符串可能是在所有基于 JVM 应用程序中最常用的非原生数据结构。然而,由于其隐式地开销负担和简便的使用,非常容易成为占用大量内存的罪归祸首。

这个问题很明显不在于字符串字面值,而是在运行时分配内存初始化产生的。让我们快速看一下动态构建字符串的例子:

public static String toString(T[] array) {
 
    String result = "[";
 
    for (int i = 0; i & lt; array.length; i++) {
        result += (array[i] == array ? "this" : array[i]);
        if (i & lt; array.length - 1) {
            result += ", ";
        }
    }
 
    result += "]";
 
    return result;
}

这是个看似不错的方法,接收一个字符数组然后返回一个字符串。但是这对于对象内存分配却是灾难性的。

很难看清这语法糖的背后,但是幕后的实际情况是这样的:

public static String toString(T[] array) {
 
    String result = "[";
 
    for (int i = 0; i & lt; array.length; i++) {
 
        StringBuilder sb1 = new StringBuilder(result);
        sb1.append(array[i] == array ? "this" : array[i]);
        result = sb1.toString();
 
        if (i & lt; array.length - 1) {
            StringBuilder sb2 = new StringBuilder(result);
            sb2.append(", ");
            result = sb2.toString();
        }
    }
 
    StringBuilder sb3 = new StringBuilder(result);
    sb3.append("]");
    result = sb3.toString();
 
    return result;
}

字符串是不可变的,这意味着每发生一次拼接时,它们本身不会被修改,而是依次分配新的字符串。此外,编译器使用了标准的 StringBuilder 类来执行这些拼接操作。这就会有问题了,因为每一次迭代,既隐式地分配了一个临时字符串,又隐式分配了一个临时的 StringBuilder 对象来帮助构建最终的结果。

最佳的方式是避免上面的情况,使用 StringBuilder 和直接的追加,以取代本地拼接操作符(“+”)。下面是一个例子:

public static String toString(T[] array) {
 
    StringBuilder sb = new StringBuilder("[");
 
    for (int i = 0; i & lt; array.length; i++) {
        sb.append(array[i] == array ? "this" : array[i]);
        if (i & lt; array.length - 1) {
            sb.append(", ");
        }
    }
 
    sb.append("]");
    return sb.toString();
}

这里,我们只在方法开始的时候分配了唯一的一个 StringBuilder。至此,所有的字符串和 list 中的元素都被追加到单独的一个StringBuilder中。最终使用 toString() 方法一次性将其转成成字符串返回。

6.尽量使用特定的原生类型的集合而非包装类集合

Java 标准的集合库简单且支持泛型,允许在使用集合时对类型进行半静态地绑定。比如想要创建一个只存放字符串的 Set 或者存储 Map<Pair, List>这样的 map,这种处理方式是非常棒的。

真正的问题源于当我们想要使用一个 list 存储 int 类型,或者一个 map 存储 double 类型作为 value。因为泛型不支持原生数据类型,因此另外的一种选择是使用包装类型来进行替换,这里我们使用 List 。

这种处理方式是非常浪费的,因为一个 Integer 是一个完全的对象,一个对象的头部占用12个字节以及其内部的所维护的 int 属性,每个Integer 对象总共占用16个字节。这比起存储相同个数的 int 类型的 list 而言,其消耗的空间是它的四倍!比这个更加严重的问题在于,事实上因为 Integer 是真正的对象实例,因此它需要垃圾收集阶段被垃圾收集器所考虑是否要回收

为了处理这个问题,我们在 Takipi 中使用非常棒的 Trove 集合库。Trove 摒弃了部分泛型的特定来支持特定的使用内存更高效的原生类型的集合。比如,我们使用非常消耗性能的 Map<Integer, Double>,在 Trove 中有另一种特别的选择方案,其形式为 TIntDoubleMap

TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...

Trove 的底层实现使用了原生类型的数组,所以**当操作集合的时候不会发生元素的装箱(int->Integer)或者拆箱(Integer->int), 没有存储对象,**因为底层使用原生数据类型存储。

两个对象相互引用会不会被回收

会。
用引用计数法的话是解决不了这个问题的,因为这两个对象一直就是互相引用,引用个数一直不会是0,所以不会被回收。

但是现在主要用的是可达性分析法。
可达性分析算法的基本思想:通过一系列的被称为“gc roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到“gc roots”没有任何引用链相连时,则证明此对象是不可用的。

因为JVM按照对象在以GC root为根节点的图中的可达性来决定对象是否被GC。相互引用的两个对象,引用数虽然不为0,但如果跟外界其他对象都没有引用关系,即是一个孤岛,仍然会被GC。就像下图中的object5、6 7一样。他们互相引用,但是他们到GC roots是不可达的,所以判定为是可回收的对象。
在这里插入图片描述

java中可以被GC(可以作为GC Root)的对象有:

  1. 虚拟机栈中引用的对象(本地变量表);
  2. 方法区中静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中引用的对象(本地对象)

内存泄漏问题出现的情况

  1. 静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
    Static Vector v = new Vector();
    for (int i = 1; i<100; i++)
    {
    Object o = new Object();
    v.add(o);
    o = null;
    }
    在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

  2. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

  3. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

复习用的GC总结:http://www.importnew.com/16173.html

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/89630777
GC