JAVA内存管理

1.Java内存管理概述

在java中,有java程序、虚拟机、操作系统三个层次,其中java程序与虚拟机交互,而虚拟机与操作系统间交互,这就保证了java程序的平台无关性。
 
1、程序运行前:JVM向操作系统请求一定的内存空间,称为初始内存空间!程序执行过程中所需的内存都是由java虚拟机从这片内存空间中划分的。
 
2、程序运行中:java程序一直向java虚拟机申请内存,当程序所需要的内存空间超出初始内存空间时,java虚拟机会再次向操作系统申请更多的内存供程序使用!
 
3、内存溢出:程序接着运行,当java虚拟机已申请的内存达到了规定的最大内存空间,但程序还需要更多的内存,这时会出现内存溢出的错误!

至此可以看出,Java 程序所使用的内存是由 Java 虚拟机进行管理、分配的。Java 虚拟机规定了 Java 程序的初始内存空间和最大内存空间,
开发者只需要关心 Java 虚拟机是如何管理内存空间的,而不用关心某一种操作系统是如何管理内存. 
 
2.Java内存空间划分
Java 程序运行时的内存结构分成:
程序计数器:作用可以看成当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,
每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。---线程私有

虚拟机栈描述JAVA方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表

操作数栈,动态链接,方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型和对象引用类型,所需内存空间在编译期间完成分配。---线程私有

虚拟机栈中的两种异常状况:如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError;

如果虚拟机栈可以动态扩展,当扩展无法申请到足够的内存时抛出OutOfMemoryError。

本地方法栈:为虚拟机使用到的Native方法服务。---线程私有

堆:是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有的对象实例以及数组都要在堆中分配,垃圾收集器管理的主要区域

方法区:存储被虚拟机加载的类信息(类名、访问修饰符、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码等数据。

垃圾收集器主要是针对该区域的常量的回收和对类型的卸载

运行时常量池(属于方法区部分):存放编译期生成的各种字面量和符号引用。动态性:运行期间也可能将新的常量放入池中,如String类的intern()方法。

直接内存:堆外内存,新IO类中引入的机遇通道Channel与缓冲的I/O方式,使用Native函数库直接分配对外内存。

  中文版

   

3.Java垃圾回收机制(内存空间的释放)

Java语言中,内存回收任务由JVM来担当。

在程序的运行环境中,JVM提供了一个系统级的垃圾回收器线程,它负责自动回收那些无用对象所占用的内存。这种内存回收的过程被称为垃圾回收。

垃圾回收优点:

    • 程序员从复杂的内存追踪,监测和释放等工作解放出来,减轻程序员进行内存管理的负担。
    • 防止系统内存被非法释放,从而使系统更加健壮和稳定。
    • 只有当对象不再被程序中的任何引用变量引用时,它的内存才可能被回收。
    • 程序无法迫使垃圾回收器立即执行垃圾回收操作。
    • 当垃圾回收器将要回收无用对象的内存时,先调用该对象的finalize()方法,该方法有可能使对象使对象复活,导致垃圾回收器取消回收该对象的内存

对象的可触及性:

在JVM的垃圾回收器来看。堆区中的每个对象都肯能处于以下三个状态之一:

    • 可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
    • 可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
    • 不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。

垃圾回收的时间

    当一个对象处于可复活状态时,垃圾回收线程执行它的finalize()方法,任何使它转到不可触及状态,任何回收它占用的内存,这对于程序来说都是透明的。程序只能决定一个对象任何不再被任何引用变量引用,使得它成为可以被回收的垃圾。

    类比:居民把无用物品放在指定的地方,清洁工人会把它收拾走。但垃圾被收走的时间,居民是不知道的,也无需了解。

    垃圾回收器作为低优先级线程独立运行。在任何时候,程序都无法迫使垃圾回收器立即执行垃圾会后操作。

程序中可调用System.gc()或Runtime.gc()方法提示垃圾回收器尽快执行垃圾回收操作,但是不能保证调用后垃圾回收器会立即执行垃圾回收。

    类比:小区垃圾成堆时,居民打电话给环保局,催促清洁工尽快来处理垃圾。但是清洁工不一定立即就来了,也有可能很长时间后再来。

对象的finalize()方法简介

finalize()定义在Object类中:

protected void finalize() throws Throwable

因为该方法为protected,所以任何Java类都可以覆盖finalize()方法,该方法中进行释放对象所占的相关资源的操作。

注意:

JVM的垃圾回收操作对程序来说都是透明的。因此程序无法预料某个无用对象的finalize()方法何时被释放。

finalize()方法的特点:

  • 垃圾回收器是否会执行该方法及何时执行该方法,都是不确定的。
  • finalize()方法有可能使对象复活,使它恢复到可触及状态。
  • 垃圾回收器在执行finalize()方法时,如果出现异常,垃圾回收器不会报告异常,程序继续正常运行。

   强引用(StrongReference

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

    当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解  决内存不足的问题。

    软引用(SoftReference)

   如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

  只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的    高速缓存

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中

 弱引用(WeakReference)

   弱引用与软引用的区别在于:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,

 不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

   弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

 弱引用可以让您保持对对象的引用,同时允许GC在必要时释放对象,回收内存。

 对于那些创建便宜但耗费大量内存的对象,即希望保持该对象,又要在应用程序需要时使用,同时希望GC必要时回收时,可以考虑使用弱引用

 虚引用(PhantomReference)

  “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

   虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:

 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,

  如果发现它还有虚引用 就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中

4.Java内存泄露

导致内存泄漏主要的原因是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,那么这些对象就会驻留内存,消耗内存,

因为无法让垃圾回收器GC验证这些对象是否不再需要。如果存在对象的引用,这个对象就被定义为"有效的活动",同时不会被释放。

要确定对象所占内存将被回收,我们就要务必确认该对象不再会被使用。典型的做法就是把对象数据成员设为null或者从集合中移除该对象。

但当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

容易引起内存泄漏的几大原因
1.静态集合类
      像HashMap、Vector 等静态集合类的使用最容易引起内存泄漏,因为这些静态变量的生命周期与应用程序一致,如示例1,如果该Vector 是静态的,那么它将一直存在,而其中所有的Object对象也不能被释放,因为它们也将一直被该Vector 引用着。

当集合里面的对象属性被修改后,再调用remove()方法时不起作用。 
例: 

public static void main(String[] args)  
{  
Set<Person> set = new HashSet<Person>();  
Person p1 = new Person("唐僧","pwd1",25);  
Person p2 = new Person("孙悟空","pwd2",26);  
Person p3 = new Person("猪八戒","pwd3",27);  
set.add(p1);  
set.add(p2);  
set.add(p3);  
System.out.println("总共有:"+set.size()+" 个元素!");  //结果:总共有:3 个元素!   
p3.setAge(2);    //修改p3的年龄,此时p3元素对应的hashcode值发生改变   
set.remove(p3);  //此时remove不掉,造成内存泄漏   
set.add(p3);     //重新添加,居然添加成功   
System.out.println("总共有:"+set.size()+" 个元素!");  //结果:总共有:4 个元素!   
for (Person person : set)  
{  
        System.out.println(person);  
}   
} 

2.监听器
     在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
3.物理连接
         一些物理连接,比如数据库连接和网络连接,除非其显式的关闭了连接,否则是不会自动被GC 回收的。Java 数据库连接一般用DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放,因为这些连接是独立于JVM的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。
4.内部类和外部模块等的引用
        内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。对于程序员而言,自己的程序很清楚,如果发现内存泄漏,自己对这些对象的引用可以很快定位并解决,但是现在的应用软件
并非一个人实现,模块化的思想在现代软件中非常明显,所以程序员要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用

   5.单例模式 

posted @ 2013-09-07 09:29  野原新之助  阅读(258)  评论(0编辑  收藏  举报