精华内容
下载资源
问答
  • 什么是Java的对象引用? Java中都有哪些类型的对象引用? Java中提供的Java对象引用主要有什么目的? 通过本文,你就能很清楚得了解Java的对象引用

    Java对象的引用

    一、概念,什么是Java对象的引用?
      每种编程语言都有自己的数据处理方式。有些时候,程序员必须注意将要处理的数据是什么类型。你是直接操纵元素,还是用某种基于特殊语法的间接表示(例如C/C++里的指针)来操作对象。所有这些在 Java 里都得到了简化,一切都被视为对象。因此,我们可采用一种统一的语法。尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(reference)。 对Java对象的引用,是描述的定义。 
      
    二、Java对象引用的目的
    Java中提供这四种引用类型主要有两个目的:
    第一是 可以让程序员通过代码的方式决定某些对象的生命周期
    第二是 有利于JVM进行垃圾回收

    三、四中Java对象的引用
    Java对象的引用包括:强引用,软引用,弱引用,虚引用
    强引用:是指创建一个对象并把这个对象赋给一个引用变量。
    软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
    如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被 程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
    SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
    弱引用:WeakReference弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
    虚引用:虚引用(PhantomReference) 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收 。

    要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

    展开全文
  • Java引用对象

    千次阅读 2018-12-11 10:09:03
    在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我...

    简介

    在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就爱上它了。在我不需要再管理内存后我才意识到之前耗费了多少精力。

    接着我就遇到了第一个OutOfMemoryError。当时我就坐在那面对着控制台,没有堆栈,因为堆栈也需要内存。调试这个错误很困难,因为常用的工具都不能用了,甚至malloc logger都没有,而且1999年的时候Java的调试器还很原始。

    我不记得当时是什么原因导致的错误了,但我肯定当时没有用引用对象解决它。引用对象是在一年后我写服务端数据库缓存,尝试用软引用来限制缓存大小时才进入我的“工具箱”的。结果证明它们在这种场景下用处不大,我下面会解释原因。但当引用类型才进入我的“工具箱”后,我发现了很多其他用途,并且对JVM也有了更好的理解。

    Java堆和对象生命周期

    对于刚接触Java的C++程序员来说,栈和队之间的关系很难理解。在C++中,对象可以通过new操作在堆上创建,也可以通过“自动”分配在栈上创建。下面这种在C++中是合法的,会在栈上创建一个新的Integer对象,但对于Java编译器来说这有语法错误。

    1
    
    Integer foo = Integer(1);
    

    不同于C++,Java的所有对象都在堆保存,要求用new操作来创建对象。局部变量仍然储存在栈中,但它们持有这个对象的指针而不是这个对象本身(更让C++程序员困惑的是这些指针被叫做“引用”)。下面这个Java方法,有一个Integer变量引用一个从String解析而来的值:

    1
    2
    3
    4
    
    public static void foo(String bar)
    {
        Integer baz = new Integer(bar);
    }
    

    下图显示了这个方法相应的堆和栈之间的关系。栈被分割为栈帧,用于保存调用树中各个方法的参数和局部变量。这些变量指向对象–这个例子中的参数bar和局部变量baz–指23向存在于堆中的变量。

    现在仔细看看foo()的第一行,创建了一个Integer对象。这种情况下,JVM会先试图去为这个对象找足够的堆空间–在32位JVM上大约12 bytes,如果可以分配出空间,就调用Integer的构造函数,Integer的构造函数会解析传入的String然后初始化这个新创建的对象。最后,JVM在变量baz中保存一个指向该对象的指针。

    这是理想的道路,还有一些不那么美好的道路,其中我们关心的是当new操作不能为这个对象找到12 bytes的情况。在这种情况下,JVM会在放弃并抛出OutOfMemoryError之前调用垃圾回收器尝试腾出空间。

    垃圾回收

    虽然Java给了你new操作来在堆上分配对象,但是没有给你对应的delete操作来移除它们。当方法foo()返回,变量baz离开了作用域,但是它指向的对象依然存在于堆中。如果只是这样的话,那所有的程序都会很快耗尽内存。Java提供了垃圾回收器来清理那些不再被引用的对象。

    垃圾回收器会在程序尝试创建一个新对象但堆没有足够的空间时工作。回收器在堆上寻找那些不再被程序使用的对象并回收它们的空间时,请求创建对象的线程会暂停。如果回收器无法腾出足够的空间,并且JVM无法扩展堆,new操作就会失败并抛出OutOfMemoryError,通常接下来你的应用会停止。

    标记-清除

    其中一个关于垃圾回收器的误区是,很多人认为JVM为每个对象保存了一个引用计数,回收器只会回收那些引用计数为0的对象。事实上,JVM使用被称为“标记-清除”的技术。标记-清除算法的思路很简单:所有不能被程序访问到的对象都是垃圾,都可以被收集。

    标记-清除算法有以下阶段:

    阶段一:标记

    垃圾回收器从“root”引用开始,标记所有可以到达的对象。

    阶段二:清除

    在第一阶段没有被标记的都是不可到达的,也就是垃圾。如果垃圾对象定义了finalizer,它会被加到finalization队列(后文详细讨论)。否则,它占用的空间就可以被重新分配使用(具体的情况视GC的实现而定,有很多种实现)。

    阶段三:压缩(可选)

    一些回收器有第三步——压缩。在这一步,GC会移动对象使回收的对象留下的空闲空间合并,这可以防止堆变得碎片化,避免大块相邻内存分配的失败。

    例如,Hotspot JVM,在新生代使用会压缩的回收器,而在老年代使用非压缩的回收器(至少在1.6和1.7的“server” JVM是这样)。想了解更多信息,可以看本文后面的参考文献。

    那么什么是“roots”呢?在一个简单的Java应用中,它们是方法参数和局部变量(保存在栈中)、当前执行的表达式操作的对象(也保存在栈中)、静态类成员变量。

    对于使用自己classloader的程序,例如应用服务器,情况复杂一些:只有被system classloader(JVM启动时使用这个loader)加载的类包含root引用。那些被应用创建的classloader一旦没有其他引用也会被回收。这是应用服务器可以热部署的原因:它们为每个部署的应用创建独立的classloader,当应用下线或重新部署时释放classloader引用。

    理解root引用很重要,因为这定义了“强引用”,即如果可以从root沿着引用链到达某个对象,那么这个对象就被“强引用”了,则不会被回收。

    回到foo()方法,参数bar和局部变量baz使用当方法执行时才是强引用,一旦方法结束,它们都超出了作用域,被他们引用的对象就可以回收。另一种可能是,foo()返回一个它创建的Integer引用,这意味着这个对象会被调用foo()的那个方法保持强引用。

    看下面这个例子:

    1
    2
    
    LinkedList foo = new LinkedList();
    foo.add(new Integer(123));
    

    变量foo是一个指向LinkedList对象的root引用,列表中有0个或多个元素,都指向其对象。当我们调用add()时,向列表中添加了一个指向值为123的Integer实例的元素,这是一个强引用,意味着这个Integer实例不会被回收。一旦foo超出了作用域,这个LinkedList和它里面的一切都可以被回收,当前前提是没有其他强引用指向它了。

    你也许想知道循环引用会发生什么,即对象A包含一个对象B的引用,同时对象B也包含对象A的引用。答案是标记-清除回收器并不傻,如果A和B都无法由强引用链到达,那么它们都可以被回收。

    Finalizers

    C++允许对象定义析构方法,当对象离开作用域或者被明确删除时,它的析构函数会被调用来清理它使用的资源,对大多数对象来说即释放通过newmalloc分配的内存。在Java中,垃圾回收器会为你处理内存清理,所以不需要明确的析构函数来做这些。

    然而,内存并不是唯一可能需要被清理的资源。例如FileOutputStream,当创建这个对象的实例时,会从操作系统分配一个文件操作符(文件句柄),如果你在关闭流之前让它的所有引用都离开作用域了,这个文件操作符会发生什么呢?答案是流有finalizer,这个方法会在垃圾回收器回收对象前被JVM调用。这个例子中的FileOutputStreamfinalizer方法中会关闭流,这样就会将文件操作符返回给操作系统,同时也会刷新缓存,确保所有数据被正确地写到磁盘。

    任何对象都可以有finalizer,你只需要定义finalize()方法即可:

    1
    2
    3
    4
    
    protected void finalize() throws Throwable
    {
        // 在这里释放你的对象
    }
    

    finalizers看上去是一个由你自己清理的简单方式,但实际上有严重的限制。首先,你永远也不要依赖它做重要的事,因为对象的finalizers可能不会被调用,应用可能在对象被回收之前就结束了。finalizers还有一些更微妙的问题,我会在虚引用时讨论。

    对象的生命周期(无引用对象)

    总结起来,对象的一生可以用下面的图总结:被创建、被使用、可回收、最终被回收。阴影部分表示对象是“强可达”的时期,这是与引用对象规定的可达性比较而言很重要的时期。

    进入引用对象的世界

    JDK 1.2引入了java.lang.ref包,对象的生命周期增加了3种阶段:软可达、弱可达、虚可达。这些阶段只用来可否被回收,换言之,那些不是强引用的对象,必须是其中一种引用对象的被引用者:

    • 软可达
      对象是SoftReference的被引用者,并且没有强引用指向它。垃圾回收器会尽可能地保留它,但会在抛出OutOfMemoryError之前回收它。

    • 弱可达
      对象是WeakReference的被引用者,并且没有强引用指向它。垃圾回收器可以在任何时间回收它,不会试图去保留它。通常这个对象会在Major GC被回收,可能在Minor GC中存活。

    • 虚可达
      对象是PhantomReference的被引用者,它已经被选择要回收并且finalizer(如果有)已经运行了。这里的“可达”有点用词不当,在这个时候你已经没有办法访问到原始的对象了。

    如你所想,把这三种新的可选状态加到对象生命周期图中会变得很复杂。尽管文档指出了一个逻辑上从强可达到软可达、弱可达、虚可达的回收过程,但实际过程取决于你的程序创建了哪种引用对象。如果你创建了一个WeakReference而不是一个SoftReference,那么对象回收的过程是直接从强可达到弱可达最后被回收的。

    还有一点需要清楚的是,不是所有的对象都需要与引用对象关联,事实上,只有极少部分对象需要。引用对象是一个间接层:你通过引用对象去访问它的被引用者,你肯定不希望你的代码中充斥着这些间接层。事实上大部分程序只会使用引用对象去访问很少一部分它创建的对象。

    引用和被引用者

    引用对象是在你程序代码和一些称为被引用者的对象之间的中间层。每个引用对象都是围绕它的被引用者创建,并且被引用者是不能修改的。

    引用对象提供了get()方法来获取被引用者的强引用。垃圾回收器可能在某些情况下回收被引用者,一旦回收了,get()会返回null。正确使用引用,你需要类似下面这样的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());
    
    // 代码其他地方创建了`Foo`,你想把它添加到列表中
    List<Foo> list = ref.get();
    if (list != null)
    {
        list.add(foo);
    }
    else
    {
        // 列表已经被回收了,做一些恰当的事
    }
    

    换言之:

    1. 你必须总是检查看被引用者是否是null。
      垃圾回收器可能在任何时间回收被引用者,如果你无所顾忌地使用,很快就会收获NullPointerException
    2. 当你想使用被引用者时,你必须持有一个它的强引用。
      再次强调, 垃圾回收器可能在任何时间回收被引用者,甚至是在单个表达式之间。上面的例子如果我不定义list变量,我而是简单地调用ref.get().add(foo),被引用者可能在检查是否为null和实际使用之间被回收。牢记垃圾回收器是在它自己的线程运行的,它不关心你的代码在干什么。
    3. 你必须持有引用类型的强引用。
      如果你创建了一个引用对象,但超出了它的作用域,那么这个引用对象自己也会被回收。这是显然的,但很容易被忘记,特别是在用引用队列(qv)追踪引用对象的时候。

    同样要记住的是软引用、弱引用、虚引用只有在没有其他强引用指向被引用者时才有意义,它们让你可以在对象通常会成为垃圾回收器的食物时候获得该对象。这可能看起来很奇怪,如果不再持有强引用了,为什么我还关心这个对象呢?原因视特殊的引用类型而定。

    软引用

    我们先从软引用开始来回答这个问题。如果一个对象是SoftReference的被引用者,并且它没有强引用,那么垃圾回收器可以回收但尽量不去回收它。因此,只要JVM有足够的内存,软引用对象就会在垃圾回收中存活,甚至经历好几轮垃圾回收依然存活。

    JDK文档说软引用适用于内存敏感的缓存:每个缓存对象都通过SoftReference访问,如果JVM觉得需要内存,它就会清除一些或者所有引用并回收对应的被引用者。如果JVM不需要内存,被引用者就会留在堆中,并且可以被程序代码访问到。在这种方案下,被引用者在使用时是强引用的,其他情况是软引用的,如果软引用被清除了,你需要刷新缓存。

    想作为这种角色使用,被缓存的对象需要比较大,如每个几kB。比如说,你想实现一个文件服务相同的文件会被定期检索,或者有一些大的图片对象需要缓存时会有用。但如果你的对象很小,你只有在需要定义大量对象时情况才会不同,引用对象还会增加整个程序的负担。

    内存限制型缓存被认为是有害的
    我的观点是,可用内存绝对是最差的管理缓存的方式。如果你的堆很小,你不时需要重新加载对象,无论它们是不是被活跃地使用,你也无法知道这个,因为缓存会静默地处理它们。大的堆更糟:你会持有对象远大于它的正常寿命,当每次垃圾回收时会使你的应用变慢,因为需要检查这些对象。如果这些对象没有被访问,这一部分堆有可能被交换出去,回收过程中可能有大量页错误。
    底线:如果你要用缓存,详细它会如何被使用,选一个适合的缓存策略(LRU、timed LRU),在选择基于内存的策略前仔细考虑。

    软引用用于断路器

    用软引用为内存分配提供断路器是更好的选择:在你的代码和它分配的内存之间使用软引用,你就可以避免可怕的OutOfMemoryError。这个技巧可以正常运作是因为在应用里内存的分配是趋于局部的:从数据库中读取行、从一个文件中处理数据。

    例如,如果你写过很多JDBC的代码,你可能会有类似下面这样的方法以某种方式处理查询的结果并且确保ResultSet被正确地关闭。这只有一个小缺陷:如果查询返回了一百万行,你没有可用的内存去保存它们时会发生什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public static List<List<Object>> processResults(ResultSet rslt)
    throws SQLException
    {
        try
        {
            List<List<Object>> results = new LinkedList<List<Object>>();
            ResultSetMetaData meta = rslt.getMetaData();
            int colCount = meta.getColumnCount();
    
            while (rslt.next())
            {
                List<Object> row = new ArrayList<Object>(colCount);
                for (int ii = 1 ; ii <= colCount ; ii++)
                    row.add(rslt.getObject(ii));
    
                results.add(row);
            }
    
            return results;
        }
        finally
        {
            closeQuietly(rslt);
        }
    }
    

    答案当然是会得到OutOfMemoryError。这是使用断路器的绝佳地方:如果在处理查询时JVM要耗尽内存了,那就释放所有已经使用的那些内存,抛出一个应用特殊的异常。

    你可能很奇怪,这种情况下这次查询将被忽略,为什么不直接让内存耗尽的错误来做这件事呢?原因是并不仅仅只有你的应用被内存耗尽影响。如果你在一个应用服务器上运行,你的内存使用可能干掉其他应用。即使是在一个独有的环境,断路器也能提升你的应用的健壮性,因为它能限制问题,让你有机会恢复并继续运行。

    要创建一个断路器,首先你需要做的是把结果的列表包装在SoftReference中(你在前面已经见过这个代码了):

    1
    2
    
    SoftReference<List<List<Object>>> ref
            = new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>());
    

    然后,你遍历结果,在你需要更新这个列表时为它创建强引用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    while (rslt.next())
    {
        rowCount++;
        // store the row data
    
        List<List<Object>> results = ref.get();
        if (results == null)
            throw new TooManyResultsException(rowCount);
        else
            results.add(row);
    
        results = null;
    }
    

    这可以满足要求是因为这个方法几乎所有的内存分配都发生在2个地方:调用next()时和代码把行里的数据存放到它自己的列表中时。第一种情况当你调用next()时会发生很多事情:ResultSet一般会在包含多行的一大块二进制数据中检索,然后当你调用getObject(),它会取出一部分数据把它转成Java对象。

    当这些昂贵的操作发生时,这个list只有来自SoftReference的引用,如果内存耗尽,引用会被清除,list会变成垃圾。这意味着这个方法可能抛出异常,但抛出异常的影响是有限的,也许调用方能以一点数量限制重新进行查询。

    一旦昂贵的操作完成,你可以没有影响地拿到list的强引用。注意到我用LinkedList保存结果而不是ArrayListLinkedList增长时只会增加少量字节,不太可能引起OutOfMemoryError,而如果ArrayList需要增加容量,它需要创建一个新数组,对于大列表来说,这可能意味着数MB的内存分配。

    还注意到我在添加新元素后把results变量设置为null,这是少数几种这样做是合理的情形之一。尽管在循环的最后变量超出了作用域,但垃圾回收器可能并不知道(因为JVM没有理由去清除变量在调用栈中的位置)。因此如果我不清除这个变量的话,它会在随后的循环中成为隐藏的强引用。

    软引用不是万能的

    软引用可以预防很多内存耗尽的情况,但不能预防所有。问题在于:为了真正地使用软引用,你需要创建一个被引用者的强引用,即为了向results中添加一行,我们需要持有实际列表的引用。我们持有强引用的时候就会面临发生内存耗尽错误的风险。

    使用断路器的目标是把一些无用的东西的时间窗口减到最小:你持有对象强引用的时间,更重要的是在这段时间中分配内存的总量。在我们的例子中,我们限制强引用去添加一行到results中,我们使用LinkedList而不是ArrayList因为前者扩容时增长更小。

    我想重申的是,如果我一个变量持有强引用,但这个变量很快超出了作用域,语言细则没有说JVM需要清除超出作用域的变量,如果是像写的这样,Oracle/OpenJDK JVM都没有这样做,如果我不明确地清除results变量,在遍历期间会保持强引用,阻止软引用做它的工作。

    最后,仔细考虑那些隐藏的强引用。例如,你可能会想在使用DOM构造XML文档时加入断路器。在DOM中,每个节点都持有它父节点的引用,从而导致持有了树中每个其他节点的引用。如果你用递归去创建文档,你的栈中可能塞满了个别节点的引用。

    弱引用

    弱引用,正如它名字显示,是一个当垃圾回收器来敲门时不会反抗的引用对象。如果被引用者没有强引用或软引用而只有弱引用,那它就可以被回收。所以弱引用有什么用呢?有2个主要用途:关联没有内在联系的对象,或者通过canonicalizing map减少重复。

    ObjectOutputStream的问题

    第一个例子,我准备聚焦不使用弱引用的对象序列化。ObjectOutputStream以及它的伙伴ObjectInputStream提供了任意Java对象与字节流之间相互转换的方式。根据对象模型的观点,流和用这些流写的对象之间是没有联系的。流不是由这些被写的对象组成的,也不是它们的聚集。

    但是当你看这些流的说明时,你会看到事实上是有联系的:为了维持对象的唯一性,输出流会和每个被写的对象关联一个唯一的标识符,随后的写对象的请求被替换为写这个标识符。这个特征对于流序列号对象的能力来说绝对是很重要的,如果没有这个特征,自我引用的对象会变成一个无限的字节流。

    要实现这个特征,流需要持有每个写到流中的对象的强引用。对于决定在socket通信时用对象流作为消息协议的程序员来说,有这么一个问题:消息被设计为短暂的,但流会在内存中持有它们,不久之后,程序会耗尽内存(除非程序员知道在每次通信后调用reset())。

    这种非与生俱来的联系惊人的普遍。它们会在程序员为了使用对象而需要去维持必不可少的上下文时出现。有时这些联系被运行环境默默管理,例如servlet Session对象;有时这些联系需要被程序员明确地管理,例如对象流;还有些时候,这种联系只有当生产环境的服务抛出内存耗尽的错误时才会被发现,比如埋藏在程序代码深处的静态Map

    弱引用提供了一种维持这种联系的同时还能让垃圾回收器做它的工作的方式,弱引用只有在同时还有强引用时才保持有效。回到对象流的例子,如果你用流来通信,一旦消息被写完就可以被回收了。另一方面,当流用来RMI访问一个生命周期很长的数据结构时,它能保持它一致。

    不幸的是,尽管对象流通信协议在JDK 1.2时被更新了。虚引用也是这样被加入的,但JDK的开发者并没有选择把二者结合到一起,所以记得调用reset()

    Canonicalizing Maps消除重复数据

    尽管存在对象流这种情况,但我不认为有很多你应该关联两个没有内在关系的对象的情行。我所看到的一些例子,例如Swing监听器,它们会自我清理,看起来更像是黑客,而不是有效的设计选择。

    当我最初写这篇文章的时候,大约是在2007年,我提出了canonicalizing map作为String.intern()的替代物,是在假设被存入常量池的字符串永远不会被清理的前提下。后来我得知这种担心是毫无根据的。更重要的是,从JDK 8开始,OpenJDK已经完全去掉了永久代。因此,没有必要害怕intern(),但是canonicalizing map对于字符串以外的对象仍然有用。

    在我看来,弱引用的最佳用途是实现canonicalizing map,这是一种确保同时只存在一个值对象实例的办法。String.intern()是这种map的典型例子:当你把一个字符串存入常量池时,JVM会将它添加到一个特殊的map中,这个map也用于保存字符串文本。这样做的原因不是像一些人认为的那样为了更快地进行比较。这是为了最大限度地减少重复的非文字字符串(如从文件或消息队列中读取的字符串)占用的内存量。

    简单的canonicalizing map通过使用相同的对象作为key和value来工作:你用任意实例传给map,如果map中已经有一个值,你就返回它。如果map中没有值,则存储传入的实例(并返回它)。当然,这仅适用于可用作map的key的对象。如果我们不担心内存泄漏,下面可能是我们实现String.intern()的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private Map<String,String> _map = new HashMap<String,String>();
    
    public synchronized String intern(String str)
    {
        if (_map.containsKey(str))
            return _map.get(str);
        _map.put(str, str);
        return str;
    }
    

    如果你只有少量字符串要放入常量池,例如也许在处理一个文件的简单方法中,这个实现没什么问题。然而,假设你正在编写一个长期运行的应用程序,该应用程序必须处理来自多个来源的输入,其中包含范围广泛的字符串,但仍有高度的重复。例如,一台处理上传的邮政地址数据文件的服务:New York将会有很多条目,Temperanceville VA的条目就不多了。你会想要消除前者的重复,但是不想保留后者超过必要的时间。

    这就是弱引用的canonicalizing map有所帮助的地方:只有程序中的一些代码正在使用它,它才允许你创建一个规范的实例。最后一个强引用消失后,这个规范的字符串将被回收。如果稍后再次出现该字符串,它将成为新的规范的实例。

    为了改进我们的“规范化工具”,我们可以用WeakHashMap替换HashMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    private Map<String,WeakReference<String>> _map
        = new WeakHashMap<String,WeakReference<String>>();
    
    public synchronized String intern(String str)
    {
        WeakReference<String> ref = _map.get(str);
        String s2 = (ref != null) ? ref.get() : null;
        if (s2 != null)
            return s2;
    
        _map.put(str, new WeakReference(str));
        return str;
    }
    

    首先要注意的是,虽然map的key是字符串,但它的值是WeakReference<String>。这是因为WeakHashMap对其key使用弱引用,但对其value持有强引用。因为我们的key和value是相同的,所以entry永远不会被回收。通过包装条目,我们让GC回收它。

    其次,注意返回字符串的过程:首先我们检索弱引用,如果它存在,那么我们检索引用对象。但是我们也必须检查那个对象。存在引用仍在map中但已经被清除了的可能。只有当引用对象不为空时,我们才返回它;否则,我们认为传入的字符串是新的规范的版本。

    第三,请注意我对intern()方法用了synchronizedcanonicalizing map最有可能的用途是在多线程环境中,例如应用服务,WeakHashMap没有内部同步。这个例子中的同步实际上相当幼稚,intern()方法可能成为争论的焦点。在现实世界的实现中,我可能会使用ConcurrentHashMap,但是对于教程来说,这种幼稚的方法更有效。

    最后,WeakHashMap的文档关于条目何时从map中移除有些模糊。它指出,“WeakHashMap的行为可能就像一个未知线程正在无声地删除条目。”实际上没有其他线程。相反,每当map被访问时,它就会被清理。为了跟踪哪些条目不再有效,它使用了引用队列。

    引用队列

    虽然判断一个引用是不是null可以让你知道它的引用对象是不是已经被回收,但是这样做并不是很高效;如果你有很多引用,你的程序会花大部分时间寻找那些已经被清除的引用。

    更好的解决方案是引用队列:你在构建时将引用与队列相关联,并且该引用将在被清除后放入队列中。要发现哪些引用已被清除,你需要从队列拉取。这可以通过后台线程来完成,但是在创建新引用时从队列拉取通常更简单(WeakHashMap就是这样做的)。

    引用队列最常与虚引用一起使用,在后面会描述,但是可以与任何引用类型一起使用。下面的代码是一个弱引用的例子:它创建了一组缓冲区,通过WeakReference访问,并且在每次创建后查看哪些引用已经被清除。如果运行此代码,你会看到create消息的长时间出现,当垃圾回收器运行时偶尔会出现一些clear消息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public static void main(String[] argv) throws Exception
    {
        Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
        
        for (int ii = 0 ; ii < 1000 ; ii++)
        {
            WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue);
            System.err.println(ii + ": created " + ref);
            refs.add(ref);
            
            Reference<? extends byte[]> r2;
            while ((r2 = queue.poll()) != null)
            {
                System.err.println("cleared " + r2);
                refs.remove(r2);
            }
        }
    }
    

    一如既往,关于这个代码有一些值得注意的事情。首先,虽然我们创建的是WeakReference实例,但是队列会给我们返回Reference。这提醒你,一旦它们入队,你使用的是什么类型的引用就不再重要了,被引用者已经被清除。

    第二,我们必须对引用对象本身进行强引用。引用对象知道队列,但是队列在引用进入队列前不知道引用。如果我们没有维护对引用对象的强引用,它本身就会被回收,并且永远不会被添加到队列中。在这个例子中,我使用了一个Set,一旦引用被清除,就删除它们(将它们留在Set中是内存泄漏)。

    虚引用

    虚引用不同于软引用和弱引用,它们不用于访问它们的被引用者。相反,他们的唯一目的是当它们的被引用者已经被回收时通知你。虽然这看起来毫无意义,但它实际上允许你比finalizers更灵活地执行资源清理。

    Finalizers的问题

    这篇文章中我更详细地讨论了finalizers。简而言之,你应该依靠try/catch/finally清理资源,而不是finalizers或虚引用。

    在对象生命周期的描述中,我提到finalizers有一些微妙的问题,使得它们不适合清理非内存资源。还有一些非微妙的问题,为了完整起见,我将在这里讨论。

    • finalizer可能永远不会被调用

      如果你的程序从未用完可用内存,那么垃圾回收器不会运行,你的finalizer也不会运行。对于长时间运行的应用程序(例如服务)来说,通常不会出现这个问题,但是短时间运行的程序可能会在没有运行垃圾收集的情况下完成。虽然有一种方法可以告诉JVM在程序退出之前运行finalizers,但这是不可靠的,可能会与其他shutdown hooks冲突。

    • Finalizers可能创建一个对象的其他强引用

      例如,通过将对象添加到集合中。这基本上复活了这个对象,但是,就像Stephen King's Pet Sematary一样,返回的对象“不太正确”。尤其是,当对象再次符合回收条件时,它的finalizer不会运行。也许你会使用这种复活技巧是有原因的,但是我无法想象,而且在代码上看起来会非常模糊。

    现在这些都已经过时了,我相信finalizers的真正问题是它们在垃圾回收器首次识别要回收的对象的时间和实际回收其内存的时间之间引入了间隙,因为finalization发生在它自己的线程上,独立于垃圾回收器的线程。JVM保证在返回OutMemoryError之前执行一次full collection,但是如果所有符合回收条件的对象都有finalizers,则回收将不起作用:这些对象保留在内存中等待finalization。假设一个标准JVM只有一个线程来处理所有对象的finalization,一些长时间运行的finalization,你就可以看到问题可能会出现。

    以下程序演示了这种行为:每个对象都有一个finalizer休眠半秒钟。不会有很长时间,除非你有成千上万的对象要清理。每个对象在创建后都会立即超出作用域,但是在某个时候你会耗尽内存(如果你想运行这个例子,我建议使用-Xmx64m来使错误快速发生;在我的开发机器上,有3Gb堆,实际上需要几分钟才能失败)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class SlowFinalizer
    {
        public static void main(String[] argv) throws Exception
        {
            while (true)
            {
                Object foo = new SlowFinalizer();
            }
        }
    
        // some member variables to take up space -- approx 200 bytes
        double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
    
        // and the finalizer, which does nothing by take time
        protected void finalize() throws Throwable
        {
            try { Thread.sleep(500L); }
            catch (InterruptedException ignored) {}
            super.finalize();
        }
    }
    

    虚引用知晓之事

    当对象不再被使用时虚引用允许应用程序知晓,这样应用程序就可以清理对象的非内存资源。然而,与finalizers不同的是,当应用程序知道到这一点时,对象本身已经被收集了。

    此外,与finalizers不同,清理由应用程序而不是垃圾回收器来调度。您可以将一个或多个线程专用于清理,如果对象数量需要,可以增加线程数量。另一种方法——通常更简单——是使用对象工厂,并在创建新实例之前清理所有回收的实例。

    理解虚引用的关键点是,你不能使用引用来访问对象: get()总是返回null,即使对象仍然是强可达的。这意味着引用对象持有的不能是要清理的资源的唯一引用。相反,你必须维持对这些资源的至少一个其他强引用,并使用引用队列来通知被引用者已被回收。与其他引用类型一样,您的程序也必须拥有对引用对象本身的强引用,否则它将被回收,资源将内存泄露。

    用虚引用实现连接池

    数据库连接是任何应用程序中最宝贵的资源之一:它们需要时间来建立,并且数据库服务器严格限制它们将接受的同时打开的连接的数量。尽管如此,程序员对它们非常粗心,有时会为每个查询打开一个新的连接,或者忘记关闭它,或者不在finally块中关闭它。

    大多数应用服务部署使用连接池,而不是允许应用直接连接数据库:连接池维护一组打开的连接(通常是固定的),并根据需要将它们交给程序。用于生产环境的连接池提供了几种防止连接泄漏的方法,包括超时(识别运行时间过长的查询)和恢复被垃圾回收器回收的连接。

    下面这个连接池旨在演示虚引用,不能用于生产环境。Java有几个可用于生产环境的连接池,如Apache Commons DBCPC3P0

    后一个特性是虚引用的一个很好的例子。为了使它工作,连接池提供的Connection对象只是实际数据库连接的包装,可以在不丢失数据库连接的情况下回收它们,因为连接池保持对实际连接的强引用。连接池将虚引用与“包装成的”连接相关联,如果引用最终出现在引用队列中,则会将实际连接返回给连接池。

    连接池中最不有趣的部分是PooledConnection,如下所示。正如我说过的,它是一个包装,委派对实际连接的调用。不同的是我使用了反射代理来实现。JDBC接口随着Java的每一个版本而发展,其方式既不向前也不向后兼容;如果我使用了具体的实现,除非你使用了与我相同的JDK版本,否则你将无法编译。反射代理解决了这个问题,也使代码变得更短。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    public class PooledConnection
    implements InvocationHandler
    {
        private ConnectionPool _pool;
        private Connection _cxt;
    
        public PooledConnection(ConnectionPool pool, Connection cxt)
        {
            _pool = pool;
            _cxt = cxt;
        }
    
        private Connection getConnection()
        {
            try
            {
                if ((_cxt == null) || _cxt.isClosed())
                    throw new RuntimeException("Connection is closed");
            }
            catch (SQLException ex)
            {
                throw new RuntimeException("unable to determine if underlying connection is open", ex);
            }
    
            return _cxt;
        }
    
        public static Connection newInstance(ConnectionPool pool, Connection cxt)
        {
            return (Connection)Proxy.newProxyInstance(
                       PooledConnection.class.getClassLoader(),
                       new Class[] { Connection.class },
                       new PooledConnection(pool, cxt));
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable
        {
            // if calling close() or isClosed(), invoke our implementation
            // otherwise, invoke the passed method on the delegate
        }
    
        private void close() throws SQLException
        {
            if (_cxt != null)
            {
                _pool.releaseConnection(_cxt);
                _cxt = null;
            }
        }
    
        private boolean isClosed() throws SQLException
        {
            return (_cxt == null) || (_cxt.isClosed());
        }
    }
    

    需要注意的最重要的一点是,PooledConnection同时引用了底层数据库连接和连接池。后者用于那些确实记得关闭连接的应用程序:我们希望立即告知连接池,以便底层连接可以立即被重用。

    getConnection()方法也值得一提:它的存在是为了捕捉那些在显式关闭连接后试图使用该连接的应用程序。如果连接已经交给另一个消费者,这可能是一件非常糟糕的事情。因此close()显式清除引用,getConnection()会检查该引用,并在连接不再有效时抛出异常。invocation handler用于所有委托调用。

    现在让我们将注意力转向连接池本身,从它用来管理连接的对象开始。

    1
    2
    3
    4
    5
    6
    
    private Queue<Connection> _pool = new LinkedList<Connection>();
    
    private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>();
    
    private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>();
    private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();
    

    当连接池被构造并存储在_pool中时,可用连接被初始化。我们使用引用队列_refQueue来标识已回收的连接。最后,我们有连接和引用之间的双向映射,在将连接返回到连接池时使用。

    正如我之前说过的,实际的数据库连接将在提交给应用程序代码之前被包装在PooledConnection中。这发生在wrapConnection()函数中,也是我们创建虚引用和连接-引用映射的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private synchronized Connection wrapConnection(Connection cxt)
    {
        Connection wrapped = PooledConnection.newInstance(this, cxt);
        PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue);
        _cxt2Ref.put(cxt, ref);
        _ref2Cxt.put(ref, cxt);
        System.err.println("Acquired connection " + cxt );
        return wrapped;
    }
    

    wrapConnection对应的是releaseConnection(),该函数有两种变体。当应用程序代码显式关闭连接时,PooledConnection调用第一个,这是“快乐的道路”,它将连接放回连接池中供以后使用。它还会清除连接和引用之间的映射,因为它们不再需要。请注意,此方法具有默认(包)同步:它由PooledConnection调用,因此不能是私有的,但通常不可访问。

    1
    2
    3
    4
    5
    6
    7
    
    synchronized void releaseConnection(Connection cxt)
    {
        Object ref = _cxt2Ref.remove(cxt);
        _ref2Cxt.remove(ref);
        _pool.offer(cxt);
        System.err.println("Released connection " + cxt);
    }
    

    另一个变体使用虚引用来调用,这是“可悲的道路”,当应用程序不记得关闭连接时才会调用。在这种情况下,我们得到的只是虚引用,我们需要使用映射来检索实际连接(然后使用第一个变体将其返回到连接池中)。

    1
    2
    3
    4
    5
    6
    
    private synchronized void releaseConnection(Reference<?> ref)
    {
        Connection cxt = _ref2Cxt.remove(ref);
        if (cxt != null)
            releaseConnection(cxt);
    }
    

    有一种边缘情况:如果引用在应用程序调用close()之后进入队列,会发生什么?这种情况不太可能发生:当我们清除映射时,虚引用应该已经有资格被回收,这样它就不会进入队列。然而,我们必须考虑这种情况,这导致上面的空检查:如果映射已经被移除,那么连接已经被显式返回,我们不需要做任何事情。

    好了,您已经看到了底层代码,现在是时候让应用程序调用唯一的方法了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public Connection getConnection()
    throws SQLException
    {
        while (true)
        {
            synchronized (this) 
            {
                if (_pool.size() > 0)
                    return wrapConnection(_pool.remove());
            }    
    
            tryWaitingForGarbageCollector();
        }
    }
    

    getConnection()的最佳路径是在_pool中有可用的连接。在这种情况下,一个连接被移除、包装并返回给调用者。不信的情况是没有任何连接,在这种情况下,调用者希望我们阻塞直到有一个连接可用。这可以通过两种方式发生:要么应用程序关闭连接并返回到_pool中,要么垃圾回收器找到一个已被放弃的连接,并将其关联的虚引用加入队列。

    为什么我使用synchronized(this)而不是显式锁?简而言之,这个实现是作为教学辅助工具,我想用最少的样板来强调同步点。在生产环境使用的连接池中,我实际上会避免显式同步,而是依赖并行数据结构,如ArrayBlockingQueueConcurrentHashMap

    在走这条路之前,我想谈谈同步。显然,对内部数据结构的所有访问都必须同步,因为多个线程可能会尝试同时获取或返回连接。只要_pool中有连接,同步代码就能快速执行,竞争的可能性就很低。然而,如果我们必须循环直到连接变得可用,我们希望最大限度地减少同步的时间:我们不希望在请求连接的调用者和返回连接的另一个调用者之间造成死锁。因此,在检查连接时,使用显式同步块。

    那么,如果我们调用getConnection(),并且池是空的,会发生什么呢?这是我们检查引用队列以找到被废弃的连接的时机。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    private void tryWaitingForGarbageCollector()
    {
        try
        {
            Reference<?> ref = _refQueue.remove(100);
            if (ref != null)
                releaseConnection(ref);
        }
        catch (InterruptedException ignored)
        {
            // we have to catch this exception, but it provides no information here
            // a production-quality pool might use it as part of an orderly shutdown
        }
    }
    

    这个函数强调了另一组相互冲突的目标:如果引用队列中没有任何引用,我们不想浪费时间,但是我们也不想在一个循环中重复检查_pool_refQueue。所以我在轮询队列时使用了一个短暂的超时时间:如果没有准备好,它会给另一个线程返回连接的机会。当然,这也带来了一个公平性问题:当一个线程正在等待引用队列时,另一个线程可能会返回一个被第三个线程立即占用的连接。理论上,等待线程可能会永远等待。在现实世界中,由于不太需要数据库连接,这种情况不太可能发生。

    虚引用带来的问题

    前面我提到到finalizers不能保证被调用。虚引用也是这样,原因相同:如果回收器不运行,不可达的对象不会被回收,对这些对象的引用也不会进入队列。考虑一个程序只在循环中调用getConnection(),让返回的连接超出作用域,如果它没有做任何其他事情来让垃圾回收器运行,那么它会很快耗尽连接池然后阻塞,等待永远无法恢复的连接。

    当然,有办法解决这个问题。最简单的方法之一是在tryWaitingForGarbageCollector()中调用System.gc()。尽管围绕这种方法有一些争议,但这是促使JVM回到理想状态的有效方式。这是一种既适用于finalizers也适用于虚引用的技术。

    这并不意味着你应该忽略虚引用,只使用finalizer。例如,在连接池的情况下,你可能希望显式关闭该连接池并关闭所有底层连接。你可以用finalizer来完成,但是需要和虚引用一样多的工作。在这种情况下,通过引用获得的可控因素(相对于任意终结线程)使它们成为更好的选择。

    最后一些思考:有时候你只需要更多内存

    虽然引用对象是管理内存消耗的非常有用的工具,但有时它们是不够的,有时又是过度的。例如,假设你正在构建一些大型对象,其中包含从数据库中读取的数据。虽然你可以使用软引用作为读取的断路器,并使用弱引用将数据规范化,但最终您的程序需要一定量的内存来运行。如果你不能给它足够的内存来实际完成任何工作,那么不管你的错误恢复能力有多强都无济于事。

    应对OutOfMemoryError时你首先应该搞清楚它为什么会发生。可能你有内存泄露,可能仅仅是你内存的设置太低了。

    开发过程中,你应该指定大的堆内存大小——1G或更多——关注程序到底用了多少内存(这种情况jconsole是一个有用的工具)。大多数应用会在模拟的负载下达到一个稳定的状态,这将指引你的生产环境堆配置。如果你的内存使用随时间增长,那很可能你在对象不再使用后仍持有强引用,引用类型可能会有用,但更可能的是有bug需要修复。

    底线是你需要理解你的应用。如果没有重复,canonicalizing map对你没有帮助。如果你希望定期执行数百万行查询,软引用是没有用的。但是在可以使用引用对象的情况下,它们会是你的救命恩人。

    其他信息

    你可以下载这篇文章中的示例代码:
    CircuitBreakerDemo通过模拟数据库的结果集引出内存驱动的断路器。
    WeakCanonicalizingMap 用WeakHashMap创建了典范字符串。这个demo 可能更有趣: 它用极端的长度来触发垃圾回收(注意:在大的堆内存下运行可能不凑效,试试-Xmx100m).
    SlowFinalizer展示了如何在垃圾回收器运行的情况下耗尽内存。
    ConnectionPool 和 PooledConnection 实现了一个简单的连接池。ConnectionPoolDemo 通过内存型的HSQLDB数据库来运用这个连接池(这里 是构建这个和其他例子的Maven POM)。
    “string canonicalizer” 类可以在这下到SourceForge, licensed为Apache 2.0.
    Sun有许多关于调整他们JVM的内存管理的文章。这篇 文章是一篇精彩的介绍,并提供了其他文档的链接。
    Brian Goetz在IBM developerWorks网站上有一个极好的专栏,叫做”Java Theory and Practice”。几年前他写了关于软引用 和 弱引用 的专栏. 这些文章对一些我看过的议题影响很深,例如使用WeakHashMap来用不同生命时期关联对象。

    我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

    展开全文
  • Java中的引用类型的对象存放在哪里

    千次阅读 2020-06-19 12:04:22
    根据上下文来确定。 根据上下文来确定。 根据上下文来确定。 比如 void func() ...对于方法中的局部变量的引用时存放在java运行时数据区的栈中,对于实例变量则是存放在java运行时数据区的堆中。 ...

    根据上下文来确定。
    根据上下文来确定。
    根据上下文来确定。
    比如

    void func()
    {
        Object obj = new Object();//这个obj在函数的栈里。
    }
    
    class Test
    {
       private Object obj = new Object();//这个obj随对应的Test对象分配在堆里
    }
    

    对于方法中的局部变量的引用时存放在java运行时数据区的栈中,对于实例变量则是存放在java运行时数据区的堆中。

    展开全文
  • 吃人的那些 Java 名词:对象引用、堆、栈

    万次阅读 多人点赞 2019-09-05 15:57:09
    作为一个有着 8 年 Java 编程经验的 IT 老兵,说起来很惭愧,我被 Java 当中的四五个名词一直困扰着:**对象引用、堆、栈、堆栈**(栈可同堆栈,因此是四个名词,也是五个名词)。每次我看到这几个名词,都隐隐...

    作为一个有着 8 年 Java 编程经验的 IT 老兵,说起来很惭愧,我被 Java 当中的四五个名词一直困扰着:对象、引用、堆、栈、堆栈(栈可同堆栈,因此是四个名词,也是五个名词)。每次我看到这几个名词,都隐隐约约觉得自己在被一只无形的大口慢慢地吞噬,只剩下满地的衣服碎屑(为什么不是骨头,因为骨头也好吃)。

    记得中学的课本上,有一篇名为《狂人日记》课文;那时候根本理解不了鲁迅写这篇文章要表达的中心思想,只觉得满篇的“吃人”令人心情压抑;老师在讲台上慷慨激昂的讲,大多数的同学同我一样,在课本面前“痴痴”的发呆。

    十几年后,再读《狂人日记》,恍然如梦:

    鲁迅先生以狂人的口吻,再现了动乱时期下中国人的精神状态,视角新颖,文笔细腻又不乏辛辣之味。

    当时的中国,混乱成了主色调。以清廷和孔教为主的封建旧思想还在潜移默化地影响着人们的思想,与此同时以革命和新思潮为主的现代思想已经开始了对大众灵魂的洗涤和冲击。

    最近,和沉默王二技术交流群(120926808)的群友们交流后,Java 中那四五个会吃人的名词:对象、引用、堆、栈、堆栈,似乎在脑海中也清晰了起来,尽管疑惑有时候仍然会在阴云密布时跑出来——正鉴于此,这篇文章恰好做一下归纳。

    一、对象和引用

    在 Java 中,尽管一切都可以看做是对象,但计算机操作的并非对象本身,而是对象的引用。 这话乍眼一看,似懂非懂。究竟什么是对象,什么又是引用呢?

    先来看对象的定义:按照通俗的说法,每个对象都是某个类(class)的一个实例(instance)。那么,实例化的过程怎么描述呢?来看代码(类是 String):

    new String("我是对象张三");
    new String("我是对象李四");
    

    在 Java 中,实例化指的就是通过关键字“new”来创建对象的过程。以上代码在运行时就会创建两个对象——“我是对象张三"和"我是对象李四”;现在,该怎么操作他们呢?

    去过公园的同学应该会见过几个大爷,他们很有一番本领——个个都能把风筝飞得老高老高,徒留我们眼馋的份!风筝飞那么高,没办法直接用手拽着飞啊,全要靠一根长长的看不见的结实的绳子来牵引!操作 Java 对象也是这个理,得有一根绳——也就是接下来要介绍的“引用”(我们肉眼也常常看不见它)。

    String zhangsan, lisi;
    zhangsan = new String("我是对象张三");
    lisi = new String("我是对象李四");
    

    这三行代码该怎么理解呢?

    先来看第一行代码:String zhangsan, lisi;——声明了两个变量 zhangsan 和 lisi,他们的类型为 String。

    ①、歧义:zhangsan 和 lisi 此时被称为引用。

    你也许听过这样一句古文:“神之于形,犹利之于刀;未闻刀没而利存,岂容形亡而神在?”这是无神论者范缜(zhen)的名言,大致的意思就是:灵魂对于肉体来说,就像刀刃对于刀身;从没听说过刀身都没了刀刃还存在,那么怎么可能允许肉体死亡了而灵魂还在呢?

    “引用”之于对象,就好比刀刃之于刀身,对象还没有创建,又怎么存在对象的“引用”呢?

    如果 zhangsan 和 lisi 此时不能被称为“引用”,那么他们是什么呢?答案很简单,就是变量啊!(鄙人理解)

    ②、误解:zhangsan 和 lisi 此时的默认值为 null

    应该说 zhangsan 和 lisi 此时的值为 undefined——借用 JavaScript 的关键字;也就是未定义;或者应该是一个新的关键字 uninitialized——未初始化。但不管是 undefined 还是 uninitialized,都与 null 不同。

    既然没有初始化,zhangsan 和 lisi 此时就不能被使用。假如强行使用的话,编译器就会报错,提醒 zhangsan 和 lisi 还没有出生(初始化);见下图。

    如果把 zhangsan 和 lisi 初始化为 null,编译器是认可的(见下图);由此可见,zhangsan 和 lisi 此时的默认值不为 null

    再来看第二行代码:zhangsan = new String("我是对象张三");——创建“我是对象张三"的 String 类对象,并将其赋值给 zhangsan 这个变量。

    此时,zhangsan 就是"我是对象张三"的引用;“=”操作符赋予了 zhangsan 这样神圣的权利。

    第三行代码 lisi = new String("我是对象李四");和第二行代码 zhangsan = new String("我是对象张三");同理。

    现在,我可以下这样一个结论了——对象是通过 new 关键字创建的;引用是依赖于对象的;= 操作符把对象赋值给了引用

    我们再来看这样一段代码:

    String zhangsan, lisi;
    zhangsan = new String("我是对象张三");
    lisi = new String("我是对象李四");
    zhangsan = lisi;
    

    zhangsan = lisi; 执行过后,zhangsan 就不再是"我是对象张三"的引用了;zhangsan 和 lisi 指向了同一个对象(“我是对象李四”);因此,你知道 System.out.println(zhangsan == lisi); 打印的是 false 还是 true 了吗?

    二、堆、栈、堆栈

    谁来告诉我,为什么有很多地方(书、博客等等)把栈叫做堆栈,把堆栈叫做栈?搞得我都头晕目眩了——绕着门柱估计转了 80 圈,不晕才怪!

    我查了一下金山词霸,结果如下:

    我的天呐,更晕了,有没有!怎么才能不晕呢?我这里有几招武功秘籍,你们尽管拿去一睹为快:

    1)以后再看到堆、栈、堆栈三个在一起打牌的时候,直接把“堆栈”踢出去;这仨人不适合在一起玩,因为堆和栈才是老相好;你“堆栈”来这插一脚算怎么回事;这世界上只存在“堆、栈”或者“堆栈”(标点符号很重要哦)。

    2)堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是:

    当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。

    另外,需要记住,堆遵循“先进后出”的规则(此处有雷)。就好像,一个和尚去挑了一担水,然后把一担水装缸里面,等到他口渴的时候他再用瓢舀出来喝。请放肆地打开你的脑洞脑补一下这个流程:缸底的水是先进去的,但后出来的。所以,我建议这位和尚在缸上贴个标签——保质期 90 天,过期饮用,后果自负!

    还是记不住,看下图:

    (不好意思,这是鼎,不是缸,将就一下哈)

    3)栈,又名堆栈(简直了,完全不符合程序员的思维啊,我们程序员习惯说一就是一,说二就是二嘛),能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快;举个十分不恰当的例子哈——眼睛相对嘴巴是离脑子近的一方,因此,你可以一目十行,但绝对做不到一开口就读十行字,哪怕十个字也做不到

    既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗?

    不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。

    现在清楚堆、栈和堆栈了吧?

    三、基本数据类型

    先来看《Java 编程思想》中的一段话:

    在程序设计中经常用到一系列类型,他们需要特殊对待。之所以特殊对待,是因为 new 将对象存储于“堆”中,故用 new 创建一个对象──特别小、简单的变量,往往不是很有效。因此,不用new来创建这类变量,而是创建一个并非是引用的变量,这个变量直接存储值,并置于栈中,因此更加高效。

    在 Java 中,这些基本类型有:boolean、char、byte、short、int、long、float、double 和 void;还有与之对应的包装器:Boolean、Character、Byte、Short、Integer、Long、Float、Double 和 Void;它们之间涉及到装箱和拆箱,点击链接。

    看两行简单的代码:

     int a = 3;
     int b = 3;
    

    这两行代码在编译的时候是什么样子呢?

    编译器当然是先处理 int a = 3;,不然还能跳过吗?编译器在处理 int a = 3; 时在栈中创建了一个变量为 a 的内存空间,然后查找有没有字面值为 3 的地址,没找到,就开辟一个存放 3 这个字面值的地址,然后将 a 指向 3 的地址。

    编译器忙完了 int a = 3;,就来接着处理 int b = 3;;在创建完 b 的变量后,由于栈中已经有 3 这个字面值,就将 b 直接指向 3 的地址;就不需要再开辟新的空间了。

    依据上面的概述,我们假设在定义完 a 与 b 的值后,再令 a=4,此时 b 是等于 3 呢,还是 4 呢?

    思考一下,再看答案哈。

    答案揭晓:当编译器遇到 a = 4;时,它会重新搜索栈中是否有 4 的字面值,如果没有,重新开辟地址存放 4 的值;如果已经有了,则直接将 a 指向 4 这个地址;因此 a 值的改变不会影响到 b 的值哦。

    最后,留个作业吧,下面这段代码在运行时会输出什么呢?

    public class Test1 {
        public static void main(String args[]) {
            int a = 1;
            int b = 1;
    
            a = 2;
    
            System.out.println(a);
            System.out.println(b);
    
            TT t = new TT("T");
            TT t1 = t;
            t.setName("TT");
    
    
            System.out.println(t.getName());
            System.out.println(t1.getName());
        }
    }
    
    class TT{
        private String name;
    
        public TT (String name) {
            this.name = name;
        }
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String name1) {
            this.name = name1;
        }
    }
    

    上一篇:如何理解 Java 中的继承?

    下一篇:Java 的操作符——“=”号

    微信搜索「沉默王二」公众号,关注后回复「免费视频」获取 500G Java 高质量教学视频(已分门别类)。

    展开全文
  • Java对象引用变量

    万次阅读 多人点赞 2016-08-31 00:45:30
    对于引用变量的深层含义,未必在初学的时候就能深刻理解, 所以理解好下面这两句话的真正含义非常重要Case cc=new Case();... 基本类型的变量和对象引用变量 存取速度比堆要快,仅次于寄存器,栈
  • java对象对象引用变量

    万次阅读 多人点赞 2018-07-12 14:47:54
    Java对象及其引用 先搞清楚什么是堆,什么是栈。 Java开辟了两类存储区域,对比二者的特点 存储区域 存储内容 优点 缺点 回收 栈 基本类型的变量和对象的引用变量 存取速度比堆要快,仅次于...
  • 浅谈java对象引用及对象赋值

    千次阅读 多人点赞 2017-01-05 15:11:46
    一、Java对象及其引用  初学Java,总是会自觉或不自觉地把Java和C++相比较。在学习Java类与对象章节的时候,发现教科书和许多参考书把对象和对象的引用混为一谈。可是,如果分不清对象与对象引用, 那实在没法很...
  • 创建对象的方式用new语句创建...调用对象的clone()方法使用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。还有其他一些隐式创建对象的方法:对于java命令中的每个命令行参数,Java虚拟机都会...
  • 判断java对象是否已被gc

    千次阅读 2021-02-27 23:13:30
    在以往的教科书中说java是采用引用计数算法来决定gc的。简单描述下引用计数算法:给对象添加一个引用计数器。每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能...
  • java中,对象,还有所有的new出来的对象都是存放在堆里。  方法存放在方法体中。  基本类型的变量的数据和对象引用存放在栈中  ...
  • 在前面两篇文章中了解到Java对象实例是如何在HotSpot虚拟机的Java堆中创建的,以及创建后的内存布局是怎样的。下面详细了解在Java堆中的Java对象是如何访问定位的:先来了解reference类型数据是什么,再来了解两种...
  • java对象引用对象的区别

    千次阅读 2019-09-15 15:49:09
    什么是对象,什么是对象引用 对象,就是类的一个实例化,把一个抽象不好理解的类举出一个实体来,例如人类是一个类,会吃喝拉撒,实例化出一个小明这个具体的人。 对象引用,就是得给这个人取个名字来指代他,跟c++...
  • java对象存储在哪里?

    千次阅读 2020-02-29 14:29:17
    1、寄存器 寄存器是速度最快的存储区域,...常用于存放对象引用与基本数据类型,不存放Java对象。栈内存被要求存放在其中的数据的大小、生命周期必须是已经确定的。 3、堆 通用的内存池,位于RAM中,用于存放所有的...
  • 对象引用和清除_Java语言程

    千次阅读 2021-03-17 15:54:13
    对象引用和清除_Java语言程4.3.3 对象引用和清除在创建了类的对象后,就可以使用对象。即对象使用的原则是“先创建后使用”。使用对象的方法是:通过运算符“.”访问对象的各个成员变量和成员方法,进行各种...
  • Java对象引用变量

    千次阅读 2019-08-19 14:59:26
    基本类型的变量和对象引用变量 存取速度比堆要快,仅次于寄存器,栈数据可以共享 存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量 当超过变量的作用域后,Java会自动释放掉...
  • 本文将全面讲解判断Java对象存活的方式,希望你们会喜欢
  • 浅谈一下JAVA对象对象引用以及对象赋值

    万次阅读 多人点赞 2013-09-19 00:50:29
    浅谈一下JAVA对象对象引用以及对象赋值   今天有班级同学问起JAVA对象的引用是什么。正好趁着这次机会,自己总结一下JAVA对象对象引用以及对象赋值。自己总结了所看到的网上相关方面的不少帖子,整理汇总形成...
  • Java对象的四种引用

    千次阅读 2021-09-09 16:00:03
    也就是说,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。垃圾回收器一旦发现这些无用对象,就会对其进行回收。但是,在某些情况下,我们会希望有些对象不需要被立即回收,或者说从全局的角度来说没有...
  • java引用对象什么时候回收?

    千次阅读 2020-08-19 15:38:11
    那么就有一个问题,Object obj=new Object(),obj作为强引用存在虚拟机栈中,而new Object()作为对象存在于堆中,当obj的作用域结束,对应的虚拟机栈消失,obj引用也同时消失,但new Object()对象却仍然存在于堆中,...
  • 堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象) 1.引用计数算法  很多教科书判断...
  • java对象的几种引用方式

    千次阅读 2018-05-27 21:48:14
    在看java的ThreadLocal的源码实现时,涉及到了弱引用,对于这种引用方式,并不太常用到,翻看...只要强引用存在对象就不会被垃圾回收器回收。 可以通过将引用置空的方式,让JVM回收该对象。 2.软引用 S...
  • Java数组属于引用类型对象,以此为例说明地址引用和内容复制的区别 1.地址引用 为一个数组变量赋值另一个数组变量后,2个数组变量指向同一个内存地址,引用同一个数组对象,此时内存中并没有建立新的数组对象。 2....
  • JAVA中的引用四种引用类型

    千次阅读 2020-08-30 10:09:38
    关于值类型和引用类型的话题,C++、JAVA、python、go、C#等等高级语言都有相关的概念,只要理解了其底层工作原理,可以说即使是不同的语言,在面试学习工作实践中都可以信手拈来(不要太纠集语言),当然此处我选择了...
  • Java引用对象在堆、栈内存中的变化

    千次阅读 2018-09-04 21:18:20
    最近又重新开始学习Java基础,再次学习也对引用对象使用时内存变化有了进一步的了解。 这里先对Java虚拟机中堆栈功能简单总结; 1、对象主要存放在堆内存中;方法和属性主要存放在栈内存中。 2、栈是运行时单位...
  • Java对象到底存在堆中还是栈中

    千次阅读 2020-07-30 14:50:02
    创建一个对象的时候,到底是在栈中分配还是在堆中分配需要看2个方面:对象类型和在Java存在的位置 1.如果是基本数据类型,byte、short、int、long、float、double、char,如果是在方法中声明,则存储在栈中,其它...
  • Java对象的复制三种方式

    千次阅读 2021-02-12 09:36:34
    Java对象的复制三种方式概述在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也...
  • Effective-Java 通过接口引用对象

    千次阅读 2020-06-28 09:47:33
    64. 通过接口引用对象 条目 51 指出,应该使用接口而不是类作为参数类型。更一般地说,你应该优先使用接口而不是类来引用对象。如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。 惟一...
  • Java父类引用指向子类对象

    千次阅读 2018-07-12 17:06:54
    JAVA 通过父类对象new 子类对象,这个对象的声明的类型就是父类的类型,调用这个对象的方法也只能是父类型的方法,子类独有的方法是不能够被使用的。例如 List alist =new ArrayList&lt;&gt;();//只能用...
  • java对象之间相互循环引用实例

    千次阅读 2016-04-15 12:29:29
    在C++中使用过智能指针的同学们应该都清楚智能指针对C++中内存... 最近参与到android的项目开发,对java的内存的管理有了一个初步的了解,很容易想到了循环引用的问题。比如下面这个例子:  public void buidDog
  • Java未将对象引用设置到对象的实例

    千次阅读 2018-12-06 11:18:08
    场景 在执行单元测试时,提示信息: 可能原因 (1)所设置的变量为空值或没有取到值,一般出现在传递参数的时候出现这个...(4)在程序中所引用的控件不存在。 解决 在接口调用时需要传递一个时间Date参数,原来是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 579,624
精华内容 231,849
关键字:

java对象的引用存在哪

java 订阅