Java随笔 垃圾回收机制

垃圾回收机制

垃圾回收的意义

在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;
而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。

圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。

垃圾回收能自动释放内存空间,减轻编程的负担。
垃圾回收的一个潜在的缺点是它的开销影响程序性能。

垃圾回收主要关注 Java 堆

图摘自《码出高效》

Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由 JIT 编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

按代的垃圾回收机制

新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称为:Minor GC。

老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正因为其较大的空间,发生在老年代的GC次数要比新生代少的多。对象从老年代消失的过程,称之为:Major GC或者Full GC。

持久层(Permanent generation)也称为方法区(Method area):用来保存类常量以及字符串常量。注意,这个区域不是用来存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC时间也被算为Major GC。只不过这个区域发生GC的条件非常苛刻,必须符合以下三种条件才会被回收:
1.所有实例被回收
2.加载该类的ClassLoader被回收
3.CLass对象无法通过任何途径访问(包括访问)

JVM GC什么时候执行?
eden区空间不够存放新对象的时候,执行Minro GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,小于时被HandlePromotionFailure参数强制full gc。调优主要是减少Full GC的触发次数,可以通过NewRatio控制新生代转老年代的比例,通过Max
Tenuring Threshold设置对象进入老年代的年龄阀值。

JVM 和垃圾回收

在开始讨论引用之前,首先需要了解一下 JVM 和垃圾回收。Java 与 C 语言等不同,C 语言代码经过编译器编译之后,就可以直接在 CPU 上执行,而 Java 不同,Java 语言需要先编译生成字节码文件,再由 JVM 生成可以在 CPU 上运行的代码。

而且 Java 在生成对象时,并不需要手动分配内存,而是由 JVM 自动进行分配,对于不再使用的对象,JVM 会对这些对象占用的内存进行回收,这个过程称之为垃圾回收(Garbage Collection,简称 GC)。

JVM 负责对程序运行时的内存进行管理。为了提升管理效率,JVM 将运行时的内存划分成了不同的区域,总体上的划分如下:

img

JVM 内存模型

每个线程独占一个虚拟机栈,通过程序计数器记录当前代码执行的位置,本地方法栈与虚拟机栈类似。

程序中创建的大多数对象都在堆中分配内存,然后栈中的变量通过引用来指向堆中的对象,所以堆是各个线程共享的一块内存区域。方法区中则存储 Java 的类型信息、常量、静态变量等等(常量,静态变量也有可能会引用对象)。

栈内的变量通过引用来和堆内的对象建立联系,建立联系的方式有两种:使用句柄或者直接指针。

使用句柄方式如下:

img

句柄引用对象

使用直接指针如下:

img

直接指针引用对象

使用句柄的好处是引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的数据,而不会修改引用中的数据。但是直接指针的速度会更快,在主流的 HotSpot 虚拟机中,使用的就是直接指针。

Java 引用

Java 中的引用类型总共有四种:强引用,软引用,弱引用,虚引用。

强引用就是最普通的对象引用,每当 new 一个对象的时候,都是生成一个强引用。这种引用对垃圾回收免疫,即使发生 OOM,这种对象也不会被回收。

Object o = new Object();

软引用的强度相对弱一些,在发生 OOM 之前,JVM 会尝试去回收这些对象,软引用的实现类是 SoftReference。

Object o = new Object();
SoftReference srf = new SoftReference(o);

弱引用就更弱了,如果碰上了垃圾回收,弱引用的对象肯定会被回收,弱引用的实现类是 WeakReference。

Object o = new Object();
WeakReference wrf = new WeakReference(o);

虚引用无法引用对象,实际只是做一些垃圾清理之后的事情, 虚引用的实现类是 PhantomReference。

Object o = new Object();
ReferenceQueue rq = new ReferenceQueue();
PhantomReference prf = new PhantomReference(o, rq);

上面的各种引用都继承了 Reference 类,Reference 类中有一个 get 方法,如果软引用和弱引用所指向的对象没有被回收,那么使用 get 方法就可以获取原对象的引用。

Object o = new Object();
SoftReference srf = new SoftReference(o);
o = null; // 断开强引用
System.out.println(srf.get()); // java.lang.Object@17579e0f

对软引用手动触发垃圾回收:

Object o = new Object();
SoftReference srf = new SoftReference(o);
o = null;
System.gc(); // 手动触发垃圾回收
System.out.println(srf.get()); // java.lang.Object@17579e0f

由于内存充足,所以软引用指向的对象并没有被回收。对于弱引用来说,情况就不一样:

Object o = new Object();
WeakReference wrf = new WeakReference(o);
o = null; // 断开强引用
System.out.println(wrf.get()); // java.lang.Object@17579e0f

对弱引用手动触发垃圾回收:

Object o = new Object();
WeakReference wrf = new WeakReference(o);
o = null;
System.gc(); // 手动触发垃圾回收
System.out.println(wrf.get()); // null

由上面的代码可知,弱引用一定会被垃圾回收。软引用和弱引用一个经典的应用场景就是作为缓存使用,这两种引用所指向的对象一定会在发生 OOM 之前被回收,所以不会导致内存泄露问题。

虚引用 PhantomReference 的 get 方法会一直返回 null,所以无法通过虚引用获取到对象。虚引用的意义在于提供了一种在对象被回收之后做某些事情的机制,在这里就需要谈到引用队列。

ReferenceQueue 称之为引用队列。如果我们为一个引用指定一个引用队列,那么这个引用所指向的队列在被垃圾回收后,该引用就会被加入到引用队列中。

我们就可以根据引用队列中的引用来判断某个对象是否被回收,或者直接清除引用队列的引用对象,具体的逻辑要看具体的业务场景。

引用和对象的可达性

假设新生成了一个对象:

Object o = new Object();

这个时候 o 是一个强引用,所以这个对象无法被回收。

o = null;

这样一来,这个变量就不再指向这个对象了,假设也没有其他类型的引用来指向这个对象,那么这个对象就称之为不可达,就可以被回收了。

Java 中使用可达性分析来判断对象是否要被回收。可达性的出发点是一些被称之为 GC Roots 的根对象,以下的对象可以作为 GC Roots:

  • 栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • JVM 内部的引用,比如基本数据类型对应的 Class 对象

判断一个对象是否存活其实就是通过引用的类型来进行判断,对于弱引用和虚引用来说,基本就可以认为是不可达了,在下次垃圾回收时就会被回收,而对于强引用,毫无疑问,肯定是可达的。

最难处理的就是软引用。软引用在 JVM 中并没有明确把软引用判断为可达还是不可达,而是会根据当前系统的状态进行判断,如果当前系统内存充足,那么该对象就会被判断为可达,如果系统内存不足,那么该对象就会倾向被回收。

触发主GC的条件

JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大。更值得关注的是主GC的触发条件,因为它对系统影响很明显。总的来说,有两个条件会触发主GC:
  1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
  2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
  由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。

减少GC开销的措施

根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

  (1)不要显式调用System.gc()

  此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

  (2)尽量减少临时对象的使用

  临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

  (3)对象不用时最好显式置为Null

  一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

  (4)尽量使用StringBuffer,而不用String来累加字符串

  由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

  (5)能用基本类型如Int,Long,就不用Integer,Long对象

  基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

  (6)尽量少用静态对象变量

  静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

  (7)分散对象创建或删除的时间

  集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

  (8)计划好List的容量

​ List初始化,有足够的容量,所有这样可以减少内部数组在运行时不必要的分配和释放。

posted @ 2021-01-04 14:40  Sheldon-Cooper  阅读(102)  评论(0)    收藏  举报