精华内容
下载资源
问答
  • 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”关注。

    展开全文
  • 1.类与对象的介绍与定义 2.类成员初始化 3.类成员函数 toString的使用 5.包与全限定类名 6.static修饰与public、private修饰

    ====================================================================================

    1.类与对象的介绍与定义

    • 类与对象是面向对象程序设计中的核心编程;
    • 世间万物或具体或抽象;皆可对象;需要定义好对象的属性和行为;以此可以完成某件事;对象与引用介绍 跳转链接
    • 类:类是自定义的数据类型,实例化出来的变量我们成为对象;类是用来对一些实体(对象)来进行描述的,主要描述该实体(对象)具有哪些属性(外观尺寸等),哪些功能(用来干啥);
    • 使用class关键字来定义类
    // 创建类
    class ClassName{
     field; // 字段(属性) 或者 成员变量
     method(); // 行为 或者 成员方法
    }
    
    • 类名注意采用大驼峰定义
    1. 一个.java文件当中可以定义多个类,但必须有一个以public修饰的类;
    2. main方法所在的类必须要使用public修饰
    3. public修饰的类必须要和文件名相同
    4. new 关键字用于创建一个对象的实例.
    5. 使用 . 来访问对象中的属性和方法.
    6. 同一个类可以创建多个实例.

    例:
    // 在同一个包中创建的类,都可以被一个主类的主方法调用;(下例中,在Dog类中定义的该对象属性、行为;在ClassPractice类中主类的主函数中进行Dog对象实例化,进而初始化和调用方法)
    在这里插入图片描述

    package objectClass;
    
    /**
     * @Author: 19261
     * @Date: 2021/9/3 17:07
     */
    //通过创建特定的类,表示实际的对象特征;
    public class Dog {
        //属性
        public String name; //属性1:名字
        public int age;     //属性2:年龄
        public String breed;//属性3:品种
    
        //方法/行为
        public void eat(){
            System.out.println(name+"正在吃饭!");
        }
        public void play(){
            System.out.println(name+"正在玩耍!");
        }
        public void introduce(){
            System.out.println(name+"是"+breed+"品种,"+"今年"+age+"岁了!");
        }
    }
    
    package objectClass;
    
    /**
     * @Author: 19261
     * @Date: 2021/9/1 19:34
     */
    public class ClassPractice {
        public static void main(String[] args) {
            //对象实例化
            //对象属性初始化:通过 . 解引用操作
            //方法行为的调用:通过 . 调用该对象封装类的方法,完成某种行为
            Dog dog1=new Dog();
            dog1.name="小呆";
            dog1.age=1;
            dog1.breed="金毛";
    
            Dog dog2=new Dog();
            dog2.name="旺财";
            dog2.age=2;
            dog2.breed="吉娃娃";
    
            Dog dog3=new Dog();
            dog3.name="迪加";
            dog3.age=1;
            dog3.breed="边牧";
    
            dog1.introduce();
            dog2.introduce();
            dog3.introduce();
            dog1.eat();
            dog2.play();
            dog3.play();
        }
    }
    

    在这里插入图片描述

    2.类成员初始化

    (1)默认初始化

    // 默认初始化: 只要创建了类对象,Java中都会先把对象成员自动设置为默认值;

    • 数字类型(包括整数和浮点数)默认值为0;
    • 布尔类型默认为false;
    • 引用类型默认为null;
    public class Test {
        public byte a;
        public short b;
        public char c;
        public int d;
        public long e;
        public double f;
        public boolean l;
        public String s;
        public int[] arr;
    }
    
    public class ClassPractice {
        public static void main(String[] args) {
            Test t=new Test();
            System.out.println(t.a);
            System.out.println(t.b);
            System.out.println(t.c);
            System.out.println(t.d);
            System.out.println(t.e);
            System.out.println(t.f);
            System.out.println(t.l);
            System.out.println(t.s);
            System.out.println(t.arr);
    	}
    }
    

    在这里插入图片描述

    (2)构造方法对类字段初始化

    • 类对象的构造方法必须与类名相同;
    • 若类中已经定义了有参构造方法,则不在生成无参构造的对象;
    • 类中的构造方法没有return返回值,默认返回对象本身;
    • 构造方法支持重载(通过参数个数和类型的不同实现重载)
    • 构造方法不需要显式调用,在类对象通过new关键字创建时 自动调用执行;
    class Student{
        //成员/属性
        public String name;
        public int age;
        //构造方法
        public Student(){
            name="小明";
            age=18;
        }
        public Student(int a){
            age=a;
        }
        public Student(String n){
            name=n;
        }
        public Student(String n,int a){
            name=n;
            age=a;
        }
        public void show(){
            System.out.println(name+" "+age);
        }
    }
    
    public class ClassPractice {
        public static void main(String[] args) {
            //对象实例化
            //对象属性初始化:通过 . 解引用操作
            //方法行为的调用:通过 . 调用该对象封装类的方法,完成某种行为
            Student s1=new Student();
            s1.show();
            Student s2=new Student("小红");
            s2.show();
            Student s3=new Student(2);
            s3.show();
            Student s4=new Student("小白",19);
            s4.show();
        }
    }       
    

    // 创建对象时,通过 new() 调用实现自动调用构造方法初始化;
    // 在创建对象时,先默认初始化;然后再通过构造方法初始化;没有通过构造方法初始化的成员属性,仍保留默认值;
    在这里插入图片描述

    (3)this 的使用与代码块初始化

    • this 表示当前对象本身的引用
    • this 在成员方法中,只能只想该对象,不能再指向别的对象,具有final属性;
    • 可以通过this 调用自身的构造方法;
    • this 只能在成员方法中使用,必须放在第一行,第一个使用;
    • 使用场景:当形参与成员名相同时,必须加以区分,使用this . 属性 ——表示指向对象本身;
    • java编译器给每个“成员方法“增加了一个隐藏的引用类型参数,该引用参数指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中中所有成员变量的操作,都是通过该引用去访问。

    // 代码块相当于无参的构造方法;并且构造方法后执行于代码块

    public class Point {
        public int x;
        public int y;
        //相当于没有参数的构造方法,也是在new 创建对象时自动调用
        {
            x=0;
            y=1;
        }
        //构造方法在默认初始化和代码块初始化之后执行的初始化
        public Point(int x,int y){
            //当形参和类成员属性名字相同时,使用Java中的this 指向对象的该属性
            this.x=x;
            // 通过this调用它自身构造方法
            //this(x);
            this.y=y;
        }
        public void show(){
            System.out.println("点("+x+","+y+")");
        }
    }
    
    public class ClassPractice {
        public static void main(String[] args) {
            Point p=new Point(1,2);
            p.show();
        }
    }
    

    在这里插入图片描述

    • // 在IDEA 中可以自动生成构造方法:
      Alt+Insert ->Constructor -> 选择属性 ->点击ok 完成创建;
      在这里插入图片描述

    在这里插入图片描述

    3.类成员函数 toString的使用

    • 每一个类中都可以创建一个toString()方法 用于查看所创建对象的属性;
    • 调用自己创建的toString()方法:System.out.println(对象名)/ 对象名 . toString();
    • IDEA 中可以自动生成toString()方法:Alt+Insert ->toString() -> 选择全部属性 ->点击ok 完成创建;
      在这里插入图片描述

    在这里插入图片描述

    • 自动生成的toString()方法 使用的时候直接 利用输出语句调用System.out.println(对象名);
    //    public String toString(){
    //        return "点("+this.x+","+this.y+")";
    //    }
        @Override
        public String toString() {
            return "Point{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }
    

    在这里插入图片描述

    5.包与全限定类名

    • 包::为了更好的管理类,把多个类或者收集在一起成为一组,称为软件包。
    • 包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式,
    • 在同一个工程中允许存在相同名称的类,只要处在不同的包中即可;
    • // 自定义包 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包
      在这里插入图片描述
    • 编写代码时,需要创建隶属于别的包(自定义/Java系统包)的对象,需要将类导入包中:
      • 1.通过全限定类名创建:java.util.Data data=new Data();
      • 2.通过使用 import语句导入包.import java.util.Data;
        // import java.util.*;表示导入Java类工具包util中的所有类;

    // 常见的Java包:

    1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
    2. java.lang.reflect:java 反射编程包;
    3. java.net:进行网络编程开发包。
    4. java.sql:进行数据库开发的支持包。
    5. java.util:是java提供的工具程序包。(集合类等) 非常重要
    6. java.io:I/O编程开发包。

    6.static修饰与public、private修饰

    (1)static的使用

    • 在Java中,被static修饰的成员,称之为静态成员,也可以称为类成员,其不属于某个具体的对象,是所有对象所共享的。
    • static 的使用:
      • 被static修饰的成员或属性,是类的属性/成员/字段,可以通过 对象.属性 访问,但一般通过 类名.属性访问;
        • 类的成员的生命周期伴随类的一生(即:随类的加载而创建,随类的卸载而销毁)。JDK7及以前,HotSpot(Java虚拟机)中存储在方法区,JDK8及之后,类变量存储在Java堆中;
      • 被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的。静态成员一般是通过静态方法来访问的(类名 . 成员方法)。
        • 静态方法没有隐藏的this引用参数,因此不能在静态方法中访问任何非静态成员变量
        • 可以在非静态方法中调用静态成员方法
          在这里插入图片描述
    public class Computer{
        public int id;
        private static String cpu;
        public String memory;
    
        public Computer() {
            id=123;
            cpu="MAR";
            memory="100G";
        }
        public void show(){
            System.out.println(id);
            //可以在非静态方法中调用静态成员方法
            this.replace("MMM");
        }
        public static void replace(String s){
            //this.id=n;
            cpu=s;
            System.out.println(cpu);
        }
    
        @Override
        public String toString() {
            return "Computer{" +
                    "id=" + id +
                    ", memory='" + memory + '\'' +
                    ", cpu='" + cpu + '\''+
                    '}';
        }
    }
    
    public class ClassPractice {
        public static void main(String[] args) {
            Computer c=new Computer();
            System.out.println(c);
            c.show();
            Computer.replace("max");
        }
    }
    
    • 实例代码块与static代码块
        //实例代码块
        {
            this.id=333;
            this.memory="50G";
            this.cpu="MAR20";
            System.out.println("执行实例代码块");
        }
        //static代码块
        static{
            cpu="NULL";
            System.out.println("执行static代码块");
        }
    

    在这里插入图片描述
    // 通过代码运行结果可知:

    • 由static修饰的静态代码块不管生成多少个对象,其只会执行一次静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的;
      在这里插入图片描述

    (2)访问限定符

    • Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认知,而访问权限用来控制那些方法或者属性需要在类外直接使用;
    • // 表格中有三角符号表示该限定符修饰的可以访问;
      在这里插入图片描述
      public:可以理解为一个人的外貌特征,谁都可以看得到
      default: 对于自己家族中不是什么秘密,对于其他人来说就是隐私了
      private:只有自己知道,其他人都不知道
    • 访问权限除了可以限定类中成员的可见性,也可以控制类的可见性

    例:
    在这里插入图片描述
    在这里插入图片描述

    7.Java面向对象之 封装

    • 面相对象程序三大特性:封装、继承、多态。而类和对象阶段,主要研究的就是封装特性。
    • **封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。**控制用户对类的修改和访问数据的程度。 适当的封装可以让程式码更容易理解和维护,也加强了程式码的安全性。
    • 封装是一种信息隐藏技术,在java中通过关键字private,protected和public实现封装
    展开全文
  • 对象”的一生像往常一样,早上10点到了公司,赵小八打开电脑收到了PM前一天晚上发来的推荐系统新需求,内心一万只草泥马飘过,思索了半天,打开IDEA开始了“愉快的”new对象之旅。垃圾回收...

    “对象”的一生

    像往常一样,早上10点到了公司,赵小八打开电脑收到了PM前一天晚上发来的推荐系统新需求,内心一万只草泥马飘过,思索了半天,打开IDEA开始了“愉快的”new对象之旅。

    垃圾回收器老哥:你这样疯狂的嚯嚯对象,有考虑过我的感受吗?

    赵小八:你谁啊?我new对象干你啥事?

    垃圾回收器老哥:年轻人火气别这么大,既然你这么说那请耗子尾汁。

    赵小八:呵,你哥我是被吓大的

    垃圾回收器老哥:年轻人不讲武德...

    没两天,小八翘着尾巴给PM说,功能上线了,刚没一会儿PM骂骂咧咧的找来了,这tm为啥有时候能出来内容有时候出不来啊,小八菊花一紧赶紧查起了问题,先搂监控接口平均耗时从200ms涨到了300ms,小八心想,我不过就多new了几个对象,怎么tm的影响会这么大,同时DBA同学反馈资源监控正常,看来只能搂业务日志看看了,可是业务日志也并没有什么问题,难道GC有问题?果不其然,GC日志像疯了一样的刷日志。小八赶紧让运维紧急回滚线上代码并dump了一份GC日志分析了起来。

    现场代码复原

    上面这段代码是一个简化版的用户推荐系统,真实情况下加载需要加载的物料除机器学习物料、商业物料外,还有其他各种例如:运营物料、曝光物料、关系物料等等。

    当一个真实用户请求过来之后,上面提到的这些物料就需要全部被加载进来。对象首先从新生代中被创建出来,接着经过一段时间GC后,最后存活下来的对象成功晋级到老年代,那么对象是在什么情况下成功晋级到老年代的呢?

    case1:对象经历15次GC

    1. 小八疯狂的new对象,此时新创建的都被分配到Eden区,如下图:

    1. 小八继续疯狂new对象,直到jvm老哥的Eden区放不下更多的对象了,于是触发了一次youngGC,通过这次youngGC之后,只有Context1对象被回收,剩余存活对象进入到了Survivor1里面,如下图:

    1. 第一次youngGC结束后,小八又开始了new对象的神操作

    1. 没一会儿,jvm又开始了youngGC,此时Eden区和Survivor1里面的存活对象全部移入到Survivor2中,剩余垃圾对象被回收。

    1. 就这样反反复复经历了15次youngGC的折腾,还没有被垃圾回收掉的对象最终进入了Old区

    case2:动态年龄判断

    1. 小八疯狂的new对象

    1. 小八继续疯狂new对象,直到jvm老哥的Enden区放不下更对的对象了,于是触发了一次youngGC

    经过此次youngGC后,剩余存活对象内存占用大小超过了survivor1区大小的50%,比如:survivor1区大小为50M,而进入到survivor1区的存活对象大小为30M,此时会将当前存活时间最久的对象直接晋升到老年代(存活时间:经历过GC次数最多的对象),此时Context2对象和Context3对象进入到老年代

    case3:空间担保机制

    小八上线的用户推荐系统,JVM内存的划分情况为:整个堆大小为5G,其中老年代2.5G,新生代2.5G,其中新生代中Eden区:Survivor区=8:2,即Eden区大小为2G,两个Survivor区大小各为250M。

    在晚高峰的时候一下子涌入1000人查看推荐列表,一个用户消耗的JVM内存达到了500kb,那么在一秒内就消耗了500M,那么就意味着4秒钟就会产生一次youngGC,假设每次GC后剩余的存活对象为300M,由于300M大小的存活对象无法在survivor区中存放下,此时就触发了空间担保机制。

    1. 小八疯狂的new对象

    1. 直到发生第一次youngGC,但是一次youngGC后剩余的存活的对象大小Survivor区无法容纳下,此时所有存活对象会直接进入到Old区

    在新生代没有足够的内存存储新产生的对象时,老年代会判断自己的区域剩余的内存空间是否能够放得下历代youngGC后剩余存活对象(假设历代youngGC剩余存活对象大小为300M),假设此时老年代还有1G大小的可用内存,那么此次youngGC后剩余的存活对象将直接进入到老年代;假设此时老年代剩余可用内存大小为200M,那么就会触发一次OldGC,OldGC完成后产生的空闲空间大于300M,此时会将新生代的存活对象放入老年代,如果OldGC后剩余的空闲空间小于300M,那么不好意思,就会抛出OOM了。

    一图总结Java对象流转情况

    上图便是整个Java对象一生经历的流程,流程图相对比较复杂一点,从上往下对照前面讲到的三种情况,相信还是比较容易理解的。

    当然图中没有画图新生代触发OOM的情况,可以试想一下Eden区在什么时候会触发OOM?答案在下篇文章给出。

    总结

    通过一个实际线上案例,讲述了Java对象在不同情况下在JVM中经历的一生。通过本文大家可以尝试将该流程套用到自己公司的项目里面,来分析自己负责的项目是否有类似的问题,或者通过本篇文章来尝试优化自己的项目。另外本文的内容可能会有某些地方讲解的不合适,欢迎有问题的朋友和我私聊探讨。

    在上篇文章中留了一个问卷调查,结论如下:总投票人数7人,其中最想了解的技术是SpringCloud,最喜欢的分享方式是图文结合。虽然投票人数比较少,但我相信投票的真实性,后续我会以这个结论为导向,分享更多实用的内容给大家。

    打个小广告,年后大家有换个工作氛围的朋友或者身边有想法的朋友,快手研发、运维、产品、运营全部岗位都有你想要的坑位,各种新业务发展速度快,机会多多,面试流程反馈速度超快,欢迎朋友们自荐或者推荐朋友来一起做点有意义的事。

     程序员小赵

    进欢加我私人微信来一场灵魂的探讨

    展开全文
  • 前言 上周有反映内容对于初学者较难,其实我觉得是讲的内容太多,导致自己不能生动具象地让所有人理解透彻,之后会注意。这里更提出两点建议适用于学习知识: 1. 不要囫囵吞枣,理论性知识要...什么是Java对象 Java

    前言

    上周有反映内容对于初学者较难,其实我觉得是讲的内容太多,导致自己不能生动具象地让所有人理解透彻,之后会注意。这里更提出两点建议适用于学习知识:

    1. 不要囫囵吞枣,理论性知识要逐字地去看

    2. 选取适当且固定的知识源,官网和经典书籍优先,养成学习惯性

    我写博客的目的就是更好地让大家理解难懂的概念,然后可以再去深入地讨论问题

    本期介绍JVM中对象的生命周期,应该算是一个常见且基础的问题,这里再拿出来重新梳理,希望能对读者有所帮助。

    目录

    前言

    一,Java 对象的本质

    二,如何识别“垃圾”

    三,如何在回收期间拯救java对象

    四,方法区可以执行GC么


    一,Java 对象的本质

    什么是Java对象

    Java对象是Java类所产生的实体

    概念本身没有什么可讨论的,不过为什么我们要如此重视Java对象的概念呢?或者说对象对于Java来说有什么特殊性?

    这就要从内存分配和垃圾回收说起

    Java和C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,外边的人想进去,里边的人想出来。

    C++编程每个生成的对象需要我们去控制它的生命周期以及分配内存,细致管理有始有终。而Java把对象的诞生,内存分配和消亡都交给了JVM。这就导致如果你不理解JVM,其实就不理解Java对象在机器中的状态,不是真正全面地理解Java。而且在实际生产环境中会遇到大量JVM相关的问题,所以了解JVM并不一定让你去开发JVM,而是能更好理解你的Java程序。

    所以当你Object o的时候创建了一个引用。

    当你new Object()的时候,你已经创建了一个对象

    Object o = new Object()已经创造出了一个引用为o的Object对象,你把它托付给JVM给它分配内存空间,并通过代码配合JVM管理它身为Java对象的一生。

     

    对象引用

    对象引用并不是只有上边一种,从JDK1.2开始,java引用共有四种(强度依次递减):

    强引用,软引用,弱引用,虚引用

    强引用就是上边的这种形式:

    Object o = new Object()

    只要引用还存在,不符合JVM回收垃圾的规则,就不会被回收掉,是我们大多数使用的对象引用。

     

    软引用 

    SoftReference<String> ref = new SoftReference<String>("Hello world")

    通过SoftReference创建,标识如果系统将要发生内存溢出异常,将会把这些对象列进回收范围并进行二次回收。

    可以用作缓存的设计

     

    弱引用

    WeakReference<String> abcWeakRef = new WeakReference<String>(abc);

    被弱引用所关联的对象无论内存够不够用,都只能生存到下一次垃圾回收发生之前。

     

    虚引用

    PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc,referenceQueue);   

    虚引用不会影响到实际对象的生命周期,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。你声明虚引用的时候是要传入一个queue的。当你的虚引用所引用的对象已经执行完finalize函数的时候,就会把对象加到queue里面。你可以通过判断queue里面是不是有对象来判断你的对象是不是要被回收了。

     

    二,如何识别“垃圾”

    如何判定对象是否存活,如何处理垃圾对象清除的时间?一般有以下两个方法。

    引用计数

    引用计数是基础计算引用的方法。

    简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收。

    // 我们初始化两个类的对象
    Obj a = new Obj();
    Obj b = new Obj();
    
    // 假设Obj有成员变量member的类型也是Obj,a与b相互引用
    a.member = b;
    b.member = a;
    
    // 这是当我们想回收a和b的时候 发现因为他们互相引用 导致引用计数方法不能让JVM回收他们
    a = null;
    b = null;

    不过 现在的gc不会使用引用计数来判断对象是否存活,一般使用根搜索方法,也叫可达性分析。

    可达性分析

    可达性算法的原理是以一系列叫做  GC Root  的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象会被 GC 回收。

    可作为GC Root的四种对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象 //引用范围?

    • 方法区中类静态属性引用的对象// static

    • 方法区中常量引用的对象 // constant

    • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 //JNI?

    理解GC Root需要掌握JVM的内存模型,线程非共享的虚拟机栈中存储的是此线程中正在被需要的对象引用,本地方法栈中存储native方法(用C++或其他语言的转化的方法)的引用对象。至于静态和常量的生命周期是Java程序中伴随着程序消亡的对象引用,所以以上引用可以作为GC Root。

     

    三,如何在回收期间拯救java对象

    在深入理解JVM中,阐述过关于回收期间拯救Java对象这么一个情节。其原理是如果被回收的对象覆盖了finalize方法,实现可达性分析标记GC Root引用链后,会把这个对象放到一个特殊队列中,开启另外一个线程等到主回收GC完成后,再标记特殊队列中的预回收对象进行回收,我们可以利用这个时间差控制对象重新添加GC Root引用链来实现挽救。不过这样做控制不好可能导致JVM崩溃,不建议这样实现。

     

    四,方法区可以执行GC么

    永久代与方法区

    方法区用于存放class的相关信息。永久代和方法区的关系相当于Java的类和接口的关系。很多垃圾回收器没有永久代概念,永久代是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。JDK8之后方法区的实现交给了元空间,永久代有一个JVM本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到OutOfMemoryError。

     

    永久代的垃圾回收

    在方法区实现垃圾回收效率比较低,一般会回收两类数据:

     

    废弃常量

    什么时候回收废弃常量?

    废弃常量 和堆中的对象回收类似,当常量池中的常量没有其他地方引用的时候,执行方法区的GC就会被回收

     

    无用的类

    怎么判断无用的类?

    该类的所有实例都已经被回收,堆中不存在该类的任何实例

    加载该类的classloader已经被回收

    该类对应的java.lang.class对象没有地方引用,即无法通过反射访问该类的方法

    不设置虚拟机-Xnoclassgc参数

     

    关于Xnoclassgc参数,在大量使用发射、动态代理、cglib等框架比如Spring、hibernate等,都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。如果限制类卸载功能及限制 PermSize大小,方法区很快就会溢出,所以常规不建议设置。

     

    参考:

    1. 深入理解JVM - 周志明

    2. 垃圾回收讲解:https://mp.weixin.qq.com/s/_AKQs-xXDHlk84HbwKUzOw

    展开全文
  • 但这种方法是如此符合直觉,以至于今天很多使用 Java 或 Python 等面向对象语言的程序员还在用这种过程式思维在写代码。只不过 C 中的结构体变成了 Java 中的“贫血类”(只有属性和 get、set 方法的类),C 中的...
  • java一个对象一生

    2019-09-25 22:16:17
    一个对象的一生:我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了...
  • JavaClass文件 java的class文件是java文件通过javac编译之后得到的jvm可执行的文件,以.class结尾的文件。 二 Java Class 对象的生命周期
  • Java对象创建过程

    2020-06-09 00:55:00
    Java中,一个对象只要被正确的实例化之后才能被使用,在对象实例化的时候,会先检查相关的类信息是否已经被加载并初始化,在类初始化完毕之后才会继续完成对象的实例化,类的一生主要经历加载、连接(验证、准备、...
  • JAVA面向对象编程_孙卫琴.pdf
  • java中实现类实现了接口,创建对象为什么要写成 接口 对象名 = new 类名 而不是 子类对象 对象名 = new 类名 例: public Interface A{ public abstract int test(); } class B implements A{ public int test(){ ...
  • java中什么样的对象能够进入老年代

    千次阅读 2019-10-08 16:17:22
    1.大对象:所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。 2.长期存活的...
  • JAVA对象的生成到终结

    2019-08-10 10:11:47
    类的一生分为七个阶段:加载,验证,准备,解析,初始化,使用,卸载。 前面五个阶段是类加载过程,类加载过程有且只有一次。 加载的工作内容是使用类的全限定名来获取定义一个二进制字节流,定义一个Class对象...
  • java面向对象思想的核心是多态 。多态机制也叫 动态绑定机制 。理解这些概念和核心思想比较好的方式就是画内存图。把一个多态例子程序的执行过程,在纸上画出来,那么就相对有一个较好的理解了。分析问题三步走:第...
  • JAVA XML转对象 对象转XML

    千次阅读 2018-10-18 11:17:37
    import java.util.List; /** * 报文对应class * * @author ***@163.com">*** * @version 1.0.0 * @since 1.0.0 * * Created at 2018/9/30 */ @JacksonXmlRootElement(localName = "msgbody") public ...
  • java对象实例化顺序

    2017-01-12 15:52:03
    java对象实例化时的顺序(静态成员变量、静态代码块、成员变量、方法块加载、构造函数加载) Java程序在执行过程中,类,对象以及它们成员加载、初始化的顺序如下: 1、首先加载要创建对象的类及其直接与间接父类。...
  • 其中J2SE是关键,如果学好了java se 部分,基础扎实了,后面进阶学习也比较轻松! 补充说明一下:我觉得学习java比较合适的方法是先把所有的知识点过一遍,然后把所有的知识点串起来,边做开发边补充,就像写文章一...
  • 你打算用Java 8一辈子都不打算升级到Java 14,真香

    万次阅读 多人点赞 2020-03-26 10:04:35
    05、最后,一定会有不少读者想要问我怎么学习 Java 的,那我干脆就把我看过的优质书籍贡献出来: 1)入门版:《Head First Java》、《Java 核心技术卷》 2)进阶版:《Java编程思想》、《Effective Java》、《Java...
  • 1、把对象的状态和行为看成一个统一的整体,将二者存放在一个独立的模块中(类); 2、“信息隐藏”,把不需要让外界知道的信息隐藏起来,尽可能隐藏对象功能实现细节,向外界暴露方法, 保证外界安全访问功能,把...
  • 对象的生命周期还好理解,就像是人的一生,从出生、少年、青年、中年、老年、死亡; 关于对象,可达性可以理解为根据引用类型的不同而被分成了不同的可达等级。 该篇只是对生命周期和可达级别做一个了解,目的是...
  • 就这段代码的情况,当new对象的时候,栈内存中就有一个值p,然后会把Person.class文件从硬盘中通过jvm将class文件加载进内存,然后是person类的静态的东西先执行,然后开辟堆内存空间。堆内存空间就有p的指向new ...
  • 谈起面向对象和面向过程,我们不免想起c语言和Java语言的互掐了,都已经乱斗好几年了; 其实在真正的开发中,我们就可以了解到所谓的面向对象和面向过程都是相辅相成的结果 在程序员一生的道路的书中我们就可以看到...
  • java对象分配

    2016-10-05 21:19:00
    1.为什么会有年轻代 我们先来屡屡,为什么需要把堆分代?...而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出...
  • JAVA类与对象

    2021-08-29 16:08:28
    Java是一门纯面相对象的语言(Object Oriented Program,继承OOP),在面相对象的世界里,一切皆为对象。面相对象是解决问题的一种思想,主要依靠对象之间的交互完成一件事情。用面相对象的思想来涉及程序,更符合人们...
  • Java面相对象

    2018-09-10 17:52:52
    四,数组 1一维数组 定义数组 数组类型 [] =new 数据类型 [数组长度]; 或者: 数组类型 数组名 [] =new 数组类型 [数组长度]; 注意: 1.定义数组时一定要指定数组名和数组长度 2.必须带有[],表示定义了一...
  • 前言 说起Java创建的对象一共有多少种方式这个问题,还是曾经有一次面试的时候被问起的。作为java开发者,我们每天创建很多对象,但是我们通常...本文将介绍5种方式来创建一个java对象: new关键字 Class.newInstan...
  • java面向对象

    2016-03-01 12:26:21
    (引用对数据库关系模型的理解“世界是由对象和关系构成,对象就是抽象出来的实体,关系就是实体间复杂的联系”,java中也类似,一切实体都可以抽象出来成为对象,忽略实体对象中个体的差异,将对象共有的属性不断...
  • 1.封装:隐藏具体实现细节,对外提供公共的访问接口。 优点:增强代码的可维护性。 ...具体实现方式:通过java接口继承定义统一的实现接口,通过方法重写为不同的实现类,子类定义不同的操作。
  • 作为一个在java这门领域沉迷一年半的人,如今在技术栈广度和深度 还是业务能力在客观层面讲确实进步迅速 (可我就是没秃头,诶这头发咋都掉不了) (变强和头发我都要(狗头保命)) 回顾自己的学习生涯我进步迅速...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,572
精华内容 5,428
关键字:

java对象的一生

java 订阅