基础篇:JAVA - Object对象

1 Object的内存结构和指针压缩了解一下

image
//hotspot的oop.hpp文件中class oopDesc
class oopDesc {
  friend class VMStructs;
  private:
  volatile markOop  _mark; //对象头部分
  union _metadata {  // klassOop 类元数据指针
    Klass*      _klass;   
    narrowKlass _compressed_klass;
  } _metadata;
  • Object的实例数据内存使用三部分组成的,对象头实际数据区域内存对齐区
  • 对象头布局如下:主要和锁,hashcode,垃圾回收有关;由于锁机制的内容篇幅过长,这里就不多解释了;和锁相关的markWord(markOop)内存布局如下


    image
  • 内存对齐区是什么? HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
  • 内存对齐好处
    • 有利于内存的管理
    • 更快的CPU读取,CPU从内存获取数据,并不是一个个字节的读取,而是按CPU能处理的长度获取,如32位机,是4个字节的内存块;当只需其中两个字节时,则由内存处理器处理挑选。如果需要三个字节分布在两个不同内存块(四字节的内存块),则需要读取内存两次(如果是存在同一内存块只需一次读取)。而当对象按一定的规则合理对齐时,CPU就可以最少地请求内存,加快CPU的执行速度
  • 指针压缩
    • 在上图可以看到,在64位jvm里Object的MarkWord会比32位的大一倍;其实klassOop也扩大一倍占了64位(数组长度部分则是固定四字节)。指针的宽度增大,但是对于堆内存小于4G的,好像也用不到64位的指针。这可以优化吗?答案是就是指针压缩
    • 指针压缩的原理是利用jvm植入压缩指令,进行编码、解码
    • 哪些信息会被压缩
      • 会被压缩对象:类属性、对象头信息、对象引用类型、对象数组类型
      • 不被压缩对象:本地变量,堆栈元素,入参,返回值,NULL这些指针
    • 指针压缩开启,klassOop大小可以由64bit变成32bit;对象的大小可以看看下面的具体对比:JVM - 剖析JAVA对象头OBJECT HEADER之指针压缩
    public static void main(String[] args){
        Object a = new Object(); // 16B   关闭压缩还是16B,需要是8B倍数;12B+填充的4B
        int[] arr = new int[10]; // 16B   关闭压缩则是24B
    }
    
    public class ObjectNum {
        //8B mark word
        //4B Klass Pointer   如果关闭压缩则占用8B
        //-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,
        int id;        //4B
        String name;   //4B  如果关闭压缩则占用8B
        byte b;        //1B  实际内存可能会填充到4B
        Object o;      //4B  如果关闭压缩则占用8B
    }
    
    • 为什么开启指针压缩时,堆内存最好不要超过32G,指针使用32个bit,为什么最大可使用内存不是4G而是32G

      jvm要求对象起始位置对齐8字节的倍数,可以利用这点提升选址范围,理论上可以提升到2^11 * 4G。不过jvm只是将指针左移三位,因此2^3 * 4G = 32G。如果大于32G,指针压缩会失效。如果GC堆大小在 4G以下,直接砍掉高32位,避免了编码解码过程
    • 启用指针压缩-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops

2 Object的几种基本方法

  • 本地方法
    • private static native void registerNatives() 将Object定义的本地方法和java程序链接起来。Object类中的registerNatives
    • public final native Class<?> getClass() 获取java的Class元数据
    • public native int hashCode() 获取对象的哈希Code
    • protected native Object clone() throws CloneNotSupportedException 获得对象的克隆对象,浅复制
    • public final native void notify() 唤醒等待对象锁waitSet队列中的一个线程
    • public final native void notifyAll() 类似notify(),唤醒等待对象锁waitSet队列中的全部线程
    • public final native void wait(long timeout) 释放对象锁,进入对象锁的waitSet队列
  • 普通方法
    public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
    public boolean equals(Object obj) { return (this == obj);}
    public final void wait(long timeout, int nanos) throws InterruptedException;
    //都是基于native void wait(long timeout)实现的
    public final void wait() throws InterruptedException;
    wait(long timeout, int nanos)、wait() 
    //jvm回收对象前,会特意调用此方法 
    protected void finalize() throws Throwable; 
    

3 == 、 equals、Comparable.compareTo、Comparator.compara 四种比较方法

如不指定排序顺序,java里的默认排序顺序是升序的,从小到大

  • ==, (A)对于基本类型之间的比较是值 (B)基本类型和封装类型比较也是值比较 (C)对于引用类型之间的比较则是内存地址
  • equals(Object o), 在Object基本方法里可以看到public boolean equals(Object obj) { return (this == obj);} 是使用 == 去比较的。equals方法的好处是我们可以重写该方法
  • Comparable.compareTo 是接口Comparable里的抽象方法;如果对象实现该接口,可使用Collections.sort(List< T> col)进行排序。接下来看看源码怎么实现的
    Collections.java
    //Collections.sort(List<T> list),调用的是List的sort方法
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }
    
    List的sort 则调用了Arrays.sort
    List.java
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    
    如果Comparator c 为null,则是调用 Arrays.sort(Object[] a) ;最终调用LegacyMergeSort(归并排序)方法处理
    Arrays.java
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
    
    LegacyMergeSort方法里的一段代码;最终底层是使用归并排序和compareTo来排序
    Arrays.java
    ......
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
                         ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }
    
  • Comparator也是一个接口,不过提供了更丰富的操作,需要实现int compare(T o1, T o2)方法

    Comparator提供了常用的几个静态方法thenComparing、reversed、reverseOrder(操作对象需要实现Comparator或者Comparable);可配合List.sort、Stream.sorted、Collections.sort使用。
    @Data
    @AllArgsConstructor
    static class Pair implements Comparator<Pair>, Comparable<Pair> {
        Integer one;
        Integer two;
        @Override
        public String toString() { return one + "-" + two; }
        @Override
        public int compareTo(Pair o) { return one.compareTo(o.one);  }
        @Override
        public int compare(Pair o1, Pair o2) {return o1.compareTo(o2);}
    }
    public static void main(String[] args) {
        List<Pair> col = Arrays.asList( new Pair(4, 6), new Pair(4, 2),new Pair(1, 3));
        col.sort(Comparator.reverseOrder());
        System.out.println("----------------");
        col.stream().sorted(Comparator.comparing(Pair::getOne).thenComparing(Pair::getTwo))
                .forEach(item ->  System.out.println(item.toString()) );
    }
    
    Collections.sort默认是升序排序的,可以看到reverseOrder将顺序反过来了; 用了thenComparing的col则是先判断Pair::getOne的大小,如果相等则判断Pair::getTwo大小来排序
    result:
    4-6
    4-2
    1-3
    ----------------
    1-3
    4-2
    4-6
    

4 方法的重写和重载

  • 方法的重写是指子类定义和父类方法的名称、参数及顺序一致的方法;需要注意的是,子类重写方法修饰符不能更加严格,就是说父类方法的修饰符是protected,子类不能使用private修饰而可用public,抛出的异常也不能比父类方法定义的更广
  • 方法的重载则是同一个类中定义和已有方法的名称一致而参数或者参数顺序不一致的方法,(返回值不能决定方法的重载)
  • 重载的方法在编译时就可确定(编译时多态),而重写的方法需要在运行时确定(运行时多态,我们常说的多态)

    多态的三个必要条件 1、有继承关系 2、子类重写父类方法 3、父类引用指向子类对象

5 构造方法是否可被重写

构造方法是每一个类独有的,并不能被子类继承,因为构造方法没有返回值,子类定义不了和父类的构造方法一样的方法。但是在同一个类中,构造方法可以重载

public class TestEquals {
    int i;
    public TestEquals() {   i = 0; }
    //构造方法重载
    public TestEquals(int i) {   this.i = i } 
}

6 Object的equals和hashCode

equals是用来比较两个对象是否相等的,可以重写该方法来实现自定义的比较方法;而hashCode则是用来获取对象的哈希值,也可以重写该方法。当对象存储在Map时,是首先利用Object.hashCode判断是否映射在同一位置,若在同一映射位,则再使用equals比较两个对象是否相同。

7 equals一样,hashCode不一样有什么问题?

如果重写equals导致对象比较相同而hashCode不一样,是违反JDK规范的;而且当用HashMap存储时,可能会存在多个我们自定义认为相同的对象,这样会为我们代码逻辑埋下坑。

8 Object.wait和Thread.sheep

Object.wait是需要在synchronized修饰的代码内使用,会让出CPU,并放弃对对象锁的持有状态。而Thread.sleep则简单的挂起,让出CPU,没有释放任何锁资源

9 finalize方法的使用

  • 如果对象重写了finalize方法,jvm会把当前对象注册到FinalizerThread的ReferenceQueue队列中。对象没有其他强引用被当垃圾回收时,jvm会判断ReferenceQueue存在该对象,则暂时不回收。之后FinalizerThread(独立于垃圾回收线程)从ReferenceQueue取出该对象,执行自定义的finalize方法,结束之后并从队列移除该对象,以便被下次垃圾回收
  • finalize会造成对象延后回收,可能导致内存溢出,慎用
  • finally和finalize区别
    • finally是java关键字,用来处理异常的,和try搭配使用
    • 如果在finally之前return,finally的代码块会执行吗?
      try内的continue,break,return都不能绕过finally代码块的执行,try结束之后finally是一定会被执行的
  • 相似的关键字final
    • final修饰类,该类不能被继承;修饰方法,方法不能被重写;修饰变量,变量不能指向新的值;修饰数组,数组引用不能指向新数组,但是数组元素可以更改
    • 如果对象被final修饰,变量有哪几种声明赋值方式?
    • fianl修饰普通变量:1、定义时声明 2、类内代码块声明 3、构造器声明
    • fianl修饰静态变量:1、定义时声明 2、类内静态代码块声明

10 创建对象有哪几种方法

  • 1、使用new创建
  • 2、运用反射获取Class,在newInstance()
  • 3、调用对象的clone()方法
  • 4、通过反序列化得到,如:ObjectInputStream.readObject()

11 猜猜创建对象的数量

  • String one = new String("Hello");

    两个对象和一个栈变量:一个栈变量one和一个new String()实例对象、一个"hello"字符串对象
image
  • 题外话:string.intern();intern先判断常量池是否存相同字符串,存在则返回该引用;否则在常量池中记录堆中首次出现该字符串的引用,并返回该引用。

    如果是先执行 String s = "hello" ;相当于执行了intern();先在常量池创建"hello",并且将引用A存入常量池,返回给s。此时String("hello").intern()会返回常量池的引用A返回
    String one = "hello";
    String two = new String("hello");
    String three = one.intern();
    System.out.println(two == one);
    System.out.println(three == one);
    
    result:
    false  // one虽然不等于two;但是它们具体的char[] value 还是指向同一块内存的
    true  // one 和 three 引用相同
image

12 对象拷贝问题

  • 引用对象的赋值复制是复制的引用对象,A a = new A(); A b = a;此时a和b指向同一块内存的对象
  • 使用Object.clone()方法,如果字段是值类型(基本类型)则是复制该值,如果是引用类型则复制对象的引用而并非对象
    @Getter
    static class A implements Cloneable{
        private B b; 
        private int index;
        public A(){
            b = new B(); index = 1000;
        }
        public A clone()throws CloneNotSupportedException{  return (A)super.clone(); }
    }
    static class B{
    }
    public static void main(String[] args) throws Exception{
        A a = new A();
        A copyA = a.clone();
        System.out.println( a.getIndex() == copyA.getIndex() );
        System.out.println( a.getB() == copyA.getB() );
    }
    
    //返回结果都是true,引用类型只是复制了引用值
    true
    true
    
  • 深复制:重写clone方法时使用序列化复制,(注意需要实现Cloneable,Serializable)
    public A clone() throws CloneNotSupportedException {
            try {
                ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(byteOut);
                out.writeObject(this);
                ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
                ObjectInputStream inputStream = new ObjectInputStream(byteIn);
                return (A) inputStream.readObject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new CloneNotSupportedException(e.getLocalizedMessage());
            }
        }
    

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267