`
deepinmind
  • 浏览: 452196 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:41677
社区版块
存档分类
最新评论

Java的对象驻留

阅读更多
Java会将源代码中的字符串常量存储到常量池中。也就是说,当你这么写的时候:

String a = "I am a string";
String b = "I am a string";
 


变量a和变量b是同一个值。这不只是说它俩的值是一样的,而是说就是同一个字符串对象。用Java的话来说就是a==b的结果是true。然而这个只对字符串以及小的整型或者长整型有效。其它的对象是不会被驻留的,也就是说如果你创建了两个对象而他们的值是相等的,但他们并不是同一个对象。这个问题有时候很讨厌,尤其是当你从某个持久化存储中取出一个对象时。如果同一个对象你取了两次,你当然希望最终取出的是同一个对象,不过实际上你取出的是两份拷贝。换句话说你其实希望的是取出的是存储中那个对象在内存里面的同一个拷贝。有些存储层是会做这样的处理的。比如说JPA的实现就是遵循这个模式的,而别的情况可能你就得自己去做缓存了。

这篇文章中我会介绍一个简单的内部池的实现,这在stackoverflow上也有一个[url=http://it.deepinmind.com]相关的主题[/url]。我还会把为什么会选用文中的这种实现方式的具体原因介绍给大家。跟stackoverflow上面的讨论相比,本文中会有附上详细的教程信息。

[b]对象池[/b]

对象驻留需要一个对象池。当你需要驻留一个对象时,你会先去看一下对象池里面是不是已经有一个同样的对象了。如果已经有了,就直接使用那个对象。如果还没有,就把这个对象放进池里然后使用它。

实现过程中我们主要面临的有两大问题:


垃圾回收
多线程



当不再需要一个对象时,应该把它从池里移除掉。这个移除操作可以由应用程序负责,不过这样的做法太老套了。Java和C++相比,最大的好处就是垃圾回收(译注:C++的同学看到后求别喷)。我们可以让GC来回收这些对象。想要这样做的话,池里的对象就不能再用强引用了。

[b]引用[/b]

如果你已经知道什么是软引用,弱引用和虚引用,就直接跳到下节。

你可能已经注意到了,我没有直接说引用,而是说的强引用。如果你认为GC是回收那些没有引用的对象的话,这个是不太准确的。实际上GC判断对象是否可达看的是强引用。更准确的描述是,一个对象存在强引用,必定是从其它强引用对象的本地变量,静态变量或者其它类似的地方直接引用过来的。换句话说,如果一堆对象通过某个不存活的对象“强引用”过来的话,它们会被一起回收掉。

如果这是强引用的话,那当然你会想肯定有些引用不是强引用了。对的,有一个java.lang.Reference类,它有几个子类,分别是:


PhantomReference
WeakReference and
SoftReference


它们都在同一个包下面。如果你看一下文档,你会觉得,这里我们应该需要用到的是弱引用。虚引用肯定是不能用到这个池里了,因为通过它可能无法获取到最终的对象。软引用的话,又显得有些浪费了。如果已经没有强引用了,也没必要把对象留在池里了。下次还有请求过来的话,我们会重新再驻留它。当然这肯定就是另外一个实例了,不过你也察觉不到这个,因为原先的那个早就没人引用了。

弱引用可以用来获取对象,但又不会影响GC的判断。

[b]WeakHashMap[/b]

当然我们不会直接使用弱引用。有一个叫WeakHashMap的类,它使用弱引用来引用key。这就是我们想要的。当我们驻留一个对象时候,我们会看池里是否已经有一个一样的了,Map正好可以用来查找对象。用弱引用来存储key的话,如果这个key不再需要了,GC会负责回收掉它。

现在我们已经可以查找对象了,很好。用map是为了获取对象,如果我们想获取到同一个对象的话,如果它还没在池里,我们当然得先把它存到map里。不过如果直接放对象存到map里就破坏了我们使用弱引用存储key的初衷了。我们得把这个目标对象也存成弱引用的。

[b]WeakPool[/b]

解释了这么多,该上代码了。如果池里有一个对象和要驻留的对象一样的话,就返回那个对象。如果没有,直接返回null,同时put方法用来存储新对象到池里,它用会一个新的对象来覆盖旧的那个。

public class WeakPool<T> {
  private final WeakHashMap<T, WeakReference<T>> pool = new WeakHashMap<T, WeakReference<T>>();
  public T get(T object){
      final T res;
      WeakReference<T> ref = pool.get(object);
      if (ref != null) {
          res = ref.get();
      }else{
          res = null;
      }
      return res;
  }
  public void put(T object){
      pool.put(object, new WeakReference<T>(object));
  }
}



驻留池

问题最终的解决方案是一个驻留池,用上面的WeakPool就可以轻松实现它了。这个InternPool里面有一个WeakPool的实现,这个类只有一个synchronized的intern方法。

{% highlight java%}
public class InternPool<T> {
  private final WeakPool<T> pool = new WeakPool<T>();
  public synchronized T intern(T object) {
    T res = pool.get(object);
    if (res == null) {
        pool.put(object);
        res = object;
    }
    return res;
  }
}




这个方法会先尝试从池里获取对象,如果获取不到的话,再把这个对象存储进去,并返回它。如果已经有一个匹配的了,就返回池里那个匹配的对象。

多线程

这个方法标记成synchronized是为了确保判断和插入操作是一个原子操作。没有同步的话,如果两个线程同时检查两个相同的对象,都发现池里没有匹配的,它们会都执行插入操作。最后插入的那个会覆盖先插入的那个对象,而先完成的线程会认为自己拿到的对象是唯一的。同步解决了这个问题。

和GC的竞争冲突

尽管使用这个池的不同线程不会面临并发操作的问题,但是我们还是得看一下会不会和GC线程产生冲突。

当调用弱引用的get方法时有可能会返回null。这是有可能的,如果key对象已经被垃圾回收掉了,而WeakPool里面的WeakHashMap还没有删除这条记录的话。尽管每次WeakHashMap查询的时候都会检查这个key是否仍存在,但GC在这个get方法调用的过程中可能会介入。即便在map返回了引用的时候这个对象仍然存在,但由于这是个弱引用,在下次调用引用的get方法时,这个对象可能已经被GC给回收掉了。

这个情况下,WeakPool是会返回null的。不过没事,这对InternPool也不会产生影响。

如果你看一下我前面提到的stackoverflow里面一个讨论话题,你会发现有人是这么实现的:

public class InternPool<T> {
 
    private WeakHashMap<T, WeakReference<T>> pool =
        new WeakHashMap<T, WeakReference<T>>();
 
    public synchronized T intern(T object) {
        T res = null;
        // (The loop is needed to deal with race
        // conditions where the GC runs while we are
        // accessing the 'pool' map or the 'ref' object.)
        do {
            WeakReference<T> ref = pool.get(object);
            if (ref == null) {
                ref = new WeakReference<T>(object);
                pool.put(object, ref);
                res = object;
            } else {
                res = ref.get();
            }
        } while (res == null);
        return res;
    }
}



这段代码里作者用了个无限的循环来解决这个问题。虽然这样做并不是很好,但也能解决问题。这个循环不太可能无限执行下去,但不太可能也是有可能的。同时这个结构让人很难读懂,太复杂了。记住一点,单一职责原则。关注最简单的事情,把你的程序拆分成各个简单的组件。

总结

尽管Java只对字符串和一些基础类型做了驻留,但有时候你还是需要自己驻留一些别的对象。这种情况下,应用程序就得自己想办法解决了。这个时候,上面这两个类就派上用场了,你可以直接复制粘贴到你的代码里,也可以:

<dependency>
  <groupId>com.javax0</groupId>
  [url=http://it.deepinmind.com]intern</artifactId>
  <version>1.0.0</version>
</dependency>



导入上面的maven库。这个库非常小,只有上面那两个类而已,并且它是apache license的,放心用吧。库的源码在GitHub上可以下载到。


原创文章转载请注明出处:http://it.deepinmind.com

英文原文链接















4
1
分享到:
评论
3 楼 andrii 2014-05-26  
andrii 写道
String str1 = "str1";
String str2 = "str2";
System.out.println(str1 == str2)//false;

自己写错了!
2 楼 andrii 2014-05-26  
String str1 = "str1";
String str2 = "str2";
System.out.println(str1 == str2)//false;
1 楼 fair_jm 2014-04-16  
文章格式有点问题吧...挺不错的~

相关推荐

    Java对象在JVM中的生命周期详解

    "Java对象在JVM中的生命周期详解" Java对象在JVM中的生命周期是Java编程语言中一个非常重要的概念,它涉及到Java对象的创建、使用、释放和销毁整个过程。在JVM中,Java对象的生命周期可以分为七个阶段:创建阶段、...

    JAVA基础面试题(面向对象基础)

    【Java基础面试题(面向对象基础)】 在Java编程中,面向对象的特征主要包括四个核心概念:抽象、继承、封装和多态性。 1. 抽象:抽象是将复杂问题简化的过程,它关注对象的主要特征,忽略次要细节。抽象分为过程...

    Java入门:状态对象--数据库的替代者

    1. **性能提升**:由于状态对象驻留在内存中,避免了频繁的磁盘I/O操作,从而大大提高了数据访问速度。 2. **简化开发**:状态对象使得状态管理更加直观和简便,开发者可以更专注于业务逻辑而非底层的数据持久化细节...

    Android仿String的对象驻留示例分析

    对象驻留是指在内存中,如果有两个引用指向完全相同的值,它们实际上是同一个对象,而不是两个独立的副本。这对于节省内存和提高性能非常有用。在Java中,这种特性对于字符串是自动实现的,但对于自定义对象,我们则...

    JAVA知识:面向对象的特征,接口,集合类等

    【JAVA知识:面向对象的特征,接口,集合类等】 在JAVA编程中,面向对象的特征是核心概念,包括抽象、继承、封装和多态性。 1. 抽象:抽象是面向对象编程的基础,它关注的是对象的主要特征,忽略不重要的细节。...

    Java中的字符串驻留

    Java中的字符串驻留机制是为了优化字符串的使用,减少内存中不必要的重复对象。它涉及到Java的字符串常量池,这是一个特殊区域,存储了程序中所有的字符串字面量(literal strings)和通过`intern()`方法添加的字符...

    jca javacore分析工具

    - **排查与修复**:定位到问题后,分析代码逻辑,找出导致问题的原因,如无用对象未被正确释放,或者全局变量导致大量对象驻留等,并进行相应的代码修改。 - **验证结果**:修改代码后,重新生成javacore和heap...

    java-error-in-idea64主要是javaJVM的dump导出学习使用

    - **对象引用关系**:检查是否有循环引用或不必要的大对象长期驻留在内存中。 - **持久代使用情况**:如果涉及持久代(PermGen或Metaspace),确保没有过多的类装载导致内存溢出。 - **垃圾收集行为**:分析GC日志,...

    Java基础[Java基础]--Java GC工作原理

    短期存在的对象(如局部变量)很快会被销毁,而长期存在的对象则会长时间驻留在内存中。因此,对不同生命周期的对象采取不同的回收策略可以显著提升GC的效率。 **4.2 如何分代** Java虚拟机中的堆空间可以被划分为...

    java简单示例rmi

    Java Remote Method Invocation (RMI) 是Java平台上的一个特性,它允许在分布式环境中进行对象间的交互,使得一个Java对象能够调用另一个在网络另一端的Java对象的方法。这个技术是Java在分布式计算领域的重要应用,...

    JAVA的面向对象编程课堂笔记.doc

    面向对象编程是Java的核心特性,它包括抽象、继承、封装和多态这四个基本原则。 1. 抽象:抽象是面向对象编程中的一个重要概念,它指的是在设计阶段忽略与当前目标无关的细节,关注主要的功能点。在Java中,抽象...

    JavaRMI快速入门

    2. **远程对象**:远程对象是实现了远程接口的实例,它可以驻留在网络中的任何地方。Java RMI允许这些对象的引用在不同进程中传递,使得客户端可以调用远程对象的方法。 3. **注册表(Registry)**:RMI注册表是一...

    Java 2 实用教程(耿祥怡)课后答案.doc

    本资源提供了 Java 2 实用教程的课后答案,涵盖了 Java 基础知识点,包括 Java 开发和运行过程、应用程序和小应用程序的区别、Java 程序的组成、类和对象、标识符、关键字和数据类型等。 一、Java 开发和运行过程 ...

    Java RMI demo

    Java RMI(Remote Method Invocation,远程方法调用)是Java平台提供的一种分布式计算技术,它允许Java对象在不同的网络环境中进行交互,就像调用本地方法一样。RMI是Java在网络编程中的重要组成部分,尤其适用于...

    java rmi HelloWorld版(源码)

    4. ** stub 和 skeleton**: stub 是远程对象的代理,驻留在客户端,负责将客户端的调用转换为网络通信; skeleton 在服务器端,接收来自网络的请求并调用相应的远程方法。现代的Java RMI已经不再需要显式创建stub和...

    JAVA RMI

    远程对象可以驻留在网络上的任何地方,并通过网络调用其方法。 2. **远程接口(Remote Interface)**:定义了远程对象对外提供的服务,所有远程方法都声明在这个接口中。这个接口必须继承自java.rmi.Remote。 3. *...

    java的RMI

    Java的RMI(Remote Method Invocation)是Java平台中用于实现分布式计算的一种技术,它允许一个Java对象在不同的JVM(Java虚拟机)之间进行方法调用。RMI系统的核心概念包括远程接口、远程对象和Stub/Skeleton。在这...

    一个简单的RMI例子Java源代码,用于学习理解RMI

    它允许Java对象在不同的JVM之间进行交互,仿佛这些对象都在同一台机器上一样。这个"一个简单的RMI例子Java源代码"是为了帮助开发者更好地理解和应用RMI。 RMI的核心概念包括: 1. **远程接口(Remote Interface)*...

Global site tag (gtag.js) - Google Analytics