前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >疯狂Java笔记之Java的内存与回收

疯狂Java笔记之Java的内存与回收

作者头像
HelloJack
发布2018-08-28 15:02:42
4350
发布2018-08-28 15:02:42
举报
文章被收录于专栏:Jack的Android之旅Jack的Android之旅

Java引用的种类

1.对象在内存中的状态

对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用改对象?只要有引用变量引用对象,垃圾回收机制就不会回收它。

也就是说,当java对象被创建出来之后,垃圾回收机制会实时的监控每个对象的运行状态,包括对象的申请,引用,被引用,赋值等。当垃圾回收机制实时的监控到某个对象不再被引用变量所引用时,垃圾回收机制就会回收它所占用的空间。

基本上,可以把JVM内存中的对象引用理解成一种有向图,把引用变量,对象都当成有向图的顶点,将引用关系当成图的有向边,有向边总是从引用端指向被引用的Java对象。因为Java的所有对象都是由一条条线程创建出来的,因此可以把线程对象当成有向图的起始顶点。

对于单线程程序而言,整个程序只有一条main线程,那么该图就是以main进程为顶点的有向图。在这个有向图中,main顶点可达的对象都处于可达状态,垃圾回收机制不会回收它们;如果某个对象在这个有向图中处于不可达状态,那么就认为这个对象不再被引用。

采用有向图管理内存中的对象具有较高的精度,当缺点是效率较低。 当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以把它所处的状态分成

  • 可达状态:当一个对象被创建后,有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,那么它就处于可达状态,程序可以通过引用变量来调用该对 象的属性和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象。在这种状态下,系统的垃圾回 收机制准备回收该对象所占用的内存。在回收该对象之前,系统会调用可恢复状态的对象的finalize方法进行资源清理,如果系统调用finalize方法重新让一个以L的 引用变量引用该对象,则这个对象会再次变为可达状态:否则,该对象将进入不可达状态。
  • 不可达状态:书对象的所有关联都被切断,fl甲系统调用所有对象的finaii}e方法依然没有使该对象变成可达状态后,这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

memery.PNG

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或者被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象那个被其他对象的实例变量引用时,只有当引用该对象的对象被销毁或变成不可达状态后,改对象才会进入不可达状态。

对于垃圾回收机制来说,判断一个对象是否可回收的标准就在改对象时候被引用,因此引用也是JVM进行内存管理的一个重要概念。为了更好的管理对象的引用,从JDK1.2开始,Java在java.lang.ref包下提供了三个类:SoftReference,PhantomReference和WeakReference,它们分别代表了系统对对象的三种引用方式:软引用,虚引用和弱引用。归纳起来,Java语言对对象的引用有如下四种:

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

2.强引用

当程序创建一个对象,并把这个对象赋给一个引用变量,这个引用变量就是强引用。强引用是最常见的。

当一个对象被一个或一个以上的强引用变量所引用时,它处于可达状态,它不可能被系统垃圾回收机制回收,即使系统内存非常紧张,即使有些Java对象以后永远都不会被用到,JVM也不会回收被强引用所引用的Java对象。

由于JVM肯定不会回收被强引用所引用的Java对象,因此强引用时造成Java内存泄漏的只要原因之一。

3.软引用

对于强引用所引用的Java对象而言,无论系统的内存如何紧张,即使某些Java以后不再使用,垃圾回收机制也不会回收它所占的内存。当时软引用不同,当系统内存充足时,和强引用是没有什么区别的。但是当系统内存不足时,软引用所引用的Java对象可以被垃圾回收机制回收,从而避免系统内存的不足的异常。

当程序需要大量创建某个类的新对象,而且有可能重新访问已创建的来对象是,可以充分使用软引用来解决内存紧张的难题。代码如下:

代码语言:javascript
复制
class Person{
    String name;
    int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public String toString(){
        return "Person[name="+name+",age="+age+"]";
    }
}

public class Main {
    public static void main(String[] args) {
        SoftReference<Person>[] people=new SoftReference[100000];
        for(int i=0;i<people.length;i++){
            people[i]=new SoftReference<Person>(new Person("HelloJack"+i,i));
        }
        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());
    }
}

4.弱引用

软引用与软引用有点相似,区别在于弱引用所引用的对象的生命周期更短。弱引用通过WeakReference类实现。对于软引用的对象而言,当系统垃圾回收机制运行时,不管系统内存时候足够,总会回收改对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。

弱引用具有很大的不确定性,因为每次垃圾回收机制执行时都会回收弱引用所引用的对象,而垃圾回收机制的运行又不受程序员的控制,因此程序获取弱引用所引用的Java对象是必须小心空指针异常,通过弱引用所获取的Java对象可能是null. 代码如下:

代码语言:javascript
复制
String str=new String("HelloJack");
WeakReference<String> wr=new WeakReference<String>(str);
str=null;
System.out.println(wr.get());
System.gc();
System.runFinalization();
System.out.println(wr.get());

5.虚引用

弱引用和软引用可以单独使用,当虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收。虚引用通过PhantomReference类实现,他完全类似于没有引用。虚引用对对象本身没有太大的影响,对象甚至感觉不到虚引用的存在。 代码如下:

代码语言:javascript
复制
String str=new String("HelloJack");
ReferenceQueue<String> rq=new ReferenceQueue<String>();
PhantomReference<String> pr=new PhantomReference<>(str,rq);

str=null;
System.out.println(pr.get());
System.gc();
System.runFinalization();

System.out.println(rq.poll()==pr);

Java的内存泄漏

程序运行过程中会不断地分配内存空间,那些不在使用的内存空间应该即时被回收,从而保证系统可以再次使用这些内存,如果存在无用的内存空间应该即时被回收,从而保证系统可以再次使用这些内存,如果存在无用的内存没有被回收回来,那就内存泄漏。

垃圾回收机制

垃圾回收机制只要完成两件事:

  • 跟踪并监控每个Java对象,当某个对象处于不可达状态,回收该对象所占用的内存
  • 清理内存分配,回收过程中产生的内存碎片

一个高效的JVM一个重要的方面是提供高效的垃圾回收机制,高效的垃圾回收机制可以保证垃圾回收的快速运行,避免应用程序的性能瓶颈,又不会到时应用程序卡顿。

1.垃圾回收的基本算法

实际上,垃圾回收机制不可能实时检测到每个Java对象的状态,因此当一个对象失去引用后,它也不会立即被回收,只有等垃圾回收机制运行时才会被回收。 对于一个垃圾回收器的设计算法来说,大致如下可供选择的设计。

  • 串行回收(Serial)和并行回收(Parallel):串行回收就是不管系统有多少个CPU,始终只用一个CPU来执行回收操作;而并行回收就是把整个回收工作拆分成多部分,每个部分有一个CPU负责,从而让多个CPU并行回收。并行回收的执行效率很高,但复杂度增加,另外也有其他一些副作用,比如内存碎片会增加等。
  • 并发执行(Concorrent)和应用程序(Stop-the-world)停止:Stop-the-world的垃圾回收方式在执行垃圾回收的同时会导致应用程序暂停。并发执行的垃圾回收虽然不会导致应用程序暂停,但由于并发执行垃圾回收需要解决和应用程序的执行冲突(应用程序可能会在垃圾回收的过程中修改对象),因此并发执行垃圾回收的系统开销比Stop-the-world更高,而且执行时也需要更多的堆内存。
  • 压缩(Compacting)/不压缩(Non-compacting)和复制(Copying):为了减少内存碎片,支持压缩的垃圾回收器会把所有的活对象搬迁到一起,然后将之前占用的内存全部回收。不压缩的垃圾回收器只是回收内存,这样回收回来的内存不可能是连续的,因此将有较多的内存碎片,相对压缩垃圾回收机制,不压缩垃圾回收机制回收内存更快,而分配内存是就会更慢,而且无法解决内存碎片的问题。复制 垃圾回收会将所有的可达对象复制到另一块相同的内存中,这种方式的优点是垃圾回收过程不会产生内存碎片,但缺点也很明显,需要复制数据和额外的内存。

上面介绍的复制,不压缩,压缩都是垃圾回收器回收已用内存空间的方式,关于这三种方式详述如下:

  • 复制:将堆内存分成两个相同空间,从根(类似有向图起始顶点)开始访问每一个关联的可达对象,将空间A的可达对象全部复制到空间B,然后一次性回收整个空间A。

对于复制算法而言,因为只需访问所有的可达对象,将所有的可达对象复制完成后就回收整个空间,完全不用理会那些不可达对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。

  • 标记清除(mark-sweep):也就是不压缩回收方式。垃圾回收器先从根开始访问所有的可达对象,将他们标记为可状态,然后再遍历一次整个内存区域,对所有的没有标记为可达对象进行垃圾回收处理。
  • 标记压缩(mark-sweep-compact):这是压缩回收方式,这种方式充分利用上述两种算法的优点,垃圾回收器先从根开始访问所有的可达对象,将它们标记为可达状态。接下来垃圾回收器会将这些活动对象搬迁在一起,这个过程也被称为内存压缩,然后垃圾回收机制再次回收那些不可达对象所占用的内存空间,这样对避免了回收产生内存碎片。

上面无论用哪种回收方式,具体实现起来总是利弊参半。因此,实际垃圾回收时总是使用多种设计方式,也就是针对不同的情况采用不同的垃圾回收方式实现。

现行的垃圾回收器用分代的方式来采用不用的回收设计。分代的基本思路是根据对象生存时间的长短,把堆内存分成三代:

  • Young(新生代)
  • Old(老年代)
  • Permanent(永生代)

垃圾回收器会根据不同代的特点采用不同的回收算法,从而充分利用各种回收算法的优点。

2.堆内存的分代回收

分代回收的一个依据就是对象生存时间的长短,然后根据不同代采取不同的垃圾回收策略。采用这种“分代回收”的策略基于如下两点事实。

  • 绝大多数的对象不会被长时间引用,这些对象在其Young期间就会被回收。
  • 很老的对象(生存时间很长)和很新的对象(生存时间很短)之间很少存在互相引用的情况。

对于Young代的对象而言,大部分对象都会很快进入不可达状态,只要少量的对象能熬到垃圾回收执行,而垃圾回收器只需保留Young代中处于可达状态的对象,如果采用复制算法只需要少量的复制成本,因此大部分垃圾回收器对Young代都采用复制算法。

  1. Young代

对Young代采用复制算法只需遍历那些处于可达状态的对象,而且这些对象的数量较少,可复制成本也不大,因此可以充分发挥复制算法的优点。

Young代由一个Eden区和两个Survivor区构成。绝大多数对象先分配到Eden区中(有一些大的对象可能会直接被分配到old代中),Survivor区中的对象都至少在Young代中经历过一次垃圾回收,所以这些对象在被转移到old代之前会先保留在Survivor空间中。同一时间两个Sunrtvor空间中有一个用来保存对象,而另一个是空的,用来在下次垃圾回收时保存Young代中的对象。每次复制就是将Aden和第一个Survivpr区的可达对象复制到第二个Survivor区,然后清空Eden与第一个Survivor区。

young.PNG

2.Old代

如果Young代中的对象经过数次的垃圾回收依然没有被回收掉,即这个对象经过足够长的时间还处于可达状态,垃圾回收机制就会将这个对象转移到Old代。

old.PNG

Old代的大部分对象都是“久经考验”的老人了,因此它们没有那么容易被回收。而且随着时间的流逝,Old代的对象会越来越多,因此Old代的空间要比Young代的空间更大。出于这两点考虑,具有如下特征:

  • Old垃圾回收的执行频率无须太高。因为很少有对象会死掉。
  • 每次对Old代执行垃圾回收都需要更长的时间来完成。

基于以上考虑,垃圾回收器一般会采用标记压缩算法,这个算法可以避免复制Old代的大量对象,而且Old代的对象不会很快死亡,回收过程不会大量的产生内存碎片。因此相对比较划算。

3.Permanent代 Permanent代主要用于装载Class,方法等信息,默认为64MB,垃圾回收机制通常不会回收Permanent代的对象。对于那些需要加载很多类的服务器程序,往往需要加大Permanent代的内存,否则可能因为内存不足而导致程序终止。

当Young代的内存将要用完时,垃圾回收机制会对Young代进行垃圾回收,垃圾回收机制会采用较高的频率对Yn}rng代进行扫描和回收。因为这种回收的系统开销比较小,因此也被称为次要回收(minor collection ).当old代的内存将要用完时,垃圾回收机制会进行全回收,也就是对Young代和old代都要进行回收,此时回收成本就大得多了,因此也称为主要 回收(major callectivn)。

通常来说,Young代的内存会先被回收,而且会使用专门的回收算法(复制算法)来回收Young代的内存:对于Old代的回收频率则要低得多,因此也会采用专门的回收算法。如果需要进行内存压缩,那么每个代都独立地进行压缩。

3.常见的垃圾回收器

1.串行回收器

串行回收器通过对Young代和Old代的回收都是串行的(只使用一个CPU),而且垃圾回收执行期间会使的应用程序产生暂停。具体策略为,Young代采用串行复制算法,Old代采用串行标记压缩算法。

2.并行回收器

并行回收器对于Young代采用与串行回收器基本形似的回收算法,只是增加了多CPU并行的能力,即同时启动多线程并行来执行垃圾回收。线程数默认问CPU个数,当计算机中的CPU很多时,可以用-XX:ParallelGCThreads=size来减少并行线程的数目。

3.并行压缩回收器

并行压缩回收器的改变主要体现在对Old代的回收上。系统首先将Old代划分成几个固定大小的区域。在Mark阶段,多个垃圾回收线程会并行标记Old代中的可达对象。当某个对象被标记为可达对象时,还会更新对象所在区域的大小,以及该对象的位置信息。

接下来是summary阶段。summary阶段直接操作Old代的区域,而不是单个的对象。由于每次垃圾回收的压缩都会在Old代的左边部分存储大量的可达对象,对这样的高密度可达对象的区域进行压缩往往很不划算。所以summary阶段会从最左边的区域开始检测每个区域的密度,当检测到某个区域中能回收的空间达到了某个数值时(也就是可达对象的密度较小时),垃圾回收器会判定该区域,以及该区域右边的所有区域都应该进行回收,而该区域左边的区域都会被标识为密集区域,垃圾回收器既不会把新对象移动到这些密集区域中,也不会对这些密集区域进行压缩;该区域和其右边的所有区域都会被压缩并回收空间。summary阶段目前还是串行操作,虽然并行是可以实现的,但重要性不如对mark和压缩阶段的并行重要。

最后是compact阶段。回收器利用summary阶段生成的数据识别出有哪些区域是需要装填的,多个垃圾回收线程可以并行地将数据复制到这些区域中。经过这个过程后,Old代的一端会密集地存在大量的活动对象,另一端则存在大块的空闲块。

4.并发标识-清理(Mark-Sweep)回收器(CMS)

CMS回收器对Young代的回收方式和并行回收器的回收方式完全相同。由于对Young代的回收依然采用复制回收算法,因此垃圾回收时依然会导致程序暂停,除非依靠多CPU并行来提高垃圾回收的速度。

通常来说,建议适当加大Young代的内存。如果Young代的内存足够大就不用频繁地进行垃圾回收了,而且增大垃圾回收的时间间隔后可以让更多的位于Young代中的Java对象自己死掉,从而避免复制。但将Young代的内存设得过大也有一个坏处:当垃圾回收器回收Young代的内存时,复制成本会显著上升(复制算法必须等Young代满了之后才开始回收),所以回收时会让系统的暂停时间显著加大。

CMS对Old代的回收多数是并发操作,而不是并行操作。垃圾回收开始时需要一个短暂的暂停,此阶段称为初始标识(initial mark)阶段,这个阶段仅仅标识出那些被直接引用的可达对象。接下来进入并发标识阶段( concurrent marking phase),垃圾回收器会依据在初始标识中发现的可达对象来寻找其他的可达对象。由于在并发标识阶段应用程序也会同时在运行,无法保证所有的可达对象都被标识出来,因此应用程序会再次很短地暂停一下,多线程并行地重新标识之前可能因为井发而漏掉的对象,这个阶段被称为再标识(remark)阶段。

完成了再标识以后,所有的可达对象都已经被标识出来了,接下来就可以运行并发清理操作了。

4.内存管理小技巧

  • 尽量使用直接量 当需要使用字符串,还有Byte,Short,Integer,Long,Float,Double,Boolean,Character包装类的实例时,程序不应该采用new的方式来创建对象,而应该直接采用直接量来创建它们。
  • 使用StringBuilder和StringBuffer进行字符串连接

String,StringBuilder,StringBuffer都可以代表字符串,其中String代表字符序列不可变的字符串,而StringBuilder和StringBuffer都代表字符序列可变的字符串

如果程序使用多个String对象进行字符串连接运算,在运行时将生产大量的临时字符串,这些字符串会保存在内存中从而到时程序性能下降。

  • 尽早释放无用对象的引用

大部分时候,方法的局部引用变量所引用对象会随着方法的结束而变成垃圾,因为局部变量的生存期限很短,当方法运行结束时,该方法内的局部变量就结束了生存期限。因此大部分时候程序无须将局部引用变量显示设为null.

代码语言:javascript
复制
public void info(){
    Object object=new Object();
    System.out.println(object.toString());
    System.out.println(object.hashCode());
    object=null;
}

上面的方法随着info()方法执行完成,obj引用变量的作用域就结束了,原来的obj所引用的对象就会变成垃圾。因此object=null是没有必要的。但是如下代码:

代码语言:javascript
复制
public void info(){
    Object object=new Object();
    System.out.println(object.toString());
    System.out.println(object.hashCode());
    object=null;
    //执行耗时,耗内存操作
    //或者调用耗时,耗内存的方法
}

上面因为需要执行耗内存耗时的方法,可以尽早的释放对Object对象的引用。所以可能的是程序在执行耗时,耗内存操作时,obj之前所引用的Object对象可能被垃圾回收了。

  • 尽量少用静态变量

从理论上来说,Java对象何时被回收由垃圾回收机制决定,对程序员来说是不确定的。由于垃圾回收机制判断一个对象是否是垃圾的唯一标准是该对象是否有引用变量引用它,因此推荐尽早释放对象的引用。

最好的情况是,某个对象被static变量所引用,那么垃圾回收机制通常是不会回收这个对象所占的内存的。如下代码:

代码语言:javascript
复制
class Person{
    static Obejct obj=new Object();
}

Obj变量是Person类的静态变量,因此它的生命周期与Person类同步。在Person类不被卸载的情况下,Person类对应的Class对象会常驻内存,直到程序运行结束。因此,obj所引用的Object对象一旦被创建,也会常驻内存,知道程序结束。

  • 避免在经常调用的方法,循环中创建Java对象

经常调用的方法和循环有一个共同特征:这些代码段会被多次重复调用。如下:

代码语言:javascript
复制
for(int i=0;i<10;i++){
    Object obj=new Object();
}

虽然上面的obj是局部变量,执行完之后会失效,当时要循环创建10次,系统要不断地分配空间,执行初始化操作。这些对象的生存时间又不长,所以系统又要回收它们所占的内存空间,在这些操作中会消耗不少性能。

  • 缓存经常使用的对象

如果有些对象需要被经常使用,则可以考虑把这些对象用缓存池保存起来,,这样当下次需要时就可直接拿出这些对象来用,典型的缓存就是数据连接池,数据连接池缓存了大量的数据库连接,每次需要访问数据库是都可以直接去除数据库连接。

除此之外,系统的一些常用基础信息也可以通过缓存的方式存起来。实现缓存一般有两种方式:

1.使用HashMap进行缓存(不宜存储过多数据,从而内存过大,导致性能下降)

2.直接使用某些开源项目进行缓存

  • 尽量不要使用finalize()方法

当一个对象失去引用之后,垃圾回收器准备回收该对象之前,垃圾回收机制会先调用改对象的finalize()方法来执行资源清理。处于这种考虑,可能有些开发者会考虑使用finalize()方法来进行资源清理。

实际上,将资源清理放在finalize()方法中完成是非常拙劣的选择。根据前面介绍的垃圾回收算法,垃圾回收机制工作量已经够大了,尤其是回收Young代内存时, 大都会引起应用程序暂停,使得用户难以忍受。

在垃圾回收器本身已经严重制约应用程序性能的情况下,如果再选择使用finalize()方法进行资源清理,无疑是一种火上浇油的行为,这将导致垃圾回收器的负担更大,导致程序运行效率更差。

  • 考虑使用SoftReference

当程序需要创建长度很大的数组时,可以考虑使用SoftReference来包装数组元素,而不是直接让数组元素来引用对象。

SoftReference是一个很好的选择,当内存充足时不回收数据,当内存不充足时释放软引用所引用的对象。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.08.19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java引用的种类
    • 1.对象在内存中的状态
      • 2.强引用
        • 3.软引用
          • 4.弱引用
            • 5.虚引用
            • Java的内存泄漏
            • 垃圾回收机制
              • 1.垃圾回收的基本算法
                • 2.堆内存的分代回收
                  • 3.常见的垃圾回收器
                    • 4.内存管理小技巧
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档