基础篇Object对象

43次阅读

共计 7659 个字符,预计需要花费 20 分钟才能阅读完成。

1 Object 的内存构造和指针压缩理解一下

//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)内存布局如下

  • 内存对齐 区是什么? 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]; // 24B   敞开压缩则是 16B
    }
    
    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
<br/>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) 办法
    <br/>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, 抛出的 异样 也不能比父类办法定义的更广
  • 办法的 重载 则是同一个类中定义和已有办法的 名称统一而参数或者参数程序不统一 的办法,(返回值不能决定办法的重载)
  • 重载的办法在编译时就可确定(编译时多态),而重写的办法须要在运行时确定(运行时多态,咱们常说的多态)
    <br/> 多态的三个必要条件 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 的代码块会执行吗?<br/>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");

<br/> 两个对象 和一个 栈变量 :一个栈变量 one 和一个 new String() 实例对象、一个 ”hello” 字符串对象

  • 题外话:string.intern();intern 先判断常量池是否存雷同字符串, 存在则返回该援用;否则在常量池中记录堆中首次呈现该字符串的援用,并返回该援用。

<br/> 如果是先执行 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 援用雷同

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());
            }
        }

关注公众号,大家一起交换

参考文章

  • Object 类中的 registerNatives
  • 在 java 中为什么不举荐应用 finalize
  • 一个 Java 对象到底占用多大内存?
  • java 对象在内存的大小
  • 为什么须要内存对齐以及对齐规定的简略剖析
  • JVM – 分析 JAVA 对象头 OBJECT HEADER 之指针压缩
  • 几张图轻松了解 String.intern()

正文完
 0