Java-继承

继承是所有OOP语言不可缺少的部分,在java中使用extends关键字来表示继承关系。当创建一个类时,总是在继承,如果没有明确指出要继承的类,就总是隐式地从根类Object进行继承。比如下面这段代码:

class Person {
    public Person() {

         

    }
}

 

class Man extends Person {
    public Man() {

         

    }
}

类Man继承于Person类,这样一来的话,Person类称为父类(基类),Man类称为子类(导出类)。如果两个类存在继承关系,则子类会自动继承父类的方法和变量,在子类中可以调用父类的方法和变量。在java中,只允许单继承,也就是说 一个类最多只能显示地继承于一个父类。但是一个类却可以被多个类继承,也就是说一个类可以拥有多个子类。有多个儿子就十分正常了,但是有多个爸爸那就不正常了。

1.子类继承父类的成员变量

  当子类继承了某个类之后,便可以使用父类中的成员变量,但是并不是完全继承父类的所有成员变量。具体的原则如下:

  1)能够继承父类的public和protected成员变量;不能够继承父类的private成员变量;

  2)对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;

  3)对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名成员变量,需要使用super关键字来进行引用。

  2.子类继承父类的方法

  同样地,子类也并不是完全继承父类的所有方法。

  1)能够继承父类的public和protected成员方法;不能够继承父类的private成员方法;

  2)对于父类的包访问权限成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承;

  3)对于子类可以继承的父类成员方法,如果在子类中出现了同名称的成员方法,则称为覆盖,即子类的成员方法会覆盖掉父类的同名成员方法。如果要在子类中访问父类中同名成员方法,需要使用super关键字来进行引用。

  注意:隐藏和覆盖是不同的。隐藏是针对成员变量和静态方法的,而覆盖是针对普通方法的。(后面会讲到)

  3.构造器

  子类是不能够继承父类的构造器,但是要注意的是,如果父类的构造器都是带有参数的,则必须在子类的构造器中显示地通过super关键字调用父类的构造器并配以适当的参数列表。如果父类有无参构造器,则在子类的构造器中用super关键字调用父类构造器不是必须的,如果没有使用super关键字,系统会自动调用父类的无参构造器。看下面这个例子就清楚了:

class Shape {

     

    protected String name;

     

    public Shape(){
        name = "shape";
    }

     

    public Shape(String name) {
        this.name = name;
    }
}

 

class Circle extends Shape {

     

    private double radius;

     

    public Circle() {
        radius = 0;
    }

     

    public Circle(double radius) {
        this.radius = radius;
    }

     

    public Circle(double radius,String name) {
        this.radius = radius;
        this.name = name;
    }
}

  4.super

  super主要有两种用法:

  1)super.成员变量/super.成员方法;

  2)super(parameter1,parameter2….)

  第一种用法主要用来在子类中调用父类的同名成员变量或者方法;第二种主要用在子类的构造器中显示地调用父类的构造器,要注意的是,如果是用在子类构造器中,则必须是子类构造器的第一个语句。


Java初始化顺序

 JVM 类加载过程 :

   类从加载到虚拟机到卸载,它的整个生命周期包括:加载(Loading),验证(Validation),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)。其中,验证、准备和解析部分被称为连接(Linking)。

  blob.png

  加载:

    在加载阶段,虚拟机主要完成三件事:     

        1.通过一个类的全限定名来获取定义此类的二进制字节流。

        2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。

        3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。

  

  验证:

   验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:   

    1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。

    2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。

    3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。

    4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。

  

  准备:

   准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:

   pirvate static int size = 12;

   那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。

 

  解析:

   解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。

 

  初始化:

   在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。

    至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。


 Java初始化顺序过程:

   1  无继承情况下的Java初始化顺序: 

        class Sample
    {
          Sample(String s)
          {
                System.out.println(s);
          }
          Sample()
          {
                System.out.println("Sample默认构造函数被调用");
          }
    }
    class Test{
          static Sample sam=new Sample("静态成员sam初始化");
          Sample sam1=new Sample("sam1成员初始化");
          static{
                System.out.println("static块执行");
                if(sam==null)System.out.println("sam is null");
                sam=new Sample("静态块内初始化sam成员变量");
                }
          Test()
          {
                System.out.println("Test默认构造函数被调用");
          }
    
    }
    //主函数
     public static void  main(String  str[])
    {
    
         Test a=new Test();
    
     }

    输出结果为:

        静态成员sam初始化     —–静态成员初始化

        static块执行          —–静态块被执行

        静态块内初始化sam成员变量 —-静态块执行

        sam1成员初始化      —–普通成员初始化

        Test默认构造函数被调用  –—-构造函数执行

    

   由此可以得出结论:

    静态成员变量首先初始化(注意,Static可以看做一个静态成员,其执行顺序和其在类中申明的顺序有关)

    b 普通成员初始化

        c 构造代码块初始化。

        执行构造函数.

  

 

     对于静态成员(static块可以看成普通的一个静态成员,其并不一定在类初始化时首先执行)和普通成员,其初始化顺序只与其在类定义中的顺序有关,和其他因素无关。

例如下面的例子:

   

        class Test{
          static{
                System.out.println("static 块 1  执行");
                }
          static Sample staticSam1=new Sample("静态成员staticSam1初始化");
          Sample sam1=new Sample("sam1成员初始化");
          static Sample staticSam2=new Sample("静态成员staticSam2初始化");
          static{
                System.out.println("static 块 2  执行");
                }
          Test()
          {
                System.out.println("Test默认构造函数被调用");
          }
          Sample sam2=new Sample("sam2成员初始化");
      }

  则结果为:

    static 块 1  执行

    静态成员staticSam1初始化

    静态成员staticSam2初始化

    static 块 2  执行 

                          ——–静态成员

    sam1成员初始化

    sam2成员初始化

                          ——–普通成员

    Test默认构造函数被调用

                          ——–构造函数

    

2 Java继承情况下的初始化顺序:

class Test{
      static{
            System.out.println("父类static 块 1  执行");
            }
      static Sample staticSam1=new Sample("父类 静态成员staticSam1初始化");
      Sample sam1=new Sample("父类 sam1成员初始化");
      static Sample staticSam2=new Sample("父类 静态成员staticSam2初始化");
      static{
            System.out.println("父类 static 块 2  执行");
            }
      Test()
      {
            System.out.println("父类 Test默认构造函数被调用");
      }
      Sample sam2=new Sample("父类 sam2成员初始化");
}
class TestSub extends Test
{
      static Sample staticSamSub=new Sample("子类 静态成员staticSamSub初始化");
      TestSub()
      {
            System.out.println("子类 TestSub 默认构造函数被调用");
      }
      Sample sam1=new Sample("子类 sam1成员初始化");
      static Sample staticSamSub1=new Sample("子类 静态成员staticSamSub1初始化");
      
      static{System.out.println("子类 static 块  执行");}
      Sample sam2=new Sample("子类 sam2成员初始化");
}

 

    执行结果:

        父类 static 块 1  执行

        父类 静态成员staticSam1初始化

        父类 静态成员staticSam2初始化

        父类 static 块 2  执行

                                ——–父类静态成员初始化

        子类 静态成员staticSamSub初始化

        子类 静态成员staticSamSub1初始化

        子类 static 块  执行

                                ——-子类静态成员初始化

        父类 sam1成员初始化

        父类 sam2成员初始化

          父类 构造代码块

        父类 Test默认构造函数被调用       

                                ——-父类普通成员初始化和构造函数执行

        子类 sam1成员初始化

        子类 sam2成员初始化

           子类 构造代码块

        子类 TestSub 默认构造函数被调用

                                ——-父类普通成员初始化和构造函数执行

    

    由此得出Java初始化顺序结论:

    1 继承体系的所有静态成员初始化(先父类,后子类)

    2 父类初始化完成(普通成员的初始化–>构造代码块 —>构造函数的调用)

    3 子类初始化(普通成员–>构造代码块 —>构造函数)

    blob.png

     注意:

      调用类里面的final static变量是不会初始化任何类及静态代码块信息。

      调用父类的static变量或者是static方法只会初始化父类的静态代码块信息。

      调用子类的static变量或者是static方法 同时先初始化父类的静态代码信息。



   Person p = new Person("zhangsan",20);JVM运行到这句话时,做了什么事情?

        1,因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。

        2,执行该类中的static代码块,如果有的话,给Person.class类进行初始化。

        3,在堆内存中开辟空间,分配内存地址。

        4,在堆内存中建立对象的特有属性。并进行默认初始化。

        5,对对象属性进行显示初始化。

        6,对对象进行构造代码块初始化。

        7,对对象进行对应的构造函数初始化。

        8,将内存地址赋给栈内存中的p变量。


MemCached学习笔记

MemCache是什么:

      MemCache是一个自由、源码开放、高性能、分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高了网站访问的速度。MemCaChe是一个存储键值对的HashMap,在内存中对任意的数据(比如字符串、对象等)所使用的key-value存储,数据可以来自数据库调用、API调用,或者页面渲染的结果。MemCache设计理念就是小而强大,它简单的设计促进了快速部署、易于开发并解决面对大规模的数据缓存的许多难题,而所开放的API使得MemCache能用于Java、C/C++/C#、Perl、Python、PHP、Ruby等大部分流行的程序语言。

Memcached工作原理介绍:

      Memcached是一套C/S模式架构软件,在服务端启动memcached服务守护进程。可以指定监听的IP地址、端口号、并发访问连接数以及分配多少内存处理客户端的请求参数。

          1)、Socket事件处理机制:

             Memcached软件是由C语言来实现的,全部代码仅有2000多行,采用的是异步epoll/kqueue非阻塞IO网络模型,其实现的方式是基于异步的libevent事件单进程、单线程模式,使用libevent作为事件通知机制,多个服务端协同工作,但是这些服务端之间是没有任何通讯联系,没有服务端只对自己的数据进行管理。应用程序通过指定缓存的IP地址和端口,就可以链接memcached服务互相通信。

          2)、数据存储机制:

   需要被缓存的数据以key/value键值对的形式保存在服务端预分配的内存中,每个被缓存的数据都有唯一的标识key,操作memcached中的数据通过这个唯一的key进行。缓存到memcached中的数据仅放置在memcached服务预分配的内存中,而非存储在memcached所在的磁盘上,因此存取的速度非常快。

             由于memcached服务自身没有对缓存的数据进行持久化性存储设计,因此,在服务器端的memcache服务进程重启之后,存储在内存中的这些数据就会丢失。且内存的缓存的数据容量达到启动时设定的内存值,就自动使用LRU算法删除过期的缓存数据。

            开发memcached的初衷就是仅为内存缓存而设计的,因此并没有过多的考虑数据的永久存储问题,因此,如果使用memecached作为缓存数据服务,要考虑数据丢失带来的问题,例如是否可以重新生成数据,还有在高并发场合数据丢失会不会导致网络架构雪崩。

  Memcached访问模型:

       MemCache虽然被称为"分布式缓存",但是MemCache本身完全不具备分布式的功能,MemCache集群之间不会相互通信(与之形成对比的,比如JBoss Cache,某台服务器有缓存数据更新时,会通知集群中其他机器更新缓存或清除缓存数据),所谓的"分布式",完全依赖于客户端程序的实现,如图:

  blob.png

    MemCache一次写缓存的流程:         

    1、应用程序输入需要写缓存的数据

    2、API将Key输入路由算法模块,路由算法根据Key和MemCache集群服务器列表得到一台服务器编号

    3、由服务器编号得到MemCache及其的ip地址和端口号

    4、API调用通信模块和指定编号的服务器通信,将数据写入该服务器,完成一次分布式缓存的写操作

        读缓存和写缓存一样,只要使用相同的路由算法和服务器列表,只要应用程序查询的是相同的Key,MemCache客户端总是访问相同的客户端去读取数据,只要服务器中还缓存着该数据,就能保证缓存命中。

        这种MemCache集群的方式也是从分区容错性的方面考虑的,假如Node2宕机了,那么Node2上面存储的数据都不可用了,此时由于集群中Node0和Node1还存在,下一次请求Node2中存储的Key值的时候,肯定是没有命中的,这时先从数据库中拿到要缓存的数据,然后路由算法模块根据Key值在Node0和Node1中选取一个节点,把对应的数据放进去,这样下一次就又可以走缓存了,这种集群的做法很好,但是缺点是成本比较大。

  

MemCache客户端路由算法:

    memcached的分布式主要体现在client端,对于server端,仅仅是部署多个memcached server组成集群,每个server独自维护自己的数据(互相之间没有任何通信),通过daemon监听端口等待client端的请求。

    而在client端,通过一致的hash算法,将要存储的数据分布到某个特定的server上进行存储,后续读取查询使用同样的hash算法即可定位。

      1、简单的Hash算法(余数Hash):    

           client端可以采用各种hash算法来定位server:最简单的hash算法: targetServer = serverList[hash(key) % serverList.size],这种算法不仅简单,而且具有不错的随机分布特性

比方说,字符串str对应的HashCode是50、服务器的数目是3,取余数得到2,str对应节点Node2,所以路由算法把str路由到Node2服务器上。由于HashCode随机性比较强,所以使用余数Hash路由算法就可以保证缓存数据在整个MemCache服务器集群中有比较均衡的分布。

如果不考虑服务器集群的伸缩性,那么余数Hash算法几乎可以满足绝大多数的缓存路由需求,但是当分布式缓存集群需要扩容的时候,就难办了。

就假设MemCache服务器集群由3台变为4台吧,更改服务器列表,仍然使用余数Hash,50对4的余数是2,对应Node2,但是str原来是存在Node1上的,这就导致了缓存没有命中。如果这么说不够明白,那么不妨举个例子,原来有HashCode为0~19的20个数据,那么:

    blob.png

    (2)通过模拟请求的方式逐渐预热缓存,使缓存服务器中的数据重新分布    如果我扩容到20+的台数,只有前三个HashCode对应的Key是命中的,也就是15%。当然这只是个简单例子,现实情况肯定比这个复杂得多,不过足以说明,使用余数Hash的路由算法,在扩容的时候会造成大量的数据无法正确命中(其实不仅仅是无法命中,那些大量的无法命中的数据还在原缓存中在被移除前占据着内存)。这个结果显然是无法接受的,在网站业务中,大部分的业务数据度操作请求上事实上是通过缓存获取的,只有少量读操作会访问数据库,因此数据库的负载能力是以有缓存为前提而设计的。当大部分被缓存了的数据因为服务器扩容而不能正确读取时,这些数据访问的压力就落在了数据库的身上,这将大大超过数据库的负载能力,严重的可能会导致数据库宕机。

这个问题有解决方案,解决步骤为:

    (1)在网站访问量低谷,通常是深夜,技术团队加班,扩容、重启服务器

    2、一致性hash:

      为了解决上述问题,需要采用一致性算法。相对于取模的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。

为了方便理解,可以把这个有限值域理解成一个环,值顺时针递增。

         blob.png

           就如同图上所示,三个Node点分别位于Hash环上的三个位置,然后Key值根据其HashCode,在Hash环上有一个固定位置,位置固定下之后,Key就会顺时针去寻找离它最近的一个Node,把数据存储在这个Node的MemCache服务器中。使用Hash环如果加了一个节点会怎么样,看一下:

       blob.png

              看到我加了一个Node4节点,只影响到了一个Key值的数据,本来这个Key值应该是在Node1服务器上的,现在要去Node4了。采用一致性Hash算法,的确也会影响到整个集群,但是影响的只是加粗的那一段而已,相比余数Hash算法影响了远超一半的影响率,这种影响要小得多。更重要的是,集群中缓存服务器节点越多,增加节点带来的影响越小,很好理解。换句话说,随着集群规模的增大,继续命中原有缓存数据的概率会越来越大,虽然仍然有小部分数据缓存在服务器中不能被读到,但是这个比例足够小,即使访问数据库,也不会对数据库造成致命的负载压力。

            至于具体应用,这个长度为232的一致性Hash环通常使用二叉查找树实现。

            但是问题就是:增加和删除节点之后,会影响局部节点Cache的命中范围,一样会有负载不均问题。

    3、虚拟节点:

         为解决这个问题,需要使用虚拟节点的思想:

               上面的一致性Hash算法实现,可以在很大程度上解决很多分布式环境下不好的路由算法导致系统伸缩性差的问题,但是会带来另外一个问题:负载不均。

比如说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。现在在A与B之间增加了一个节点D,这导致了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。某种程度上来说,这失去了负载均衡的意义,因为负载均衡的目的本身就是为了使得目标服务器均分所有的请求。

解决这个问题的办法是引入虚拟节点,其工作原理是:为每个物理节点(server)在环上分配100~200个点,这样环上的节点较多,就能抑制分布不均匀。当为cache定位目标server时,如果定位到虚拟节点上,就表示cache真正的存储位置是在该虚拟节点代表的实际物理server上。

另外,如果每个实际server的负载能力不同,可以赋予不同的权重,根据权重分配不同数量的虚拟节点。

。采取这样的方式,就可以有效地解决增加或减少节点时候的负载不均衡的问题。

             参考:java的memcached client(gwhalin / Memcached-Java-Client)的源码来看一下consistent hash的实现。

Memcached内存管理机制深入剖析

     Malloc:

         mallloc的全称是memory allocation,中文叫动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态分配内存

       早期的Memcached内存管理方式是通过malloc分配的内存,使用后通过free来回首内存。种着方式容易产生内存碎片并降低操作系统对内存的管理效率。加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统对Memcached进程本身还慢,为了解决上述问题,slab allocator内存分配机制就诞生了。

 

    Slab内存管理机制:

        按照预先规定的大小,将分配给memcached服务的内存预先分割成特定长度的内存块(Chunk),再把内存相同的内存块分成组(chunk slab class),这些内存块不会释放,可以重复利用。 

    blob.png

      

     这张图片里面涉及了slab_class、slab、page、chunk四个概念,它们之间的关系是:

            1、MemCache将内存空间分为一组slab

   2、每个slab下又有若干个page,每个page默认是1M,如果一个slab占用100M内存的话,那么这个slab下应该有100个page

   3、每个page里面包含一组chunk,chunk是真正存放数据的地方,同一个slab里面的chunk的大小是固定的

   4、有相同大小chunk的slab被组织在一起,称为slab_class

   MemCache内存分配的方式称为allocator,slab的数量是有限的,几个、十几个或者几十个,这个和启动参数的配置相关。

   MemCache中的value过来存放的地方是由value的大小决定的,value总是会被存放到与chunk大小最接近的一个slab中,比如slab[1]的chunk大小为80字节、slab[2]的chunk大小为100字节、slab[3]的chunk大小为128字节(相邻slab内的chunk基本以1.25为比例进行增长,MemCache启动时可以用-f指定这个比例),那么过来一个88字节的value,这个value将被放到2号slab中。放slab的时候,首先slab要申请内存,申请内存是以page为单位的,所以在放入第一个数据的时候,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk数组,最后从这个chunk数组中选择一个用于存储数据。

   如果这个slab中没有chunk可以分配了怎么办,如果MemCache启动没有追加-M(禁止LRU,这种情况下内存不够会报Out Of Memory错误),那么MemCache会把这个slab中最近最少使用的chunk中的数据清理掉,然后放上最新的数据。针对MemCache的内存分配及回收算法,总结三点:

            1、MemCache的内存分配chunk里面会有内存浪费,88字节的value分配在128字节(紧接着大的用)的chunk中,就损失了30字节,但是这也避免了管理内存碎片的问题

           2、MemCache的LRU算法不是针对全局的,是针对slab的

           3、应该可以理解为什么MemCache存放的value大小是限制的,因为一个新数据过来,slab会先以page为单位申请一块内存,申请的内存最多就只有1M,所以value大小自然不能大于1M了

 

     Slab内存管理机制的特点:

1.      提前分配大内存的Slab 1MB 在进行小对象填充Chunk。

2.      避免大量的重复的初始化和清理,减少内存管理器的负担。

3.      避免频繁malloc/free内存分配导致的碎片

 

      Slab Allocator的缺点

 

      Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了:

      blob.png

       就是说,如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费。但是很遗憾,现在还不能进行任何调优,只能期待以后的版本了。但是,我们可以调节slab class的大小的差别

      MC内存管理机制小结:

1.      MC的早期内存管理机制为malloc(动态内存分配)

2.      malloc(动态内存分配)产生内存碎片,导致操作系统性能急剧下降。

3.      Slab内存分配机制就产生了,可以解决内存碎片的问题。

4.      Memcached服务的内存预分割成特定长度的内存块,成为Chunk,用户缓存数据的内存空间或内存块,相当于磁盘的Block,只不过磁盘的每一个Block都是相等的,而Chunk只有同一个Slab Class内才是相等的。

5.      Slab Class:特定大小(1M)的包括多个Chunk的集合或者组,一个Memcached包含多个Slab Class,每个Slab Class包含多个相同大小的Chunk。

6.      Slab机制也有缺点,例如,Chunk的空间会浪费(通过调优因子以及大小接近的数据存放入同一MC实例)。

      使用Growth Factor对Slab Allocator内存管理机制调优

           MemCached 默认的调优因子是1.25倍;

 

Memcached的删除机制

      Memcached不会释放已分配的内存空间,在数据过期后,客户端不能通过key取出它的值,其存储空间被重新利用。

Memcached使用的是一种Lazy Expiration策略,自己不会监控存入的key/value对是否过期,而是在获取key值时查看记录的时间戳,检查key/value对空间是否过期。这种策略不会在过期检测上浪费CPU资源。

    Memcached在分配空间时,优先使用已经过期的key/value对空间,当空间占满时,Memcached就会使用LRU算法来分配空间,删除最近最少使用的key/value对,将其空间分配给新的key/value对。在某些情况下,如果不想使用LRU算法,那么可以通过“-M”参数来启动Memcached,这样,Memcached在内存耗尽时,会返回一个报错信息。

指针回收、新的数据过来重新覆盖,指针重新指向。

MemCache指令汇总

    上面说过,已知MemCache的某个节点,直接telnet过去,就可以使用各种命令操作MemCache了,下面看下MemCache有哪几种命令:

命      令

作      用

get

返回Key对应的Value值

add 

添加一个Key值,没有则添加成功并提示STORED,有则失败并提示NOT_STORED

set 

 无条件地设置一个Key值,没有就增加,有就覆盖,操作成功提示STORED

replace 

按照相应的Key值替换数据,如果Key值不存在则会操作失败 

stats

返回MemCache通用统计信息(下面有详细解读)

stats items

返回各个slab中item的数目和最老的item的年龄(最后一次访问距离现在的秒数)

stats slabs

返回MemCache运行期间创建的每个slab的信息(下面有详细解读)

version

返回当前MemCache版本号

flush_all

清空所有键值,但不会删除items,所以此时MemCache依旧占用内存

quit

关闭连接

 

 

stats指令解读

stats是一个比较重要的指令,用于列出当前MemCache服务器的状态,拿一组数据举个例子:

复制代码

STAT pid 1023

STAT uptime 21069937

STAT time 1447235954

STAT version 1.4.5

STAT pointer_size 64

STAT rusage_user 1167.020934

STAT rusage_system 3346.933170

STAT curr_connections 29

STAT total_connections 21

STAT connection_structures 49

STAT cmd_get 49

STAT cmd_set 7458

STAT cmd_flush 0

STAT get_hits 7401

STAT get_misses 57

..(delete、incr、decr、cas的hits和misses数,cas还多一个badval)

STAT auth_cmds 0

STAT auth_errors 0

STAT bytes_read 22026555

STAT bytes_written 8930466

STAT limit_maxbytes 4134304000

STAT accepting_conns 1

STAT listen_disabled_num 0

STAT threads 4

STAT bytes 151255336

STAT current_items 57146

STAT total_items 580656

STAT evicitions 0

复制代码

这些参数反映着MemCache服务器的基本信息,它们的意思是:

参  数    名

作        用

pid

MemCache服务器的进程id 

uptime

服务器已经运行的秒数

time

服务器当前的UNIX时间戳 

version

MemCache版本 

pointer_size

当前操作系统指针大小,反映了操作系统的位数,64意味着MemCache服务器是64位的 

rusage_user

进程的累计用户时间 

rusage_system 

进程的累计系统时间 

curr_connections 

 当前打开着的连接数

total_connections 

 当服务器启动以后曾经打开过的连接数

connection_structures 

服务器分配的连接构造数 

cmd_get 

get命令总请求次数 

cmd_set

set命令总请求次数 

cmd_flush 

flush_all命令总请求次数 

get_hits 

总命中次数,重要,缓存最重要的参数就是缓存命中率,以get_hits / (get_hits + get_misses)表示,比如这个缓存命中率就是99.2% 

get_misses 

总未命中次数 

auth_cmds 

认证命令的处理次数 

auth_errors 

认证失败的处理次数 

bytes_read 

总读取的字节数

bytes_written 

总发送的字节数 

 limit_maxbytes

分配给MemCache的内存大小(单位为字节) 

accepting_conns 

是否已经达到连接的最大值,1表示达到,0表示未达到

listen_disabled_num 

统计当前服务器连接数曾经达到最大连接的次数,这个次数应该为0或者接近于0,如果这个数字不断增长, 就要小心我们的服务了

threads 

当前MemCache总线程数,由于MemCache的线程是基于事件驱动机制的,因此不会一个线程对应一个用户请求 

bytes 

当前服务器存储的items总字节数

current_items 

当前服务器存储的items总数量 

total_items 

自服务器启动以后存储的items总数量 

 

   stats slab指令解读

   如果对上面的MemCache存储机制比较理解了,那么我们来看一下各个slab中的信息,还是拿一组数据举个例子:

复制代码

 1 STAT1:chunk_size 96

 2 …

 3 STAT 2:chunk_size 144

 4 STAT 2:chunks_per_page 7281

 5 STAT 2:total_pages 7

 6 STAT 2:total_chunks 50967

 7 STAT 2:used_chunks 45197

 8 STAT 2:free_chunks 1

 9 STAT 2:free_chunks_end 5769

10 STAT 2:mem_requested 6084638

11 STAT 2:get_hits 48084

12 STAT 2:cmd_set 59588271

13 STAT 2:delete_hits 0

14 STAT 2:incr_hits 0

15 STAT 2:decr_hits 0

16 STAT 2:cas_hits 0

17 STAT 2:cas_badval 0

18 …

19 STAT 3:chunk_size 216

20 …

复制代码

         首先看到,第二个slab的chunk_size(144)/第一个slab的chunk_size(96)=1.5,第三个slab的chunk_size(216)/第二个slab的chunk_size(144)=1.5,可以确定这个MemCache的增长因子是1.5,chunk_size以1.5倍增长。然后解释下字段的含义:

参  数    名

作        用

chunk_size

当前slab每个chunk的大小,单位为字节

chunks_per_page

每个page可以存放的chunk数目,由于每个page固定为1M即1024*1024字节,所以这个值就是(1024*1024/chunk_size)

total_pages

分配给当前slab的page总数

total_chunks

当前slab最多能够存放的chunk数,这个值是total_pages*chunks_per_page

used_chunks

已经被分配给存储对象的chunks数目

free_chunks

曾经被使用过但是因为过期而被回收的chunk数

free_chunks_end

新分配但还没有被使用的chunk数,这个值不为0则说明当前slab从来没有出现过容量不够的时候

mem_requested

当前slab中被请求用来存储数据的内存空间字节总数,(total_chunks*chunk_size)-mem_requested表示有多少内存在当前slab中是被闲置的,这包括未用的slab+使用的slab中浪费的内存

get_hits

当前slab中命中的get请求数

cmd_set

当前slab中接收的所有set命令请求数

delete_hits

当前slab中命中的delete请求数

incr_hits

当前slab中命中的incr请求数

decr_hits

当前slab中命中的decr请求数

cas_hits

当前slab中命中的cas请求数

cas_badval

当前slab中命中但是更新失败的cas请求数

 

Java序列化和克隆–对象深度拷贝

序列化

  Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

 把Java对象转换为字节序列的过程称为对象的序列化。  

 把字节序列恢复为Java对象的过程称为对象的反序列化。  

1、序列化的用途

  利用对象的序列化可以保存应用程序的当前工作状态,下次再启动的时候将自动地恢复到上次执行的状态。

  对象的序列化主要有两种用途:

  (a) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

  (b) 在网络上传送对象的字节序列。

  

2、序列化的实现

  (1)JDK类库中的序列化API

     java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

    java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

     只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。

  (2)对象序列化与反序列化的过程

     将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。 

对象序列化包括如下步骤:

      (a)创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;

      (b)通过对象输出流的writeObject()方法写对象。

 

对象反序列化的步骤如下:  

 (a)创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;  

 (b)通过对象输入流的readObject()方法读取对象。 

下面让我们来看一个对应的例子,类的内容如下:

  

public class Customer implements Serializable {
    /**
     *  @author 奮鬥中德菜鳥
     *   版权所有,并保留所有权利。
     */
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    public Customer(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return "name=" + name + ", age=" + age;
    }
}
public class ObjectSaver {
    public static void main(String[] args) throws Exception {                  
        ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream("D:objectFile.obj"));                   //序列化对象               
        Customer customer = new Customer("阿蜜果", 24);             
        out.writeObject("你好!");                   
        out.writeObject(new Date()); 
        out.writeObject(customer); 
        out.writeInt(123); //写入基本类型数据                   
        out.close();                   //反序列化对象                  
        ObjectInputStream in = new ObjectInputStream (new FileInputStream("D:objectFile.obj"));                   
        System.out.println("obj1=" + (String) in.readObject());                   
        System.out.println("obj2=" + (Date) in.readObject());                 
        Customer obj3 = (Customer) in.readObject();                   
        System.out.println("obj3=" + obj3);                
        int obj4 = in.readInt();                
        System.out.println("obj4=" + obj4);     
        in.close();            
     }
}

3、serialVersionUID作用:

  序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。

有两种生成方式:  

   一个是默认的1L,比如: 

       Java代码

       private static final long serialVersionUID = 1L;     private static final long serialVersionUID = 1L;         

    一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: 

       Java代码

       private static final   long      serialVersionUID = xxxxL;    private static final   long      serialVersionUID = xxxxL; 

二、克隆

   有时想得到对象的一个复制品,该复制品的实体是原对象实体的克隆。复制品实体的变化不会引起原对象实体发生变化,这样的复制品称为原对象实体的克隆对象或简称克隆。

  1、浅复制(浅克隆)

   概念:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

   方法:类implements Cloneable,然后重写clone()方法,在clone()方法中调用super.clone()即可,没有其他操作了

  2、深复制(深克隆)

   概念:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍

  方法:

    (1)类implements Cloneable,然后重写clone()方法,在clone()方法中调用super.clone(),然后还要对引用型变量所指的对象进行克隆。

    (2)序列化:将该对象写出到对象输出流,那么用对象输入流读回的对象就是原对象的一个深度克隆

Java        
        代码 
        import java.io.ByteArrayInputStream; 
        import java.io.ByteArrayOutputStream; 
        import java.io.IOException; 
        import java.io.ObjectInputStream; 
        import java.io.ObjectOutputStream; 
         
        public class DepthClone { 
             
            public  final static Object objectCopy(Object oldObj) { 
                Object newObj = null; 
                 
                try { 
                    ByteArrayOutputStream bo = new ByteArrayOutputStream(); 
                    ObjectOutputStream oo = new ObjectOutputStream(bo); 
                    oo.writeObject(oldObj);//源对象 
                    ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray()); 
                    ObjectInputStream oi= new ObjectInputStream(bi); 
                    newObj = oi.readObject();//目标对象 
                } catch (IOException e) { 
                    e.printStackTrace(); 
                }catch (ClassNotFoundException e) { 
                    e.printStackTrace(); 
                } 
                 
                return newObj; 
            } 
        } 
         
        浅度克隆类:
        Java代码 
        import java.io.Serializable; 
         
        public class SimpleClone implements Cloneable ,Serializable { 
         
            private static final long serialVersionUID = -7552721152118950502L; 
             
            public    String s = null; 
            public Object o = null; 
            public CloneObject cloneObject = null; 
             
             
            public SimpleClone(CloneObject cloneObject) { 
                this.cloneObject = cloneObject; 
            } 
         
            public Object clone() { 
                SimpleClone newSimpleClone =  null; 
                try { 
                    /* 浅克隆 */ 
                    newSimpleClone = (SimpleClone)super.clone(); 
                } catch (CloneNotSupportedException e) { 
                    e.printStackTrace(); 
                } 
                return newSimpleClone; 
            } 
        }


Java引用类型

Java中提供了4个级别的引用:强应用、软引用、弱引用和虚引用

强引用( Final Reference)

 就是指在程序代码中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
强引用具备以下三个个特点:

      1. 强引用可以直接访问目标对象;

   2. 强引用锁指向的对象在任何时候都不会被系统回收。

  3. 强应用可能导致内存泄露;

    整个FinalReference类的定义如下(有些API中并没有加入FinalReference类的说明,只能看源码了):

1
2
3
4
5
6
7
package java.lang.ref;
/* Final references, used to implement finalization */
class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

   从类定义中可以看出,只有一个构造函数,根据所给的对象的应用和应用队列构造一个强引用。

软引用(Soft Reference)

是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
对于软引用关联着的对象,如果内存充足,则垃圾回收器不会回收该对象,如果内存不够了,就会回收这些对象的内存。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
案例1:

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
58
59
60
61
62
package collections.ref;

 

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

 

public class SoftRefTest
{
    private static ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();

 

    public static class MyObject{

 

        @Override
        protected void finalize() throws Throwable
        {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }

 

        @Override
        public String toString()
        {
            return "I am MyObject";
        }
    }

 

    public static class CheckRefQueue implements Runnable
    {
        Reference<MyObject> obj = null;
        @Override
        public void run()
        {
            try
            {
                obj = (Reference<MyObject>)softQueue.remove();
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            if(obj != null)
            {
                System.out.println("Object for SoftReference is "+obj.get());
            }
        }
    }

 

    public static void main(String[] args)
    {
        MyObject object = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference<>(object,softQueue);
        new Thread(new CheckRefQueue()).start();

 

        object = null;    //删除强引用
        System.gc();
        System.out.println("After GC: Soft Get= "+softRef.get());
        System.out.println("分配大块内存");
        byte[] b = new byte[5*1024*928];
        System.out.println("After new byte[]:Soft Get= "+softRef.get());
        System.gc();
    }
}

运行参数1:

1
-Xmx5M

运行结果1:

1
2
3
4
5
After GC: Soft Get= I am MyObject
分配大块内存
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null

运行参数2:

1
-Xmx5M -XX:PrintGCDetails

运行结果2(关于GC日志可以查看《Java堆内存http://blog.csdn.net/u013256816/article/details/50764532》):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[GC [PSYoungGen: 680K->504K(2560K)] 680K->512K(6144K), 0.0040658 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 8K->482K(3584K)] 512K->482K(6144K) [PSPermGen: 2491K->2490K(21504K)], 0.0188479 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
After GC: Soft Get= I am MyObject
分配大块内存
[GC [PSYoungGen: 123K->64K(2560K)] 605K->546K(7680K), 0.0004285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 64K->64K(2560K)] 546K->546K(7680K), 0.0003019 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 64K->0K(2560K)] [ParOldGen: 482K->482K(4608K)] 546K->482K(7168K) [PSPermGen: 2493K->2493K(21504K)], 0.0094748 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 0K->0K(2560K)] 482K->482K(7680K), 0.0003759 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 482K->472K(5120K)] 482K->472K(7680K) [PSPermGen: 2493K->2493K(21504K)], 0.0101017 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
MyObject's finalize called
Object for SoftReference is null
After new byte[]:Soft Get= null
[GC [PSYoungGen: 122K->32K(2560K)] 5235K->5144K(7680K), 0.0004806 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5144K->5112K(7680K) [PSPermGen: 2493K->2493K(21504K)], 0.0136270 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
Heap
 PSYoungGen      total 2560K, used 20K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 1% used [0x00000000ffd00000,0x00000000ffd05250,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 5120K, used 5112K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 5120K, 99% used [0x00000000ff800000,0x00000000ffcfe188,0x00000000ffd00000)
 PSPermGen       total 21504K, used 2500K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000)
  object space 21504K, 11% used [0x00000000fa600000,0x00000000fa871190,0x00000000fbb00000)

加入 -XX:PrintGCDetails参数运行可以更形象的看到GC回收的细节。
这个案例1中,首先构造MyObject对象,并将其赋值给object变量,构成强引用。然后使用SoftReference构造这个MyObject对象的软引用softRef,并注册到softQueue引用队列。当softRef被回收时,会被加入softQueue队列。设置obj=null,删除这个强引用,因此,系统内对MyObject对象的引用只剩下软引用。此时,显示调用GC,通过软引用的get()方法,取得MyObject对象的引用,发现对象并未被回收,这说明GC在内存充足的情况下,不会回收软引用对象。
接着,请求一块大的堆空间5*1024*928,这个操作会使系统堆内存使用紧张,从而产生新一轮的GC。在这次GC后,softRef.get()不再返回MyObject对象,而是返回null,说明在系统内存紧张的情况下,软引用被回收。软引用被回收时,会被加入注册的引用队列。
如果将上面案例中的数组再改大点,比如5*1024*1024,就会抛出OOM异常:

1
2
3
4
5
6
After GC: Soft Get= I am MyObject
分配大块内存
MyObject's finalize called
Object for SoftReference is null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at collections.ref.SoftRefTest.main(SoftRefTest.java:58)

软引用主要应用于内存敏感的高速缓存,在android系统中经常使用到。一般情况下,Android应用会用到大量的默认图片,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。SoftReference可以解决oom的问题,每一个对象通过软引用进行实例化,这个对象就以cache的形式保存起来,当再次调用这个对象时,那么直接通过软引用中的get()方法,就可以得到对象中中的资源数据,这样就没必要再次进行读取了,直接从cache中就可以读取得到,当内存将要发生OOM的时候,GC会迅速把所有的软引用清除,防止oom发生。
案例2:

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
public class BitMapManager {
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

 

    //保存Bitmap的软引用到HashMap
    public void saveBitmapToCache(String path) {
        // 强引用的Bitmap对象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加该对象到Map中使其缓存
        imageCache.put(path, softBitmap);
        // 使用完后手动将位图对象置null
        bitmap = null;
    }

 

    public Bitmap getBitmapByPath(String path) {

 

        // 从缓存中取软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}

弱引用(Weak Reference)

用来描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。一旦一个弱引用对象被垃圾回收器回收,便会加入到一个注册引用队列中。
我们略微修改一下案例1的代码,如下:

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
58
59
60
package collections.ref;

 

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

 

public class WeakRefTest
{
    private static ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();

 

    public static class MyObject{

 

        @Override
        protected void finalize() throws Throwable
        {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }

 

        @Override
        public String toString()
        {
            return "I am MyObject";
        }
    }

 

    public static class CheckRefQueue implements Runnable
    {
        Reference<MyObject> obj = null;
        @Override
        public void run()
        {
            try
            {
                obj = (Reference<MyObject>)weakQueue.remove();
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            if(obj != null)
            {
                System.out.println("删除的弱引用为:"+obj+"  but获取弱引用的对象obj.get()="+obj.get());
            }
        }
    }

 

    public static void main(String[] args)
    {
        MyObject object = new MyObject();
        Reference<MyObject> weakRef = new WeakReference<>(object,weakQueue);
        System.out.println("创建的弱引用为:"+weakRef);
        new Thread(new CheckRefQueue()).start();

 

        object = null;
        System.out.println("Before GC: Weak Get= "+weakRef.get());