Java内存管理

对于某些编程语言,内存管理的工作需要开发人员处理,比如C/C++。以C++为例,程序通过new操作符创建新对象之后,就会分配相应的内存资源,当程序不再需要这些对象时,需要在代码中将其显式释放,一般通过delete操作符完成。内存分配和释放的正确性,由开发人员来保证。开发过程中可能出现的与内存释放相关的问题主要有:悬挂引用(野指针)内存泄露

  • 悬挂引用

C/C++中常说的野指针。悬挂引用指的是对某个对象引用实际上指向一个错误的内存地址。比如,

1     int * a = new int [10];
2     int * b = a;  // b 和 a 指向同一块内存区域
3     delete [] a;     // 释放 a 指向的那块内存区域
4     printf("b[0]:%d", b[0]);    // 打印出无效的值
5     b[0] = 2;   // 再次引用已经释放的内存区域,结果无法预料。       

上述代码中指针b就是一个野指针。b和a指向同一块内存控件,"delete a;"语句已经将该空间释放,对b[0]进行打印会出现莫名的值,而非初始化的0,而"b[0] = 2;"试图再次引用该内存区域将会出现不可预知的错误。

  • 内存泄露

某些对象所占用的内存没有被释放,又没有对象引用指向这些内存。这样就导致这部分内存对程序来说既不可用,又无法被释放,出现内存泄露。C/C++中的内存泄露指的是内存不可达。

JAVA自动内存管理机制

因为显式内存管理比较容易出错,因此不少语言引入了内存自动管理机制,典型的代表为Java语言。Java提供了垃圾回收器(GC)来自动回收程序之后不再使用的内存。GC不仅负责内存的回收,还负责内存的分配。

当Java程序运行时,GC也同时运行在一个单独的线程中,它会根据虚拟机当前的内存状态决定什么时候进行垃圾回收工作。而GC的执行回收的具体时间和频率无法预估,取决于GC的实现算法。程序可以通过System.gc方法建议GC立即执行垃圾回收,不过在这种情况下GC也可能选择不执行回收。

引用类型 

某些情况,当系统可用内存越来越少,而GC又无法找到足够可用的空闲内存时,创建新对象的操作就会抛出OOM(OutOfMemory)错误,导致虚拟机退出。比如,一个图像处理程序可能同时打开了多个图像文件进行编辑,而同一时刻只有一张图片处于编辑状态,当同时打开多张图片时,程序占用的内存空间就会变大,而GC又无法回收这些处于活动的对象所占用的内存,使得可用内存越来越少。之后创建新对象的操作就可能导致OOM错误。

对于上述情况,我们的Java程序需要通过一种方式把对象在内存需求方面的特征告诉GC,GC可以根据对象特征进行更好地回收。这种方式就是通过Java中对象的引用类型来实现。程序运行中,对于同一个对象,可能存在多个指向它的引用,如果不再有引用指向该对象,那么这个对象就会成为GC的候选目标。Java中存在不同的对象引用类型,不同类型的引用对GC的含义是不同的。分别为强引用、软引用、弱引用以及虚引用。

强引用(Strong Reference)

最常见的引用类型,也是默认的引用类型。使用new操作符创建一个的新对象,并将其复制给一个变量的时候,这个变量就成为指向该对象的一个强引用。例如:

"Object a = new Object();", 变量a就是一个强引用,指向一个new操作符创建的Object类型的对象。对于强引用的对象,GC是不会将其作为垃圾收集的候选目标。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

强引用会阻止GC对其进行内存释放,如果某个对象之后不再使用,而程序中却一直保存着其强引用,就会导致GC无法对这块内存进行释放,造成了“Java中所说的内存泄露”。这种内存泄露和之前提到的C/C++的内存泄露意思不同,C/C++中内存泄露是指某些内存不可达,而又无法释放。Java中不可达的内存直接就被GC回收了,Java中的内存泄露本质上是这些内存是可达的,而之后程序不想继续使用它却无法被GC回收,造成了“泄露”。

Java中的内存泄露主要分两种情况:

  • 虚拟机中存在程序无法使用的内存区域。这些内存区域被程序中一些无法使用的存活对象占用,由于其隐式的强引用,导致无法被GC回收。造成这种问题主要是程序编写时候的逻辑错误。
  • 程序中存在大量存活时间过长的对象。这些对象存活时间长于使用它们的对象,正常情况,这些对象在引用它们的对象被回收后也应该被回收,但是由于某写程序中的错误而没有被回收。这些对象无法被回收,时间一长,虚拟机因为没有足够内存分配给新对象而抛出OOM错误。

第一种内存泄露的例子如下:

 1 public class LeakedQueue<T> {
 2     private List<T> backendList = new ArrayList<T>(); 
 3     private int topIndex = 0;
 4     
 5     public void enqueue(T value) {
 6         backendList.add(value);
 7     }
 8     
 9     public T dequeue() {
10         if (topIndex < backendList.size()) {
11             T result = backendList.get(topIndex);
12             topIndex++;
13             return result;
14         }
15         return null;
16     }
17 }
隐式强引用导致内存泄露

出对方法会造成内存泄露。对于dequeue的对象,backendList没有将其删除,而依然保存了该对象的强引用,因此该对象无法被GC回收。多次执行enqueue和dequeue后将会使虚拟机可用内存越来越少,导致OOM错误。

第二种内存泄露情况通常发生在基于内存实现的缓存的时候,例子如下:

 1 public class Calculator {
 2     private Map<String, Object> cache = new HashMap<String, Object>();
 3     
 4     public Object calculate(String expr) {
 5         if (cache.containsKey(expr)) {
 6             return cache.get(expr);
 7         }
 8         Object result = doCalculate(expr);
 9         cache.put(expr, result);
10         return result;
11     }
12     
13     private Object doCalculate(String expr) {
14         return new Object();
15     }
16 }
缓存生命周期过长

缓存的存活时间和Calculator一样长,只要Calculator无法被回收,其中所包含的计算结构对象也无法被回收,不断执行新的calculate,将会导致内部缓存越来越大,可用内存越来越少。

当强引用存在的时候,所指向的对象无法被GC回收,为了增强程序与GC的交互能力,JDK 1.2引入了java.lang.ref包,提供了三种新的引用类型,分别是软引用,弱引用和虚引用。这些引用类型除了可以引用对象之外,还可以在不同程度上影响GC对被引用对象的处理行为。

软引用(Soft Reference)

软引用强度上弱于强引用,用SoftReference类来表示。如果一个对象不是强引用可达,同时可以通过软引用来访问,则该对象时软引用可达的。软引用传递给GC的信息是:只有你还有足够的内存,就不要回收软引用指向的对象。GC会在抛出OOM错误发生之前,回收掉软引用指向的对象。

 使用软引用:

1         Object aObject = new Object();
2         SoftReference<Object> ref = new SoftReference<Object>(aObject);
3         aObject = null;    // 必须要释放掉强引用,否则无意义。因为只要强引用存在,GC就不会进行回收。
4         
5         Object bObject = ref.get();    // 通过get方法获取引用的对象,获取之后需要判断是否为NULL,因为可能已经被GC回收
6         if (bObject != null) {
7             // your opr
8         }

弱引用(Weak Reference)

弱引用在强度上弱于软引用,用WeakReference表示。弱引用传递给GC的信息是:在判断一个对象是否存活时,可以不考虑弱引用的存在。只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

虚引用(Phantom Reference)

虚引用是强度最弱的一种引用,用PhantomReference类表示。虚引用的主要目的是在一个对象所占的内存被实际回收之前得到通知,从而可以进行一些相关的清理工作。虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用在使用上与前两种引用有很大不同:首先,虚引用创建时必须提供一个引用队列作为参数;其次,虚引用对象的get方法总是返回null,因此无法通过虚引用获取被引用的对象。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

引用队列

引用队列的主要作用是作为一个通知机制。当对象的可达状态发生变化时,如果程序希望得到通知,可以使用引用队列。当从引用队列中获取了引用对象后,不可能再获取所指向的具体对象。因为,对于软引用和弱引用,在被放入队列之前,它们的引用关系就已经被清除。而虚引用的get方法总是返回null。

原文地址:https://www.cnblogs.com/luckyxiaoxuan/p/4463166.html