常見踩坑案例(一) subList引起FULLGC


計划真的趕不上變化,時間過得真快。廢話不多說了,今天主要記錄之前有同事遇到的一些坑分享出來。

一、封裝類的應用會引起NPE異常

  對於其他對象的應用,一般在使用之前會判斷它是否為空,如果不為空才會使用它以及它里面的一些屬性值。但是對於基本類型的封裝類型,就有很多人漏掉對於它的判斷。

  就在前面幾天有位同事問我說這段代碼它怎么會報空指針呢? 先模擬下這個場景下的代碼:

// 第三方的實體類對象
class Count{
    Integer total;
    String name;
    String leavingNum;
    //...
}

public class DemoTest {

    public void fun() {
        Count count = new Count();
        // 經過一系類對應值的獲取之后
        // 需要判斷total是否為0,如果為0就進行其他的業務操作。
        if (count.total == 0) {
            // 比方說打印日志。
        }
    }

    @Test
    public void test() {
        fun();
    }
}

  他給我看的代碼就是類似fun()方法中的代碼, 我一看這個代碼就感覺不對勁,我說你這個Count類下的total屬性是int嗎?如果是int就可以這么用。他說為什么? “如果是Integer類型,那么它的初始值不是0而是null, 而你上面的這些邏輯又不能保證total 一定會獲取到數值,那么它就還有可能是null,你這樣使用的話就有可能包NPE的問題。所以針對對象的使用提前判空更有保障。” 我接着說。 在我說的過程中他反應還是很快的,立馬查看了這個實體類中total字段的類型,於是就明白了。 如果沒有養成提前使用判空的習慣(除非你能保證一定會有值),老手都容易會踩這樣的坑。比方說針對Boolean類型的使用,有很多人會直接這樣的哦(這樣肯定會有問題的)。

        Boolean flag = null;
        // 經過一系類操作處理
        // 進行判斷
        if (flag) {
          //......  
        }

  另外針對基本類型的封裝類型使用還有些要注意的請看這篇文章:https://www.cnblogs.com/yuanfy008/p/8321217.html

二、subList帶來的隱患

  現在有很多都是基於分布式服務,那應該會存在這個域對應數據需要同步到其他域下,然后這種同步必然會產生差異,需要一種自檢的job去檢測差異。打個比方有些商家自己有官網售賣自己的產品,也還有可能會在天貓開旗艦店售賣。假如它分配在天貓的商品信息是通過它本地天貓數據庫同步過去的,那么這種難免會產生差異,特別是庫存,如果一邊多一邊少就可能會導致超賣的情況。 所以這種情況需要有個job對比兩邊的差異,下面先簡單模擬下事發情況(注意下面的用法):

 1 public class SubListTest {
 2 
 3     @Test
 4     public void test1() {
 5         // 初始集合(有序)
 6         List<Integer> list = new ArrayList<>();
 7         list.addAll(Arrays.asList(1, 2, 3, 4));
 8 
 9         // 業務場景:需要將list集合與很多場景下的數據進行對比,然后取出不同的。
10         // 對比的場景就不還原了,假設每次都是前面兩個不同。 這里只列舉四次對比,為了方便查看效果不使用for循環
11 
12         // 第一次截取不同的數據
13         list = list.subList(0, 2);
14         // 查看list中有多少數據
15         System.out.println(list);
16 
17         // 查詢有新的數據,往list中添加
18         list.addAll(Arrays.asList(5, 6));
19         //...
20         // 第二次截取
21         list = list.subList(0, 2);
22         System.out.println(list);
23 
24         // 查詢有新的數據,往list中添加
25         list.addAll(Arrays.asList(7, 8));
26         //...
27         // 第三次截取
28         list = list.subList(0, 2);
29         System.out.println(list);
30 
31         // 查詢有新的數據,往list中添加
32         list.addAll(Arrays.asList(9, 10));
33         //...
34         // 第四次截取
35         list = list.subList(0, 2);
36         System.out.println(list);
37 
38     }
39 }

  看到上面代碼其實很簡單,輸出結果大家也都知道。下面先一步一步的分析,然后再介紹在大量數據的情況下這會產生結果。

  第一步:查看下list的內存分配地址,后面會有需要。

  第二步:跳到第一次截取之后,看list有什么變化? 

   它的內存地址變了,也就說每次subList都會產生一個新對象,那么得查看下這subList的源碼,而源碼中確實是會產生一個新對象。但是請仔細SubList的構造函數,其中會存放它的父級對象。那么這會產生什么影響呢?請接着往下看。

public List<E> subList(int fromIndex, int toIndex) {
       subListRangeCheck(fromIndex, toIndex, size);
       return new SubList(this, 0, fromIndex, toIndex);
}

private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;

        SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }
        // 后面方法省略  
}

  第三步:在第三次截取之前直接在源碼中SubList構造函數中打斷點,然后跳轉進來,看看對應對象的屬性值:

  咋一看,這里面怎么param_1怎么來的?通過第二步查看SubList的源碼再加上第一步的需要你留意list原生的的對象內存地址,你就知道param_1對應是這個list的根對象,它一直保留子對象新增的對象。那么大家想下,這種做法當遇到海量數據對比差異時會產生什么影響呢?

  如果剛才看懂了上面所說的,那么肯定會明白這個list的根對象累積到后面肯定會變成大對象,這樣會導致平凡的fullGc而且你還回收不掉。因為它一直在使用,直至這個程序運行結束。

  那么像上面這種場景怎么優化解決呢? 可以這樣考慮, 每次對比時候都弄一個新的list去獲取差異,然后再把這個有差異的list添加至總的差異結果集中。(其實我們平時也不會用一個list去反反復復的subList)。如果大家有更好的優化,請留言探討。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM