`
footman265
  • 浏览: 114764 次
  • 性别: Icon_minigender_1
  • 来自: 宁波
社区版块
存档分类
最新评论

Java内存模型8

    博客分类:
  • j2SE
阅读更多

[1]全局Map造成的内存泄漏:
  无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息:
public class SocketManager{
    private Map<Socket,User> m = new HashMap<Socket,User>();
    public void setUser(Socket s,User u)
    {
        m.put(s,u);
    }
    public User getUser(Socket s){
        return m.get(s);
    }
    public void removeUser(Socket s){
        m.remove(s);
    }
}

SocketManager socketManager;
//...
socketManager.setUser(socket,user);
  这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。
  [2]弱引用内存泄漏代码:
  程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。
public class MapLeaker{
    public ExecuteService exec = Executors.newFixedThreadPool(5);
    public Map<Task,TaskStatus> taskStatus
        = Collections.synchronizedMap(new HashMap<Task,TaskStatus>());
    private Random random = new Random();
    private enum TaskStatus { NOT_STARTEDSTARTEDFINISHED };
    private class Task implements Runnable{
        private int[] numbers = new int[random.nextInt(200)];
        public void run()
        {
            int[] temp = new int[random.nextInt(10000)];
            taskStatus.put(this,TaskStatus.STARTED);
            doSomework();
            taskStatus.put(this,TaskStatus.FINISHED);
        }
    }
    public Task newTask()
    {
        Task t = new Task();
        taskStatus.put(t,TaskStatus.NOT_STARTED);
        exec.execute(t);
        return t;
    }
}
  [3]使用弱引用堵住内存泄漏:
  SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏——利用弱引用。弱引用是对一个对象(称为 referent的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachableWeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear(),get() 会返回 null。相应地,在使用其结果之前,应当总是检查get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。下边的代码给出了 WeakHashMap 的 get() 方法的一种可能实现,它展示了弱引用的使用:
public class WeakHashMap<K,Vimplements Map<K,V>
{
    private static class Entry<K,Vextends WeakReference<Kimplements Map.Entry<K,V>
    {
        private V value;
        private final int hash;
        private Entry<K,V> next;
        // ...
    }

    public V get(Object key)
    {
        int hash = getHash(key);
        Entry<K,V> e = getChain(hash);
        while(e != null)
        {
            k eKey = e.get();
            if( e.hash == hash && (key == eKey || key.equals(eKey)))
                return e.value;
            e = e.next;
        }
        return null;
    }
}
  调用 WeakReference.get() 时,它返回一个对 referent 的强引用如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法——一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。
  [4]使用WeakHashMap堵住泄漏
  在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下边代码所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。
public class SocketManager{
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();
    public void setUser(Socket s, User s)
    {
        m.put(s,u);
    }
    public User getUser(Socket s)
    {
        return m.get(s);
    }
}
  引用队列:
  WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列的作用。引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 ——polled、timed blocking 和 untimed blocking。)WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。
  4)关于Java中引用思考:
  先观察一个列表:
级别 回收时间 用途 生存时间
强引用 从来不会被回收 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 在客户端移除对象引用过后,除非再次激活,否则就放在内存敏感的缓存中 内存不足时终止
弱引用 在垃圾回收时,也就是客户端已经移除了强引用,但是这种情况下内存还是客户端引用可达的 阻止自动删除不需要用的对象 GC运行后终止
虚引用[幽灵引用] 对象死亡之前,就是进行finalize()方法调用附近 特殊的清除过程 不定,当finalize()函数运行过后再回收,有可能之前就已经被回收了。
  可以这样理解:
  SoftReference:假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。 软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。 此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。一般来说,WeakReference我们用来防止内存泄漏,保证内存对象被VM回收。
  WeakReference:弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。弱引用最常用于实现规范化的映射。假定垃圾回收器确定在某一时间点上某个对象是弱可到达对象。这时,它将自动清除针对此对象的所有弱引用,以及通过强引用链和软引用,可以从其到达该对象的针对任何其他弱可到达对象的所有弱引用。同时它将声明所有以前的弱可到达对象为可终结的。在同一时间或晚些时候,它将那些已经向引用队列注册的新清除的弱引用加入队列。 SoftReference多用作来实现cache机制,保证cache的有效性。
  PhantomReference:虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派 pre-mortem 清除操作。如果垃圾回收器确定在某一特定时间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时间,它会将该引用加入队列。为了确保可回收的对象仍然保持原状,虚引用的指示对象不能被检索:虚引用的 get 方法总是返回 null。与软引用和弱引用不同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到达
  以下是不确定概念
  【*:Java引用的深入部分一直都是讨论得比较多的话题,上边大部分为摘录整理,这里再谈谈我个人的一些看法。从整个JVM框架结构来看,Java的引用垃圾回收器形成了针对Java内存堆的一个对象的“闭包管理集”,其中在基本代码里面常用的就是强引用,强引用主要使用目的是就是编程的正常逻辑,这是所有的开发人员最容易理解的,而弱引用和软引用的作用是比较耐人寻味的。按照引用强弱,其排序可以为:强引用——软引用——弱引用——虚引用,为什么这样写呢,实际上针对垃圾回收器而言,强引用是它绝对不会随便去动的区域,因为在内存堆里面的对象,只有当前对象不是强引用的时候,该对象才会进入垃圾回收器目标区域
  软引用又可以理解为“内存应急引用”,也就是说它和GC是完整地配合操作的,为了防止内存泄漏,当GC在回收过程出现内存不足的时候,软引用会被优先回收,从垃圾回收算法上讲,软引用在设计的时候是很容易被垃圾回收器发现的。为什么软引用是处理告诉缓存的优先选择的,主要有两个原因:第一,它对内存非常敏感,从抽象意义上讲,我们甚至可以任何它和内存的变化紧紧绑定到一起操作的,因为内存一旦不足的时候,它会优先向垃圾回收器报警以提示内存不足;第二,它会尽量保证系统在OutOfMemoryError之前将对象直接设置成为不可达,以保证不会出现内存溢出的情况;所以使用软引用来处理Java引用里面的高速缓存是很不错的选择。其实软引用不仅仅和内存敏感,实际上和垃圾回收器的交互也是敏感的,这点可以这样理解,因为当内存不足的时候,软引用会报警,而这种报警会提示垃圾回收器针对目前的一些内存进行清除操作,而在有软引用存在的内存堆里面,垃圾回收器会第一时间反应,否则就会MemoryOut了。按照我们正常的思维来考虑,垃圾回收器针对我们调用System.gc()的时候,是不会轻易理睬的,因为仅仅是收到了来自强引用层代码的请求,至于它是否回收还得看JVM内部环境的条件是否满足,但是如果是软引用的方式去申请垃圾回收器会优先反应,只是我们在开发过程不能控制软引用对垃圾回收器发送垃圾回收申请,而JVM规范里面也指出了软引用不会轻易发送申请到垃圾回收器。这里还需要解释的一点的是软引用发送申请不是说软引用像我们调用System.gc()这样直接申请垃圾回收,而是说软引用会设置对象引用为null,而垃圾回收器针对该引用的这种做法也会优先响应,我们可以理解为是软引用对象在向垃圾回收器发送申请。反应快并不代表垃圾回收器会实时反应,还是会在寻找软引用引用到的对象的时候遵循一定的回收规则,反应快在这里的解释是相对强引用设置对象为null,当软引用设置对象为null的时候,该对象的被收集的优先级比较
  弱引用是一种比软引用相对复杂的引用,其实弱引用和软引用都是Java程序可以控制的,也就是说可以通过代码直接使得引用针对弱可及对象以及软可及对象是可引用的,软引用和弱引用引用的对象实际上通过一定的代码操作是可重新激活的,只是一般不会做这样的操作,这样的用法违背了最初的设计。弱引用和软引用在垃圾回收器的目标范围有一点点不同的就是,使用垃圾回收算法是很难找到弱引用的,也就是说弱引用用来监控垃圾回收的整个流程也是一种很好的选择,它不会影响垃圾回收的正常流程,这样就可以规范化整个对象从设置为null了过后的一个生命周期的代码监控。而且因为弱引用是否存在对垃圾回收整个流程都不会造成影响,可以这样认为,垃圾回收器找得到弱引用,该引用的对象就会被回收,如果找不到弱引用,一旦等到GC完成了垃圾回收过后,弱引用引用的对象占用的内存也会自动释放,这就是软引用在垃圾回收过后的自动终止。
  最后谈谈虚引用,虚引用应该是JVM里面最厉害的一种引用,它的厉害在于它可以在对象的内存物理内存中清除掉了过后再引用该对象,也就是说当虚引用引用到对象的时候,这个对象实际已经从物理内存堆清除掉了,如果我们不用手动对对象死亡或者濒临死亡进行处理的话,JVM会默认调用finalize函数,但是虚引用存在于该函数附近的生命周期内,所以可以手动对对象的这个范围的周期进行监控。它之所以称为“幽灵引用”就是因为该对象的物理内存已经不存在的,我个人觉得JVM保存了一个对象状态的镜像索引,而这个镜像索引里面包含了对象在这个生命周期需要的所有内容,这里的所需要就是这个生命周期内需要的对象数据内容,也就是对象死亡和濒临死亡之前finalize函数附近,至于强引用所需要的其他对象附加内容是不需要在这个镜像里面包含的,所以即使物理内存不存在,还是可以通过虚引用监控到该对象的,只是这种情况是否可以让对象重新激活为强引用我就不敢说了。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。

5.总结:
  本章节主要涵盖了Java里面比较底层的一个章节,主要是以JVM内存模型为基础包括JVM针对内存的线程模型的探讨以及针对Java里面内存堆和栈的详细分析。特别感谢白远方同学提供的汇编方面关于操作系统以及内存发展的资料提供。
  参考:IBM开发中心文档,《Inside JVM》
  本文的讲解可能比较概念化,希望所有读者能够仔细品味,Java与对象相关的底层内从这里面都提及到了,主要是方便初学者和深入者能够更加理解Java虚拟机处理Java对象的一个流程以及底层的相关原理,也方便查询和参考,可能会有不完善的地方,如果有什么概念错误,请来Email告知,谢谢:silentbalanceyh@126.com。而且因为这一个章节的内容概念很多,整理思考和撰写花了太长时间,抱歉!

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics