基礎篇: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的實例數據內存使用三部分組成的,對象頭,實際數據區(qū)域、內存對齊區(qū)
  • 對象頭布局如下:主要和鎖,hashcode,垃圾回收有關;由于鎖機制的內容篇幅過長,這里就不多解釋了;和鎖相關的markWord(markOop)內存布局如下


    image
  • 內存對齊區(qū)是什么? HotSpot VM的自動內存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數倍,換句話說就是對象的大小必須是8字節(jié)的整數倍。因此當對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。
  • 內存對齊好處
    • 有利于內存的管理
    • 更快的CPU讀取,CPU從內存獲取數據,并不是一個個字節(jié)的讀取,而是按CPU能處理的長度獲取,如32位機,是4個字節(jié)的內存塊;當只需其中兩個字節(jié)時,則由內存處理器處理挑選。如果需要三個字節(jié)分布在兩個不同內存塊(四字節(jié)的內存塊),則需要讀取內存兩次(如果是存在同一內存塊只需一次讀?。?。而當對象按一定的規(guī)則合理對齊時,CPU就可以最少地請求內存,加快CPU的執(zhí)行速度
  • 指針壓縮
    • 在上圖可以看到,在64位jvm里Object的MarkWord會比32位的大一倍;其實klassOop也擴大一倍占了64位(數組長度部分則是固定四字節(jié))。指針的寬度增大,但是對于堆內存小于4G的,好像也用不到64位的指針。這可以優(yōu)化嗎?答案是就是指針壓縮
    • 指針壓縮的原理是利用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字節(jié)的倍數,可以利用這點提升選址范圍,理論上可以提升到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提供了常用的幾個靜態(tài)方法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,拋出的異常也不能比父類方法定義的更廣
  • 方法的重載則是同一個類中定義和已有方法的名稱一致而參數或者參數順序不一致的方法,(返回值不能決定方法的重載)
  • 重載的方法在編譯時就可確定(編譯時多態(tài)),而重寫的方法需要在運行時確定(運行時多態(tài),我們常說的多態(tài))

    多態(tài)的三個必要條件 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規(guī)范的;而且當用HashMap存儲時,可能會存在多個我們自定義認為相同的對象,這樣會為我們代碼邏輯埋下坑。

8 Object.wait和Thread.sheep

Object.wait是需要在synchronized修飾的代碼內使用,會讓出CPU,并放棄對對象鎖的持有狀態(tài)。而Thread.sleep則簡單的掛起,讓出CPU,沒有釋放任何鎖資源

9 finalize方法的使用

  • 如果對象重寫了finalize方法,jvm會把當前對象注冊到FinalizerThread的ReferenceQueue隊列中。對象沒有其他強引用被當垃圾回收時,jvm會判斷ReferenceQueue存在該對象,則暫時不回收。之后FinalizerThread(獨立于垃圾回收線程)從ReferenceQueue取出該對象,執(zhí)行自定義的finalize方法,結束之后并從隊列移除該對象,以便被下次垃圾回收
  • finalize會造成對象延后回收,可能導致內存溢出,慎用
  • finally和finalize區(qū)別
    • finally是java關鍵字,用來處理異常的,和try搭配使用
    • 如果在finally之前return,finally的代碼塊會執(zhí)行嗎?
      try內的continue,break,return都不能繞過finally代碼塊的執(zhí)行,try結束之后finally是一定會被執(zhí)行的
  • 相似的關鍵字final
    • final修飾類,該類不能被繼承;修飾方法,方法不能被重寫;修飾變量,變量不能指向新的值;修飾數組,數組引用不能指向新數組,但是數組元素可以更改
    • 如果對象被final修飾,變量有哪幾種聲明賦值方式?
    • fianl修飾普通變量:1、定義時聲明 2、類內代碼塊聲明 3、構造器聲明
    • fianl修飾靜態(tài)變量:1、定義時聲明 2、類內靜態(tài)代碼塊聲明

10 創(chuàng)建對象有哪幾種方法

  • 1、使用new創(chuàng)建
  • 2、運用反射獲取Class,在newInstance()
  • 3、調用對象的clone()方法
  • 4、通過反序列化得到,如:ObjectInputStream.readObject()

11 猜猜創(chuàng)建對象的數量

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

    兩個對象和一個棧變量:一個棧變量one和一個new String()實例對象、一個"hello"字符串對象
image
  • 題外話:string.intern();intern先判斷常量池是否存相同字符串,存在則返回該引用;否則在常量池中記錄堆中首次出現該字符串的引用,并返回該引用。

    如果是先執(zhí)行 String s = "hello" ;相當于執(zhí)行了intern();先在常量池創(chuàng)建"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());
            }
        }
    

參考文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容