netty源碼分析(28)- PooledByteBufAllocator分析

上一節(jié)分析了UnpooledByteBufAllocator胚委,包括了堆內(nèi)堆外內(nèi)存是如何分配的士飒,底層時時如何獲取數(shù)據(jù)內(nèi)容的帘营。
本節(jié)分析分析PooledByteBufAllocator尉姨,看看它是怎么做Pooled類型的內(nèi)存管理的。

  • 入口PooledByteBufAllocator#newHeapBuffer()PooledByteBufAllocator#newDirectBuffer()拂檩,
    堆內(nèi)內(nèi)存和堆外內(nèi)存分配的模式都比較固定
  1. 拿到線程局部緩存PoolThreadCache
  2. 拿到不同類型的rena
  3. 使用不同類型的arena進行內(nèi)存分配
    @Override
    protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        //拿到線程局部緩存
        PoolThreadCache cache = threadCache.get();
        //拿到heapArena
        PoolArena<byte[]> heapArena = cache.heapArena;

        final ByteBuf buf;
        if (heapArena != null) {
            //使用heapArena分配內(nèi)存
            buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }

    @Override
    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        //拿到線程局部緩存
        PoolThreadCache cache = threadCache.get();
        //拿到directArena
        PoolArena<ByteBuffer> directArena = cache.directArena;

        final ByteBuf buf;
        if (directArena != null) {
            //使用directArena分配內(nèi)存
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }
  • 跟蹤threadCache.get()
    調(diào)用的是FastThreadLocal#get()方法侮腹。那么其實threadCache也是一個FastThreadLocal,可以看成是jdk的ThreadLocal,只不過還了一種跟家塊的是西安方法广恢。get方發(fā)住喲啊是調(diào)用了初始化方法initialize
    public final V get() {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }
        //調(diào)用初始化方法
        V value = initialize(threadLocalMap);
        registerCleaner(threadLocalMap);
        return value;
    }
private final PoolThreadLocalCache threadCache;

initialValue()方法的邏輯如下

  1. 從預(yù)先準備好的heapArenasdirectArenas中獲取最少使用的arena
  2. 使用獲取到的arean為參數(shù)凯旋,實例化一個PoolThreadCache并返回
    final class PoolThreadLocalCache extends FastThreadLocal<PoolThreadCache> {
        private final boolean useCacheForAllThreads;

        PoolThreadLocalCache(boolean useCacheForAllThreads) {
            this.useCacheForAllThreads = useCacheForAllThreads;
        }

        @Override
        protected synchronized PoolThreadCache initialValue() {
            /**
             * arena翻譯成競技場,關(guān)于內(nèi)存非配的邏輯都在這個競技場中進行分配
             */
            //獲取heapArena:從heapArenas堆內(nèi)競技場中拿出使用最少的一個arena
            final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
            //獲取directArena:從directArena堆內(nèi)競技場中拿出使用最少的一個arena
            final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);

            Thread current = Thread.currentThread();
            if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
                //創(chuàng)建PoolThreadCache:該Cache最終被一個線程使用
                //通過heapArena和directArena維護兩大塊內(nèi)存:堆和堆外內(nèi)存
                //通過tinyCacheSize钉迷,smallCacheSize,normalCacheSize維護ByteBuf緩存列表維護反復(fù)使用的內(nèi)存塊
                return new PoolThreadCache(
                        heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                        DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
            }
            // No caching so just use 0 as sizes.
            return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
        }

      //省略代碼......

      }

查看PoolThreadCache其維護了兩種類型的內(nèi)存分配策略钠署,一種是上述通過持有heapArenadirectArena糠聪,另一種是通過維護tiny,small,normal對應(yīng)的緩存列表來維護反復(fù)使用的內(nèi)存。

final class PoolThreadCache {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(PoolThreadCache.class);

    //通過arena的方式維護內(nèi)存
    final PoolArena<byte[]> heapArena;
    final PoolArena<ByteBuffer> directArena;

    //維護了tiny, small, normal三種類型的緩存列表
    // Hold the caches for the different size classes, which are tiny, small and normal.
    private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
    private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
    private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
    private final MemoryRegionCache<byte[]>[] normalHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

    // Used for bitshifting when calculate the index of normal caches later
    private final int numShiftsNormalDirect;
    private final int numShiftsNormalHeap;
    private final int freeSweepAllocationThreshold;
    private final AtomicBoolean freed = new AtomicBoolean();

    private int allocations;

    // TODO: Test if adding padding helps under contention
    //private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;

    PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena,
                    int tinyCacheSize, int smallCacheSize, int normalCacheSize,
                    int maxCachedBufferCapacity, int freeSweepAllocationThreshold) {
        checkPositiveOrZero(maxCachedBufferCapacity, "maxCachedBufferCapacity");
        this.freeSweepAllocationThreshold = freeSweepAllocationThreshold;

        //通過持有heapArena和directArena谐鼎,arena的方式管理內(nèi)存分配
        this.heapArena = heapArena;
        this.directArena = directArena;

        //通過tinyCacheSize,smallCacheSize,normalCacheSize創(chuàng)建不同類型的緩存列表并保存到成員變量
        if (directArena != null) {
            tinySubPageDirectCaches = createSubPageCaches(
                    tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);
            smallSubPageDirectCaches = createSubPageCaches(
                    smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small);

            numShiftsNormalDirect = log2(directArena.pageSize);
            normalDirectCaches = createNormalCaches(
                    normalCacheSize, maxCachedBufferCapacity, directArena);

            directArena.numThreadCaches.getAndIncrement();
        } else {
            // No directArea is configured so just null out all caches
            tinySubPageDirectCaches = null;
            smallSubPageDirectCaches = null;
            normalDirectCaches = null;
            numShiftsNormalDirect = -1;
        }
        if (heapArena != null) {
            // Create the caches for the heap allocations
            //創(chuàng)建規(guī)格化緩存隊列
            tinySubPageHeapCaches = createSubPageCaches(
                    tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);
            //創(chuàng)建規(guī)格化緩存隊列
            smallSubPageHeapCaches = createSubPageCaches(
                    smallCacheSize, heapArena.numSmallSubpagePools, SizeClass.Small);

            numShiftsNormalHeap = log2(heapArena.pageSize);
            //創(chuàng)建規(guī)格化緩存隊列
            normalHeapCaches = createNormalCaches(
                    normalCacheSize, maxCachedBufferCapacity, heapArena);

            heapArena.numThreadCaches.getAndIncrement();
        } else {
            // No heapArea is configured so just null out all caches
            tinySubPageHeapCaches = null;
            smallSubPageHeapCaches = null;
            normalHeapCaches = null;
            numShiftsNormalHeap = -1;
        }

        // Only check if there are caches in use.
        if ((tinySubPageDirectCaches != null || smallSubPageDirectCaches != null || normalDirectCaches != null
                || tinySubPageHeapCaches != null || smallSubPageHeapCaches != null || normalHeapCaches != null)
                && freeSweepAllocationThreshold < 1) {
            throw new IllegalArgumentException("freeSweepAllocationThreshold: "
                    + freeSweepAllocationThreshold + " (expected: > 0)");
        }
    }

    private static <T> MemoryRegionCache<T>[] createSubPageCaches(
            int cacheSize, int numCaches, SizeClass sizeClass) {
        if (cacheSize > 0 && numCaches > 0) {
            //MemoryRegionCache 維護緩存的一個對象
            @SuppressWarnings("unchecked")
            MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches];
            for (int i = 0; i < cache.length; i++) {
                // TODO: maybe use cacheSize / cache.length
                //每一種MemoryRegionCache(tiny,small,normal)都表示不同內(nèi)存大薪Ⅲ (不同規(guī)格)的一個隊列
                cache[i] = new SubPageMemoryRegionCache<T>(cacheSize, sizeClass);
            }
            return cache;
        } else {
            return null;
        }
    }

    private static <T> MemoryRegionCache<T>[] createNormalCaches(
            int cacheSize, int maxCachedBufferCapacity, PoolArena<T> area) {
        if (cacheSize > 0 && maxCachedBufferCapacity > 0) {
            int max = Math.min(area.chunkSize, maxCachedBufferCapacity);
            int arraySize = Math.max(1, log2(max / area.pageSize) + 1);
            //MemoryRegionCache 維護緩存的一個對象
            @SuppressWarnings("unchecked")
            MemoryRegionCache<T>[] cache = new MemoryRegionCache[arraySize];
            for (int i = 0; i < cache.length; i++) {
                //每一種MemoryRegionCache(tiny,small,normal)都表示不同內(nèi)存(不同規(guī)格)大小的一個隊列
                cache[i] = new NormalMemoryRegionCache<T>(cacheSize);
            }
            return cache;
        } else {
            return null;
        }
    }

......
}

通過查看分配緩存的方法PoolThreadCache#createSubPageCaches()可以發(fā)現(xiàn)具體維護的緩存列表對象MemoryRegionCache實際上時維護了一個Queue<Entry<T>> queue也就是隊列。

    private abstract static class MemoryRegionCache<T> {
        private final int size;
        private final Queue<Entry<T>> queue;
        private final SizeClass sizeClass;
        private int allocations;

        MemoryRegionCache(int size, SizeClass sizeClass) {
            //做一個簡單的規(guī)格化
            this.size = MathUtil.safeFindNextPositivePowerOfTwo(size);
            //持有這種規(guī)格的緩存隊列
            queue = PlatformDependent.newFixedMpscQueue(this.size);
            this.sizeClass = sizeClass;
        }
     ......
     }
  • 關(guān)于準備好的內(nèi)存競技場heapArenadirectArenaPooledByteBufAllocator持有狸棍。在實例化分配器的時候被初始化值
    private final PoolArena<byte[]>[] heapArenas;
    private final PoolArena<ByteBuffer>[] directArenas;
    
    //三種緩存列表長度
    private final int tinyCacheSize;
    private final int smallCacheSize;
    private final int normalCacheSize;

跟蹤初始化的過程可以發(fā)現(xiàn)身害,其實headArenadirectArena都是一個PoolArena[],其內(nèi)部分別定義了兩個內(nèi)部類PoolArena.HeapArenaPoolArena.DirectArena分別表示堆內(nèi)內(nèi)存競技場和堆外內(nèi)存競技場。

    public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder,
                                  int tinyCacheSize, int smallCacheSize, int normalCacheSize,
                                  boolean useCacheForAllThreads, int directMemoryCacheAlignment) {
        super(preferDirect);
        threadCache = new PoolThreadLocalCache(useCacheForAllThreads);
        this.tinyCacheSize = tinyCacheSize;
        this.smallCacheSize = smallCacheSize;
        this.normalCacheSize = normalCacheSize;
        chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);

        checkPositiveOrZero(nHeapArena, "nHeapArena");
        checkPositiveOrZero(nDirectArena, "nDirectArena");

        checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment");
        if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) {
            throw new IllegalArgumentException("directMemoryCacheAlignment is not supported");
        }

        if ((directMemoryCacheAlignment & -directMemoryCacheAlignment) != directMemoryCacheAlignment) {
            throw new IllegalArgumentException("directMemoryCacheAlignment: "
                    + directMemoryCacheAlignment + " (expected: power of two)");
        }

        int pageShifts = validateAndCalculatePageShifts(pageSize);

        //創(chuàng)建兩種內(nèi)存分配的PoolArena數(shù)組草戈,heapArenas和directArenas
        if (nHeapArena > 0) {
            //創(chuàng)建heapArenas內(nèi)存競技場(其實是PoolArena[])
            //nHeapArena:數(shù)組大小
            heapArenas = newArenaArray(nHeapArena);
            List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(heapArenas.length);
            for (int i = 0; i < heapArenas.length; i ++) {
                //堆內(nèi):PoolArena[]存放它下面的HeapArena
                PoolArena.HeapArena arena = new PoolArena.HeapArena(this,
                        pageSize, maxOrder, pageShifts, chunkSize,
                        directMemoryCacheAlignment);
                heapArenas[i] = arena;
                metrics.add(arena);
            }
            heapArenaMetrics = Collections.unmodifiableList(metrics);
        } else {
            heapArenas = null;
            heapArenaMetrics = Collections.emptyList();
        }

        if (nDirectArena > 0) {
            //創(chuàng)建heapArenas內(nèi)存競技場(其實是PoolArena[])
            directArenas = newArenaArray(nDirectArena);
            List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length);
            for (int i = 0; i < directArenas.length; i ++) {
                //堆外:PoolArena[]存放它下面的DirectArena
                PoolArena.DirectArena arena = new PoolArena.DirectArena(
                        this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment);
                directArenas[i] = arena;
                metrics.add(arena);
            }
            directArenaMetrics = Collections.unmodifiableList(metrics);
        } else {
            directArenas = null;
            directArenaMetrics = Collections.emptyList();
        }
        metric = new PooledByteBufAllocatorMetric(this);
    }
    private static <T> PoolArena<T>[] newArenaArray(int size) {
        //創(chuàng)建PoolArena數(shù)組
        return new PoolArena[size];
    }

初始化內(nèi)存競技場數(shù)組的大家的默認值為defaultMinNumArena塌鸯,2被的cpu核心數(shù),運行時每個線程可獨享一個arena唐片,內(nèi)存分配的時候就不用加鎖了

    public PooledByteBufAllocator(boolean preferDirect) {
        this(preferDirect, DEFAULT_NUM_HEAP_ARENA, DEFAULT_NUM_DIRECT_ARENA, DEFAULT_PAGE_SIZE, DEFAULT_MAX_ORDER);
    }
        //2倍cpu核心數(shù),默認創(chuàng)建這個數(shù)量大小的Arena數(shù)組
        // (這個數(shù)字和創(chuàng)建NioEventLoop數(shù)組的數(shù)量一致丙猬,每個線程都可以由一個獨享的arena,這個數(shù)組中的arena其實在分配內(nèi)存的時候是不用加鎖的)
        final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2;
        final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER;
        DEFAULT_NUM_HEAP_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numHeapArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                runtime.maxMemory() / defaultChunkSize / 2 / 3)));
        DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numDirectArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));

  • 整體分配架構(gòu),如圖
    假設(shè)初始化了4個NioEventLoop也就是4個線程的數(shù)組费韭,默認cpu核心數(shù)為2茧球。那么內(nèi)存分配器PooledByteBufAllocator持有的arena數(shù)量也是4個。創(chuàng)建一個ByteBuf的過程如下:
  • 首先星持,通過PoolThreadCache去拿到一個對應(yīng)的arena對象抢埋。那么PoolThreadCache的作用就是通過ThreadLoad的方式把內(nèi)存分配器PooledByteBufAllocator持有的arena數(shù)組中其中的一個arena(最少使用的)塞到PoolThreadCache的一個成員變量里面。
  • 然后,當每個線程通過它(threadCache)去調(diào)用get方法的時候揪垄,會拿到它底層的一個arena,也就是第一個線程拿到第一個穷吮,第二個線程拿到第二個以此類推。這樣可以把線程和arena進行一個綁定
  • PoolThreadCache除了可以直接在arena管理的這塊內(nèi)存進行內(nèi)存分配福侈,還可在它底層維護的一個ByteBuf緩存列表里進行內(nèi)存分配酒来。在PooledByteBufAllocator中持有tinyCacheSize,smallCacheSize,normalCacheSize,分配內(nèi)存時調(diào)用threadCache.get();的時候?qū)嵗?code>PoolThreadCache作為它的構(gòu)造方法參數(shù)傳入肪凛,創(chuàng)建了對應(yīng)的緩存列表堰汉。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伟墙,隨后出現(xiàn)的幾起案子翘鸭,更是在濱河造成了極大的恐慌,老刑警劉巖戳葵,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件就乓,死亡現(xiàn)場離奇詭異,居然都是意外死亡拱烁,警方通過查閱死者的電腦和手機生蚁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來戏自,“玉大人邦投,你說我怎么就攤上這事∩帽剩” “怎么了志衣?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猛们。 經(jīng)常有香客問我念脯,道長,這世上最難降的妖魔是什么弯淘? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任绿店,我火速辦了婚禮,結(jié)果婚禮上耳胎,老公的妹妹穿的比我還像新娘惯吕。我一直安慰自己,他們只是感情好怕午,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布废登。 她就那樣靜靜地躺著,像睡著了一般郁惜。 火紅的嫁衣襯著肌膚如雪堡距。 梳的紋絲不亂的頭發(fā)上甲锡,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音羽戒,去河邊找鬼缤沦。 笑死,一個胖子當著我的面吹牛易稠,可吹牛的內(nèi)容都是我干的缸废。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼驶社,長吁一口氣:“原來是場噩夢啊……” “哼企量!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起亡电,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤届巩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后份乒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恕汇,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年或辖,在試婚紗的時候發(fā)現(xiàn)自己被綠了瘾英。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懈贺。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡炒事,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褪储,到底是詐尸還是另有隱情蟀架,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布榆骚,位于F島的核電站片拍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏妓肢。R本人自食惡果不足惜捌省,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碉钠。 院中可真熱鬧纲缓,春花似錦、人聲如沸喊废。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽污筷。三九已至工闺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陆蟆。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工雷厂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叠殷。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓改鲫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親林束。 傳聞我的和親對象是個殘疾皇子像棘,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,093評論 1 32
  • 在一個方法內(nèi)部定義的變量都存儲在棧中,當這個函數(shù)運行結(jié)束后诊县,其對應(yīng)的棧就會被回收讲弄,此時,在其方法體中定義的變量將不...
    Y了個J閱讀 4,413評論 1 14
  • 在學(xué)習(xí)jemalloc之前可以了解一下glibc malloc依痊,jemalloc沒有'unlinking' 和 '...
    dcharles閱讀 6,645評論 0 7
  • Java SE 基礎(chǔ): 封裝避除、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體胸嘁,并盡...
    Jayden_Cao閱讀 2,103評論 0 8
  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數(shù)瓶摆? 在 Jav...
    侯蛋蛋_閱讀 2,415評論 1 4