精华内容
下载资源
问答
  • java对象相等问题

    万次阅读 多人点赞 2019-07-31 19:17:15
    输出结果是true和3,这理解起来很简单,java对象直接"="赋值其实就是将引用传给对方,让他也指向同一个内存地址。 所以如果a改变了里面属性的值,那这个地址存储的内容就变了,当aa去拿的时候就是变了之后的,...
    public class TestA {
    	private Integer a;
    	public Integer getA() {
    		return a;
    	}
    	public void setA(Integer a) {
    		this.a = a;
    	}
    }
    public class Test {
    	public static void main(String[] args) {
    		TestA a = new TestA();
    		a.setA(1);
    		TestA aa = a;
    		a.setA(3);
    		System.out.println(aa==a);
    		System.out.println(aa.getA());
    	}
    }
    

    输出结果是true和3,这理解起来很简单,java中对象直接"="赋值其实就是将引用传给对方,让他也指向同一个内存地址。
    所以如果a改变了里面属性的值,那这个地址存储的内容就变了,当aa去拿的时候就是变了之后的,因为两个指向依然同一个地址,
    所以aa==a是true的。
    但如果这么来:

      TestA a = new TestA();
    		a.setA(1);
    		TestA aa = a;
      a = new TestA();
    		a.setA(3);
    		System.out.println(aa==a);
    		System.out.println(aa.getA());
    

    那输出就是false和1了,因为a又重新new了一个对象,他引用的地址已经变了,就和aa一点关系都没有了!

    以上我们都知道,但对于包装类却看似不一样:

    public class Test {
    	public static void main(String[] args) {
    		Integer b = new Integer(5);
    		Integer c = b;
    		System.out.println(b==c);
    		b = b+1;
    		System.out.println(b);
    		System.out.println(c);
    		System.out.println(b==c);
    	}
    }
    

    以上的运行结果是:
    true
    6
    5
    false
    这似乎和我们之前的例子不一样了。c和b都指向同一个地址,所以第一次bc是true的,但当b加1之后,c并没有跟着b加1,
    b
    c变成了false,说明两个指向的地址已经不是同一个了。
    一开始对这个可能无法理解,我们先看看下面的例子:

    public class Test {
    	public static void main(String[] args) {
    		int x = 1;
    		int y = x;
    		x = x+1;
    		System.out.println(y==x);
    	}
    }
    

    输出是false,这个我们很容易知道,一开始x和y都是1,当x加1之后,x是2,y还是1,所以yx是false的,数值型的变量在判断时是直接比较他们的值的。

    再回忆一下java的装箱和拆箱:
    所谓装箱,就是把基本类型用它们相对应的引用类型包起来,使它们可以具有对象的特质,如我们可以把int型包装成Integer类的对象,或者把double包装成Double,等等。
    所谓拆箱,就是跟装箱的方向相反,将Integer及Double这样的引用类型的对象重新简化为值类型的数据。
    JDK1.5之后可自动进行装箱和拆箱的操作,例子如下:

    int i=10;
    Integer j=Integer.valueOf(i); //手动装箱操作
    int k=j.intValue();//手动拆箱操作
    
    int i1=10
    Integer j1=i1;//自动装箱操作
    int k1=j1;//自动拆箱操作
    

    通过上面的讲解应该就知道原因了:

    	Integer b = new Integer(5);
    	Integer c = b;
    	System.out.println(b==c);//现在两个指向同一个地址,是true的
    	b = b+1;//b是包装类,得拆箱成数值型,然后加1,变成数值6,然后把数值6再装箱变成包装类赋给b,等同于b=Integer.valueOf(b.intValue()+1);
    	//Integer.valueOf()会生成一个新的包装类对象(因为valueOf()方法的源码里写着,return的是new Integer(),源码见下)
    	System.out.println(b==c);//现在两个指向不同的地址,是false的
    

    终于真相大白了,上面的问题告一段落,我自以为对包装类很懂了,但再看看下面的题目:

        Integer b = 128;
    	Integer c = 128;
    	System.out.println(b==c);
    

    输出是false,这很好理解,128是数值型的,再赋值给包装类变量时先要装箱,上面的等同于:

         	Integer b = Integer.valueOf(128);
    		Integer c = Integer.valueOf(128);
    		System.out.println(b==c);
    

    这样很明显可以看成是false的,是两个不同的对象。
    再看看这个:

         	Integer b = 100;
    		Integer c = 100;
    		System.out.println(b==c);
    

    输出居然是true。小学生表示已经崩溃,是不是对java已经无爱了。但是我可不会就这样放弃,到底是什么原因呢?
    查看Integer的valueOf方法的源码如下:

    public static Integer valueOf(int i) {
    final int offset = 128;
    if (i >= -128 && i <= 127) { // must cache 
        return IntegerCache.cache[i + offset];
    }
        return new Integer(i);
    }
    

    于是明白了,在【-128,127】之间的数字,valueOf()返回的是缓存中的对象,所以两次调用返回的是同一个对象。
    也就是说Integer(100)这样的对象已经有了一个了,自动装箱之前就不去new了,直接使用缓存里的,所以是同一个对象,指向的是同一个引用地址。
    以上代码等同于:

         	Integer b = Integer.valueOf(100);
    		Integer c = Integer.valueOf(100);
    		System.out.println(b==c);
    

    所以输出是true。
    接下来下面这个也很明白了吧:

        Integer a = new Integer(100);  
        Integer b = 100;  
        System.out.println(a == b);
    

    输出的是false,因为a是自己new的对象,不是通过valueOf获取的,没有放进缓存里,所以b在valueOf时还是会new一个
    以上代码等同于:

        Integer a = new Integer(100);  
      	Integer b = Integer.valueOf(100);  
        System.out.println(a == b);
    

    这样就看的出来是false了。
    总之以上的现象只会在拆装箱,或者你自己手动调用valueOf方法时才会出现!

    展开全文
  • 一、Java对象及其引用 &amp;amp;amp;amp;amp;nbsp; &amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp;&amp;amp;amp;amp;amp;nbsp; 初学Java,总是会自觉或不...

    假如说你想复制一个简单变量。很简单:

    int apples = 5;  
    int pears = apples;  
    

    不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

    但是如果你复制的是一个对象,情况就有些复杂了。

    假设说我是一个beginner,我会这样写:

    class Student {  
        private int number;  
      
        public int getNumber() {  
            return number;  
        }  
      
        public void setNumber(int number) {  
            this.number = number;  
        }  
          
    }  
    public class Test {  
          
        public static void main(String args[]) {  
            Student stu1 = new Student();  
            stu1.setNumber(12345);  
            Student stu2 = stu1;  
              
            System.out.println("学生1:" + stu1.getNumber());  
            System.out.println("学生2:" + stu2.getNumber());  
        }  
    }
    

    结果:

    学生1:12345
    学生2:12345

    这里我们自定义了一个学生类,该类只有一个number字段。

    我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

    再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

    难道真的是这样吗?

    我们试着改变stu2实例的number字段,再打印结果看看:

    stu2.setNumber(54321);  
      
    System.out.println("学生1:" + stu1.getNumber());  
    System.out.println("学生2:" + stu2.getNumber());  
    

    结果:

    学生1:54321  

    学生2:54321  

    这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?

    原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,

    这样,stu1和stu2指向内存堆中同一个对象。如图:

    那么,怎样才能达到复制一个对象呢?

    是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。

    在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码,你可以把你的JDK目录下的src.zip复制到其他地方然后解压,里面就是所有的源码。发现里面有一个访问限定符为protected的方法clone():

    /*
    Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
    The general intent is that, for any object x, the expression:
    1) x.clone() != x will be true
    2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
    3) x.clone().equals(x) will be true, this is not an absolute requirement.
    */
    protected native Object clone() throws CloneNotSupportedException;
    

    仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

    1. 第一次声明保证克隆对象将有单独的内存地址分配。
    2. 第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
    3. 第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。

    因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

    要想对一个对象进行复制,就需要对clone方法覆盖。

    为什么要克隆?

      大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?

      答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。

      提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。

      而通过clone方法赋值的对象跟原来的对象时同时独立存在的。

    如何实现克隆

    先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)深克隆(DeepClone)

    在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

    一般步骤是(浅克隆):

    1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)

    2. 覆盖clone()方法,访问修饰符设为public方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)

    下面对上面那个方法进行改造:

    class Student implements Cloneable{  
        private int number;  
      
        public int getNumber() {  
            return number;  
        }  
      
        public void setNumber(int number) {  
            this.number = number;  
        }  
          
        @Override  
        public Object clone() {  
            Student stu = null;  
            try{  
                stu = (Student)super.clone();  
            }catch(CloneNotSupportedException e) {  
                e.printStackTrace();  
            }  
            return stu;  
        }  
    }  
    public class Test {  
        public static void main(String args[]) {  
            Student stu1 = new Student();  
            stu1.setNumber(12345);  
            Student stu2 = (Student)stu1.clone();  
              
            System.out.println("学生1:" + stu1.getNumber());  
            System.out.println("学生2:" + stu2.getNumber());  
              
            stu2.setNumber(54321);  
          
            System.out.println("学生1:" + stu1.getNumber());  
            System.out.println("学生2:" + stu2.getNumber());  
        }  
    }
    

    结果:

    学生1:12345  

    学生2:12345  

    学生1:12345  

    学生2:54321

    如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

    System.out.println(stu1 == stu2); // false  
    

    上面的复制被称为浅克隆。

    还有一种稍微复杂的深度复制:

    我们在学生类里再加一个Address类。

    class Address  {
        private String add;
    
        public String getAdd() {
            return add;
        }
    
        public void setAdd(String add) {
            this.add = add;
        }
    
    }
    
    class Student implements Cloneable{
        private int number;
    
        private Address addr;
    
        public Address getAddr() {
            return addr;
        }
    
        public void setAddr(Address addr) {
            this.addr = addr;
        }
    
        public int getNumber() {
            return number;
        }
    
        public void setNumber(int number) {
            this.number = number;
        }
    
        @Override
        public Object clone() {
            Student stu = null;
            try{
                stu = (Student)super.clone();
            }catch(CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return stu;
        }
    }
    public class Test {
    
        public static void main(String args[]) {
    
            Address addr = new Address();
            addr.setAdd("杭州市");
            Student stu1 = new Student();
            stu1.setNumber(123);
            stu1.setAddr(addr);
    
            Student stu2 = (Student)stu1.clone();
    
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
        }
    }
    

    结果:

    学生1:123,地址:杭州市  

    学生2:123,地址:杭州市  

     

    乍一看没什么问题,真的是这样吗?

    我们在main方法中试着改变addr实例的地址。

    addr.setAdd("西湖区");  
      
    System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
    System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); 
    

    结果:

    学生1:123,地址:杭州市  
    学生2:123,地址:杭州市  
    学生1:123,地址:西湖区  
    学生2:123,地址:西湖区  
    

    这就奇怪了,怎么两个学生的地址都改变了?

    原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

    所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

    package abc;
    
    class Address implements Cloneable {
        private String add;
    
        public String getAdd() {
            return add;
        }
    
        public void setAdd(String add) {
            this.add = add;
        }
    
        @Override
        public Object clone() {
            Address addr = null;
            try{
                addr = (Address)super.clone();
            }catch(CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return addr;
        }
    }
    
    class Student implements Cloneable{
        private int number;
    
        private Address addr;
    
        public Address getAddr() {
            return addr;
        }
    
        public void setAddr(Address addr) {
            this.addr = addr;
        }
    
        public int getNumber() {
            return number;
        }
    
        public void setNumber(int number) {
            this.number = number;
        }
    
        @Override
        public Object clone() {
            Student stu = null;
            try{
                stu = (Student)super.clone();   //浅复制
            }catch(CloneNotSupportedException e) {
                e.printStackTrace();
            }
            stu.addr = (Address)addr.clone();   //深度复制
            return stu;
        }
    }
    public class Test {
    
        public static void main(String args[]) {
    
            Address addr = new Address();
            addr.setAdd("杭州市");
            Student stu1 = new Student();
            stu1.setNumber(123);
            stu1.setAddr(addr);
    
            Student stu2 = (Student)stu1.clone();
    
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
    
            addr.setAdd("西湖区");
    
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
        }
    }
    

    结果:

    学生1:123,地址:杭州市  
    学生2:123,地址:杭州市  
    学生1:123,地址:西湖区  
    学生2:123,地址:杭州市  
    

    这样结果就符合我们的想法了。

     

    最后我们可以看看API里其中一个实现了clone方法的类:

    java.util.Date:

    /** 
     * Return a copy of this object. 
     */  
    public Object clone() {  
        Date d = null;  
        try {  
            d = (Date)super.clone();  
            if (cdate != null) {  
                d.cdate = (BaseCalendar.Date) cdate.clone();  
            }  
        } catch (CloneNotSupportedException e) {} // Won't happen  
        return d;  
    }
    

    该类其实也属于深度复制。

    参考文档:Java如何复制对象

    浅克隆和深克隆

    1、浅克隆

    在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

    简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

    浅克隆示意图

    在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆

    2、深克隆

    在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

    简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

    深克隆示意图

    在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。

    如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

    序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

    扩展
    Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,
    这种空接口也称为标识接口,标识接口中没有任何方法的定义,
    其作用是告诉JRE这些接口的实现类是否具有某个功能,
    如是否支持克隆、是否支持序列化等。
    

    解决多层克隆问题

    如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

    public class Outer implements Serializable{
      private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID
      public Inner inner;
     //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化]
      public Outer myclone() {
          Outer outer = null;
          try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              ObjectOutputStream oos = new ObjectOutputStream(baos);
              oos.writeObject(this);
          // 将流序列化成对象
              ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
              ObjectInputStream ois = new ObjectInputStream(bais);
              outer = (Outer) ois.readObject();
          } catch (IOException e) {
              e.printStackTrace();
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          }
          return outer;
      }
    }
    

    Inner也必须实现Serializable,否则无法序列化:

    public class Inner implements Serializable{
      private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
      public String name = "";
    
      public Inner(String name) {
          this.name = name;
      }
    
      @Override
      public String toString() {
          return "Inner的name值为:" + name;
      }
    }
    

    这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。

    总结

    实现对象克隆有两种方式:

      1). 实现Cloneable接口并重写Object类中的clone()方法;

      2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。

    展开全文
  • 今天闲来无事,有空闲的时间,所以想坐下来聊一聊Java的GC以及Java对象在内存中的分配。 和标题一样,本篇绝对是用最直接最通俗易懂的大白话来聊 文章中基本不会有听起来很高大上专业术语,也不会有太多概念性的...

    铁子们,快扫码关注啦!或 wx搜索:“聊5毛钱的java”,关注可领取博主的Java学习视频+资料,保证都是干货

    前言:

    今天闲来无事,有空闲的时间,所以想坐下来聊一聊Java的GC以及Java对象在内存中的分配。

    和标题一样,本篇绝对是用最直接最通俗易懂的大白话来聊

    文章中基本不会有听起来很高大上专业术语,也不会有太多概念性的描述,本着一看就懂的原则来写。

    因为之前看很多文章都是概念性的东西太多,让人越看越迷糊,越看越觉得有距离感,不接地气,看完之后跟没看一样。我最不希望的就是这个,所以我写的东西都是尽量用最通俗易懂、最接地气的大白话来描述,我写所有博客的愿景是让看文章的人看完后觉得有所收获,希望对看到文章的小可爱们有所帮助。当然,写的如有不对或不妥之处,还请海涵,欢迎下方留言指正,共同进步

    一、Java的垃圾回收GC

    先来聊GC,是因为这个过程中会涉及到JVM对堆内存的分代管理,以及eden区等等这些概念,有了这些概念后,再去聊Java对象在内存中的分配会好理解很多,所以先来聊一聊GC。

    1、确定垃圾对象

    GC(Garbage Collection)顾名思义就是垃圾回收的意思

    那既然要回收垃圾对象,首先得知道哪些对象是垃圾吧,怎么判定哪些是垃圾对象呢?

    有两个办法:

    (1)第一种是引用标记法,简单来说就是这个对象被一个地方引用了,这个计数的标记就加一,有地方释放了这个对象的引用,这个标记就减一。这个算法认为当一个对象的引用数量为零,那就意味着没有地方引用这个对象了,那此时这个对象就是垃圾对象。但是引用标记法有个致命的问题,那就是解决不了循环引用的问题,所以Java并没有采用引用标记法。具体什么是循环引用,并不是这里的重点,可以自行查一下

    (2)第二种是可达性分析法,简单说,就是从一个根对象出发,到某个对象如果有可达的路径,就认为它不是垃圾对象,否则认为是垃圾对象。根对象也被称为GC Root,那有个关键的问题:哪些对象可以被视为根对象呢?

    在《深入理解JVM虚拟机》中有这样的描述,以下三种可以作为根对象

    1.虚拟机栈:栈帧中的局部变量表引用的对象

    2.方法区中的静态变量和常量引用的对象

    3.native方法引用的对象

    确定了根对象后,通过这个算法就可以定位到哪些对象是垃圾对象了

    2、回收垃圾对象

    通过上边的方法知道了哪些对象是垃圾对象后,就可以回收垃圾了,回收垃圾同样也有几种不同的方法

    常见的有三种

    (1)标记-清除法:简单的说就是把那些已经标记为垃圾的对象进行清除,这种算法有一个缺点就是清理完成后会产生大量内存碎片(因为这些垃圾对象在内存中很多都是分散分布的,不可能总是连续的在一起的,所以清理完会导致内存不连续)

    (2)标记-压缩法:简单来说就是把那些已经标记为垃圾的对象进行清除后,再把分散开的内存碎片进行整理

    (3)复制法:简单说就是在要回收的内存区域之外,再另准备一块空白的内存,把不是垃圾的对象直接复制到这个空白内存区域里,然后就可以简单粗暴的把要回收的那个内存区域全部清空。

    不同的算法有不同的特点,不同的算法适用于不同的场景:

    少量对象存活,适合使用复制算法

    大量对象存活,适合使用标记清理或者标记压缩法

    所以JVM把堆内存进行了分代来管理,分为年轻代、老年代和永久代。不同的代,适用不同的回收算法

    由于年轻代的对象大部分是朝生夕死,只有少部分对象存活,所以很适合用复制算法
    但是复制算法有一个很大的缺点,就是需要两块一样大小的内存来进行轮换,这就导致了会浪费一半的空间。但是经过研究统计发现,年轻代每次只有大概10%的对象存活,所以就又把年轻代分为了eden区、servivor from区、servivor to区,他们的比例是8:1:1,也就是eden区占80%,servivor from和servivor to 它俩作为轮换区域,分别占10%(也就是用尽量少的内存资源来实现复制算法)。这个8:1:1面试的时候可能会被问到,需要注意一下

    但是每次大概只有10%对象存活,这是个统计的概率事件,实际中并不一定每次都只有10%或者少于10%的对象存活
    所以那要万一有时候存活的对象大于10%呢,那准备的servivor区的空间不就不够复制算法运行了吗?

    这怎么办?所以这时候需要担保,具体的说就是用老年代来作为担保,每次年轻代GC(minor GC)的时候都会去检查老年代最大连续可用空间是否大于年轻代中所有对象总和的大小,如果大于则认为这个担保是没有风险的,就进行正常的minor GC;

    但是如果发现小于,那就会继续判断你是否设置了允许担保失败,如果你设置的是不允许担保失败,那这次minor GC就要改为一次Full GC;如果你设置的是允许担保失败,那它会继续去判断老年代最大连续可用空间是否大于历次晋升到老年代的对象大小,如果大于,就尝试着去minor GC一次,尽管这次GC是有风险的,如果尝试后失败了,那就Full GC;如果小于的话,那这次minor GC也要改为一次Full GC。

    可以发现如果开启了允许担保失败的话,有可能会出现绕了一大圈最后发现还是失败了的情况,最后还是得去Full GC

    虽然会出现这种情况,但是还是建议设置开启这个允许担保失败,因为开启了允许担保失败后,会在一定程度上减少Full GC的次数,要知道一次Full GC的时间是minor GC的几倍甚至几十倍,所以要尽量避免Full GC

    画一下上述空间担保分配的简要流程图

    以上只是对垃圾回收的大体总结,并不涉及具体的细节

    有了以上大体了解后,建议拜读《深入理解JVM虚拟机》一书,写的确实很好的一本书,相信读后定会收获颇丰。

    讲垃圾回收一定绕不开那七种垃圾回收器:Serial(年轻代)、ParNew(年轻代)、Paralle Scavenge(年轻代)、Serial Old(年老代)、Parallel Old(年老代)、CMS(Concurrent Mark Sweep年老代)、G1

    不同垃圾回收器使用的算法不同(但都是基于标记清除或标记整理或复制算法这三种的),用的场景也不同

    关于这七种垃圾回收器,本篇并不展开详述,只是抛砖引玉,如有需要,也可自行参考《深入理解JVM虚拟机》

    二、Java对象在内存中的那些事

    下面来聊一聊Java对象在内存中的那些事。

    我总结了大概这几方面:对象在内存中的创建、对象在内存中的布局、对象在内存中的访问定位

    下面分别展开详述

    1、对象在内存中的创建

    对象在内存中创建到底是怎么样一个过程呢?大致有以下几个步骤:

    (1)虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用

    (2)判断这个类是否已被加载、解析和初始化

         如果没有,则必须进行相应的加载过程

    (3)为新生对象在Java堆中分配内存空间

         分配内存有两种方式:指针碰撞和空闲列表

         指针碰撞是指:假设Java堆中内存绝对规整,所有已使用的内存都放在一边,空闲的内存都放在另一边,中间放着一个指针作为分界点,那这时候分配内存就仅仅是把这个指针向空闲的那边挪动一段(挪动的大小就是需要分配的对象的大小),这种分配方式就称为“指针碰撞”。

         空闲列表是指:如果Java堆内存并不是规整的,已经使用的内存和空闲的内存相互交错,那肯定就没办法使用指针碰撞的方式进行内存分配了,这时候虚拟机就必须维护一个列表来记录哪些内存块儿是可用的,然后在需要进行内存分配的时候就从列表中找到一块儿足够大的内存划分给对象,并且更新列表上的记录,这种分配方式称之为“空闲列表”。

         到底用哪种方式进行分配是由堆内存是否规整决定的,而堆内存是否规整又是由你具体使用的哪种垃圾回收器决定的,

    如果你使用的是“标记-清除”这种类型的垃圾回收器,那么会导致堆内存不规则产生内存碎片,适合使用空闲列表的方式;

    如果你使用的是“标记-整理(压缩)”这种类型的垃圾回收器,适合使用指针碰撞的方式。

    除了讨论使用哪种方式进行内存分配外,还有一个问题需要考虑:对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针指向的位置(指针碰撞的方式)或者找到空闲空间给对象分配,并更新列表(空闲列表的方式),在并发情况下也并不是安全的,因为上述的操作并不能保证其原子性,很可能出现不同的对象申请到同一块内存的情况

    解决这个问题有几种方案:基于硬件指令的CAS方式来保证操作的原子性或者使用TLAB的方式,再或者可以使用栈上分配

    栈上分配和TLAB的具体内容本篇不展开讨论,具体内容可参考我之前写的另一篇:用大白话来聊一聊Java对象的栈上分配和TLAB

    (4)内存分配完之后,虚拟机需要将分配到的内存空间都初始化为零值

             比如int 型的零值是0,布尔的零值是false,引用数据类型零值是null等等

    (5)设置对象头相关数据:GC分代年龄、对象的哈希码 hashCode、元数据信息等等

    (6)执行<init>方法

            执行<init>方法包括但不仅限于:构造代码块儿、构造函数,具体步骤如下

       1)父类静态变量,父类静态代码块执行初始化
       2)子类静态变量,子类静态代码块执行初始化 
       3)父类全局变量,父类构造代码块执行初始化
       4)父类构造函数执行 
       5)子类全局变量,子类构造代码块执行初始化 
       6)子类构造函数执行 

    上述就是针对对象创建底层步骤的一些总结

    对象在内存里创建出来后,那创建完的对象在内存中是什么样的?对象里都包括哪些内容?

    2、对象在内存中的布局

    在虚拟机中,对象在内存中的存储布局可分为三块:对象头、实例数据和对齐填充

    (1)对象头:对象头用于存储对象的元数据信息

    对象头又可以分为两块内容:第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别位32bit和64bit,官方称它为 Mark Word。对象头的另一部分是类型指针,指向它的类元数据的指针,用于判断对象属于哪个类的实例,另外,如果对像是一个数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

    !!!注意:对象头这一块很重要,它是实现synchronized锁的基础,也是后续偏向锁,轻量级锁,自旋锁等锁优化,锁升级的基础,关于锁的介绍,可以参考我写的另一篇文章:Synchronized、偏向锁、自旋锁、轻量级锁以及锁的升级过程

    (2)实例数据

    实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。父类定义的变量会出现在子类定义的变量的前面。各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。

    (3)对齐填充

    对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。为什么需要有对齐填充呢?由于hotspot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数。因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

    再给个图帮助理解记忆

    以上就是对象在内存中布局的一些总结

    对象创建好了,也知道创建的对象在内存中存储的布局结构了

    咱们创建对象肯定是为了访问它,使用它,那怎么访问它呢?

    3、对象在内存中的访问定位

    建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位访问堆中的对象的具体位置,所以对象的访问方式取决于具体的虚拟机实现而定。目前主流的访问方式有使用句柄和直接指针两种。

    (1)句柄的访问方式

    如图所示,如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,句柄池中放的是一个一个的句柄,句柄中存的是对象实例数据与对象类型数据的指针引用。栈中局部变量里reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针

    (2)直接指针的访问方式

    如图所示,直接指针访问对象,栈中局部变量里reference中存储的就是对象地址,相当于一级指针

    (3)对比

    这两种对象访问方式各有利弊,使用句柄访问的最大好处就是在移动对象时(如垃圾回收的标记整理算法在回收完垃圾对象后需要把剩下存活的对象进行整理移动,以减少内存碎片),reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;但是如果使用直接指针方式的话,在对象被移动的时候需要修改reference中存储的地址。从效率方面比较的话,直接指针的效率要高于句柄,因为直接指针的方式只进行了一次指针定位,节省了时间开销,HotSpot采用的直接指针的实现方式。

    上述就是对Java对象的访问定位的理解与总结

    咱们说完了对象在内存中分配的以及访问的具体细节后,下面从宏观上来说一下对象分配的几个特点

    4、对象分配的几个特点

    对象的分配有以下几个特点

    (1)对象优先分配在eden区

    (2)大对象直接进去老年代,这个阈值可以自定义

    (3)年长的对象进去老年代,默认的年长值是15

    (4)动态对象年龄判断

    以上几点的具体内容也可参考《深入理解JVM虚拟机》一书

    OK,今天就先聊到这里

    铁子们,如果觉得文章对你有所帮助,可以点关注,点赞

    也可以关注下公众号:扫码或 wx搜索:“聊5毛钱的java”,欢迎一起学习交流,关注公众号可领取博主的Java学习视频+资料,保证都是干货

    3Q~

    展开全文
  • java对象与对象引用变量

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

    Java对象及其引用

    先搞清楚什么是堆,什么是栈。 
    Java开辟了两类存储区域,对比二者的特点

    存储区域 存储内容 优点 缺点 回收
    基本类型的变量和对象的引用变量 存取速度比堆要快,仅次于寄存器,栈数据可以共享 存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量 当超过变量的作用域后,Java会自动释放掉该变量,内存空间可以立即被另作他用
    由new等指令创建的对象和数组 可以动态地分配内存大小,生存期也不必事先告诉编译器 由于要在运行时动态分配内存,存取速度较慢 由Java虚拟机的自动垃圾回收器来回收不再使用的数据

    堆栈的存储特点决定了其中存储的数据类型。

    注意,栈内存储的除了基本类型的变量(int 这种类型的变量)还会存储对象的引用变量。java中,引用变量实际上是一个指针,它指向的是堆内存中对象实例。

    引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

     

     

    以下分解成了四个步骤。

    Case cc; '''在栈内存里面开辟了空间给引用变量cc,这时cc=null'''
    cc=new Case();
    '''
    1. new Case()在堆内存里面开辟了空间给Case类的对象,这个对象没有名字
    2. Case()随即调用了Case类的构造函数
    3. 把对象的地址在堆内存的地址给引用变量cc
    '''

    这样我们就明确了:

    • Java中,这里的“=”并不是赋值的意思,而是把对象的地址传递给变量;
    • 对象创建出来,其实连名字都没有,因此必须通过引用变量来对其进行操作。

    为了形象地说明对象、引用及它们之间的关系,可以做一个或许不很妥当的比喻。对象好比是一只很大的气球,大到我们抓不住它。引用变量是一根绳, 可以用来系汽球

    关于对象与引用之间的一些基本概念。

           初学Java时,在很长一段时间里,总觉得基本概念很模糊。后来才知道,在许多Java书中,把对象和对象的引用混为一谈。可是,如果我分不清对象与对象引用,

           那实在没法很好地理解下面的面向对象技术。把自己的一点认识写下来,或许能让初学Java的朋友们少走一点弯路。

           为便于说明,我们先定义一个简单的类:

           class Vehicle {

           int passengers;      

           int fuelcap;

           int mpg;

                       }

    有了这个模板,就可以用它来创建对象:

           Vehicle veh1 = new Vehicle();

    通常把这条语句的动作称之为创建一个对象,其实,它包含了四个动作。

    1)右边的“new Vehicle”,是以Vehicle类为模板,在堆空间里创建一个Vehicle类对象(也简称为Vehicle对象)。

    2)末尾的()意味着,在对象创建后,立即调用Vehicle类的构造函数,对刚生成的对象进行初始化。构造函数是肯定有的。如果你没写,Java会给你补上一个默认的构造函数。

    3)左边的“Vehicle veh 1”创建了一个Vehicle类引用变量。所谓Vehicle类引用,就是以后可以用来指向Vehicle对象的对象引用。

    4)“=”操作符使对象引用指向刚创建的那个Vehicle对象。

    我们可以把这条语句拆成两部分:

    Vehicle veh1;

    veh1 = new Vehicle();

    效果是一样的。这样写,就比较清楚了,有两个实体:一是对象引用变量,一是对象本身。

           在堆空间里创建的实体,与在数据段以及栈空间里创建的实体不同。尽管它们也是确确实实存在的实体,但是,我们看不见,也摸不着。不仅如此,

           我们仔细研究一下第二句,找找刚创建的对象叫什么名字?有人说,它叫“Vehicle”。不对,“Vehicle”是类(对象的创建模板)的名字。

           一个Vehicle类可以据此创建出无数个对象,这些对象不可能全叫“Vehicle”。

           对象连名都没有,没法直接访问它。我们只能通过对象引用来间接访问对象。

           为了形象地说明对象、引用及它们之间的关系,可以做一个或许不很妥当的比喻。对象好比是一只很大的气球,大到我们抓不住它。引用变量是一根绳, 可以用来系汽球。

           如果只执行了第一条语句,还没执行第二条,此时创建的引用变量veh1还没指向任何一个对象,它的值是null。引用变量可以指向某个对象,或者为null。

           它是一根绳,一根还没有系上任何一个汽球的绳。执行了第二句后,一只新汽球做出来了,并被系在veh1这根绳上。我们抓住这根绳,就等于抓住了那只汽球。

           再来一句:

           Vehicle veh2;

    就又做了一根绳,还没系上汽球。如果再加一句:

           veh2 = veh1;

    系上了。这里,发生了复制行为。但是,要说明的是,对象本身并没有被复制,被复制的只是对象引用。结果是,veh2也指向了veh1所指向的对象。两根绳系的是同一只汽球。

           如果用下句再创建一个对象:

    veh2 = new Vehicle();

    则引用变量veh2改指向第二个对象。

           从以上叙述再推演下去,我们可以获得以下结论:

    (1)一个对象引用可以指向0个或1个对象(一根绳子可以不系汽球,也可以系一个汽球);

    (2)一个对象可以有N个引用指向它(可以有N条绳子系住一个汽球)。

           如果再来下面语句:

           veh1 = veh2;

    按上面的推断,veh1也指向了第二个对象。这个没问题。问题是第一个对象呢?没有一条绳子系住它,它飞了。多数书里说,它被Java的垃圾回收机制回收了。

    这不确切。正确地说,它已成为垃圾回收机制的处理对象。至于什么时候真正被回收,那要看垃圾回收机制的心情了。

           由此看来,下面的语句应该不合法吧?至少是没用的吧?

    new Vehicle();

    不对。它是合法的,而且可用的。譬如,如果我们仅仅为了打印而生成一个对象,就不需要用引用变量来系住它。最常见的就是打印字符串:

        System.out.println(“I am Java!”);

    字符串对象“I am Java!”在打印后即被丢弃。有人把这种对象称之为临时对象。

           对象与引用的关系将持续到对象回收

    Java在运行时才处理别名引用

    欢迎关注下方公众号,一起交流,期待你的到来

    展开全文
  • JSONObject jo= (JSONObject) JSONObject.toJSON(javaBean); Student stu = (Student)JSONObject.toBean(jo, Student.class);
  • java对象转JSONObject

    万次阅读 2018-12-21 14:06:20
    首先导包 两个 import com.alibaba.fastjson.JSONObject; ... Gson g = new Gson();...Notice notice = g.fromJson(s, Notice.class); //json字符串转java对象 需要的是Gson /*这里需要强转一下 默认是Obje...
  • java对象转js对象

    千次阅读 2017-05-04 20:51:04
    在js中直接使用 EL表达式表达java对象时,输出是对象的类名。没有达到我们要使用该对象的目的。比如var user= ${user};在页面上查询代码为var user=com.test.domain.user;这时候想要获取user的属性值: user.name是会...
  • 什么是Java的对象引用? Java中都有哪些类型的对象引用? Java中提供的Java对象引用主要有什么目的? 通过本文,你就能很清楚得了解Java的对象引用
  • json 和java对象 互相转换(java)

    万次阅读 2018-08-29 19:25:33
    java 解析json  1. 使用 alibaba 的 fastJson 添加依赖或者jar ... Java对象 java对象 &gt; json w3c的 API 连接:https://www.w3cschool.cn/fastjson/fastjson-api.html public class MyJson { p...
  • 主要是实现复杂的嵌套的Java对象,也就是对象嵌套对象的复杂对象,转换成json字符串。然后就是反过来,把复杂的json字符串转换成对应的嵌套的Java对象。 先上工具类。如下。 package com.lxk.json; import ...
  • Java对象: List Map Person类(Java对象的JavaBean) 定义Person类的成员变量 1.List类型对象转为JSON数据: Java对象转换为JSON数据的步骤: 1声明Person类的两个对象,person1及person2,并封装成员变量...
  • JAVA对象模型

    千次阅读 2018-06-09 22:16:30
    现在我们来好好的看看Java对象模型: 几乎所有的Java对象保存在堆内存中(有例外,自行了解),在内存中Java对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态...
  • Java对象转Map

    万次阅读 2019-06-04 09:46:18
    使用org.apache.commons.beanutils包下的BeanUtils.describe()方法可以将Java对象按属性转为Map。 import org.apache.commons.beanutils.BeanUtils; class CommonUtils{ public static Map<String, Object>...
  • 在大量的应用场景中,我们需要使用redis存取java对象。redis存取对象需要将对象序列化。 序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或...
  • Java对象的序列化(Serialization)和反序列化详解

    万次阅读 多人点赞 2018-02-13 15:56:02
    1.序列化和反序列化 序列化(Serialization)是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象...2.Java对象的序列化和反序列化 在Java中,我们可以通过多种方式来创建对象,并且只要对象...
  • JAVA 对象头解析

    千次阅读 2019-06-12 14:44:34
    一个Java对象在JVM中是由一个对应角色的oop对象来描述的, 比如instanceOopDesc用来描述普通实例对象,arrayOopDesc用来描述数组对象,而这些类型的oop对象均是继承自oopDesc。 class oopDesc { friend class ...
  • 下面我们详细了解Java程序中new一个普通对象时,HotSpot虚拟机是怎么样创建这个对象的,包括5个步骤:相应类加载检查过程、在Java堆中为对象分配内存、分配后内存初始...最后我们再从JVM指令角度来解释下Java对象创建。
  • Java 对象的生命周期

    万次阅读 2014-08-05 16:21:24
    Java ... 在Java中,对象的生命周期包括以下几个阶段: 1.创建阶段(Created) 2.应用阶段(In Use) 3.不可见阶段(Invisible) 4.不可达阶段(Unreachable) 5.收集阶段(Collected) 6.终结阶段(Finalized) 7.
  • 深入理解Java对象的创建过程:类的初始化与实例化

    万次阅读 多人点赞 2017-05-18 14:17:45
    在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。...本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程。
  • java对象头信息

    千次阅读 多人点赞 2019-09-02 14:27:14
    1. 一个java对象到底占用了多少内存空间,应该如何计算? 2. 为什么在jdk1.6后,synchronized关键字性能有所提高,为什么会提高?并且很多文章中都说synchronized锁有偏向锁、轻量锁、重量锁等状态? 3. java对象...
  • 1. 首先是Java对象转Json字符串.需要jar包.五个.分别如下:commons-beanutils-1.8.3.jarcommons-collections-3.2.1.jarcommons-lang-2.6.jarcommons-logging-1.1.1.jarezmorph-1.0.6.jarjson-lib-2.4-jdk15.jar先定义...
  • 在前面两篇文章中了解到Java对象实例是如何在HotSpot虚拟机的Java堆中创建的,以及创建后的内存布局是怎样的。下面详细了解在Java堆中的Java对象是如何访问定位的:先来了解reference类型数据是什么,再来了解两种...
  • java 普通对象结构 java 数组对象结构 对象结构组成 对象头 HotSpot虚拟机的对象头包括两部分信息: Mark Word 第一部分Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志...
  • Java对象创建的流程

    千次阅读 2019-05-07 22:05:39
    Java对象创建的流程 文章目录Java对象创建的流程1.Java普通对象的创建1.1new指令1.2分配内存1.3初始化1.4对象的初始设置1.5\方法2.Java对象内存布局2.1对象头2.2实例数据2.3对齐填充 1.Java普通对象的创建 这里讨论...
  • Java对象持久化

    千次阅读 2018-07-19 12:14:17
    在JAVA中,我们可以把JAVA对象直接保存在文件中,在需要使用的时候,直接从文件中读取,这也是对象持久化的一种方式,在这一篇博客中,将演示两种将JAVA对象持久化到文件的方法,直接上代码: import java.beans....
  • java对象头 MarkWord

    千次阅读 2019-06-05 20:41:15
    原文链接:[https://blog.csdn.net/scdn_cp/article/details/86491792#comments] 我们都知道,Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节...
  • Java对象转换为Json对象

    万次阅读 2018-05-24 10:47:02
    JSONObject json = JSONObject.fromObject(userInfo);//将java对象转换为json对象 String str = json.toString();//将json对象转换为字符串
  • Java对象序列化详解

    万次阅读 多人点赞 2016-08-10 14:47:32
    一、定义 序列化:把Java对象转换为字节序列的过程。    反序列化:把字节序列恢复为Java对象的过程。二、用途 对象的序列化主要有两种用途:    1) 把对象的字节序列永久地保存到硬盘上,通常存放在一...
  • redis存储java对象

    万次阅读 2018-01-02 20:22:40
    1、redis要存储java对象,首先要将java对象序列化 public class Person implements Serializable { private int id; private String name; public Person(int id, String name) { this.id = id; this.name ...
  • testJson_1中 将不同的java对象转换为json格式文件(String),  testJson_2中传入testJson_1中生成的JSON String 参数,反向返回各种Java的对象 包括:java的String、List、HashMap、JavaBean, 以及对象内包含...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 695,110
精华内容 278,044
关键字:

java对象

java 订阅