我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Netty分布式ByteBuf怎么使用命中缓存分配

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Netty分布式ByteBuf怎么使用命中缓存分配

今天小编给大家分享一下Netty分布式ByteBuf怎么使用命中缓存分配的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

分析先关逻辑之前, 首先介绍缓存对象的数据结构

回顾上一小节的内容, 我们讲到PoolThreadCache中维护了三个缓存数组(实际上是六个, 这里仅仅以Direct为例, heap类型的逻辑是一样的): tinySubPageDirectCaches, smallSubPageDirectCaches, 和normalDirectCaches分别代表tiny类型, small类型和normal类型的缓存数组

这三个数组保存在PoolThreadCache的成员变量中:

private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

其中是在构造方法中进行了初始化:

tinySubPageDirectCaches = createSubPageCaches(        tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);smallSubPageDirectCaches = createSubPageCaches(        smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small);normalDirectCaches = createNormalCaches(        normalCacheSize, maxCachedBufferCapacity, directArena);

我们以tiny类型为例跟到createSubPageCaches方法中

private static <T> MemoryRegionCache<T>[] createSubPageCaches(        int cacheSize, int numCaches, SizeClass sizeClass) {    if (cacheSize > 0) {        @SuppressWarnings("unchecked")        MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches];        for (int i = 0; i < cache.length; i++) {            cache[i] = new SubPageMemoryRegionCache<T>(cacheSize, sizeClass);        }        return cache;    } else {        return null;    }}

这里上面的小节已经分析过, 这里创建了一个缓存数组, 这个缓存数组的长度,也就是numCaches, 在不同的类型, 这个长度不一样, tiny类型长度是32, small类型长度为4, normal类型长度为3

我们知道, 缓存数组中每个节点代表一个缓存对象, 里面维护了一个队列, 队列大小由PooledByteBufAllocator类中的tinyCacheSize, smallCacheSize, normalCacheSize属性决定的, 这里之前小节已经剖析过

其中每个缓存对象, 队列中缓存的ByteBuf大小是固定的, netty将每种缓冲区类型分成了不同长度规格, 而每个缓存中的队列缓存的ByteBuf的长度, 都是同一个规格的长度, 而缓冲区数组的长度, 就是规格的数量

比如, 在tiny类型中, netty将其长度分成32个规格, 每个规格都是16的整数倍, 也就是包含0B, 16B, 32B, 48B, 64B, 80B, 96B......496B总共32种规格, 而在其缓存数组tinySubPageDirectCaches中, 这每一种规格代表数组中的一个缓存对象缓存的ByteBuf的大小, 我们以tinySubPageDirectCaches[1]为例(这里下标选择1是因为下标为0代表的规格是0B, 其实就代表一个空的缓存, 这里不进行举例), 在tinySubPageDirectCaches[1]的缓存对象中所缓存的ByteBuf的缓冲区长度是16B, 在tinySubPageDirectCaches[2]中缓存的ByteBuf长度都为32B, 以此类推, tinySubPageDirectCaches[31]中缓存的ByteBuf长度为496B

有关类型规则的分配如下:

tiny:总共32个规格, 均是16的整数倍, 0B, 16B, 32B, 48B, 64B, 80B, 96B......496B

small:4种规格, 512b, 1k, 2k, 4k

nomal:3种规格, 8k, 16k, 32k

这样, PoolThreadCache中缓存数组的数据结构为

Netty分布式ByteBuf怎么使用命中缓存分配

大概了解缓存数组的数据结构, 我们再继续剖析在缓冲中分配内存的逻辑

回到PoolArena的allocate方法中

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {    //规格化    final int normCapacity = normalizeCapacity(reqCapacity);    if (isTinyOrSmall(normCapacity)) {         int tableIdx;        PoolSubpage<T>[] table;        //判断是不是tinty        boolean tiny = isTiny(normCapacity);        if (tiny) { // < 512            //缓存分配            if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {                return;            }            //通过tinyIdx拿到tableIdx            tableIdx = tinyIdx(normCapacity);            //subpage的数组            table = tinySubpagePools;        } else {            if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {                return;            }            tableIdx = smallIdx(normCapacity);            table = smallSubpagePools;        }        //拿到对应的节点        final PoolSubpage<T> head = table[tableIdx];        synchronized (head) {            final PoolSubpage<T> s = head.next;            //默认情况下, head的next也是自身            if (s != head) {                assert s.doNotDestroy && s.elemSize == normCapacity;                long handle = s.allocate();                assert handle >= 0;                s.chunk.initBufWithSubpage(buf, handle, reqCapacity);                if (tiny) {                    allocationsTiny.increment();                } else {                    allocationsSmall.increment();                }                return;            }        }        allocateNormal(buf, reqCapacity, normCapacity);        return;    }    if (normCapacity <= chunkSize) {        //首先在缓存上进行内存分配        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {            //分配成功, 返回            return;        }        //分配不成功, 做实际的内存分配        allocateNormal(buf, reqCapacity, normCapacity);    } else {        //大于这个值, 就不在缓存上分配        allocateHuge(buf, reqCapacity);    }}

首先通过normalizeCapacity方法进行内存规格化

我们跟到normalizeCapacity方法中

int normalizeCapacity(int reqCapacity) {    if (reqCapacity < 0) {        throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)");    }    if (reqCapacity >= chunkSize) {        return reqCapacity;    }    //如果>tiny    if (!isTiny(reqCapacity)) { // >= 512        //找一个2的幂次方的数值, 确保数值大于等于reqCapacity        int normalizedCapacity = reqCapacity;        normalizedCapacity --;        normalizedCapacity |= normalizedCapacity >>>  1;        normalizedCapacity |= normalizedCapacity >>>  2;        normalizedCapacity |= normalizedCapacity >>>  4;        normalizedCapacity |= normalizedCapacity >>>  8;        normalizedCapacity |= normalizedCapacity >>> 16;        normalizedCapacity ++;        if (normalizedCapacity < 0) {            normalizedCapacity >>>= 1;        }        return normalizedCapacity;    }    //如果是16的倍数    if ((reqCapacity & 15) == 0) {        return reqCapacity;    }    //不是16的倍数, 变成最大小于当前值的值+16    return (reqCapacity & ~15) + 16;}

 if (!isTiny(reqCapacity)) 代表如果大于tiny类型的大小, 也就是512, 则会找一个2的幂次方的数值, 确保这个数值大于等于reqCapacity

如果是tiny, 则继续往下

 if ((reqCapacity & 15) == 0) 这里判断如果是16的倍数, 则直接返回

如果不是16的倍数, 则返回 (reqCapacity & ~15) + 16 , 也就是变成最小大于当前值的16的倍数值

从上面规格化逻辑看出, 这里将缓存大小规格化成固定大小, 确保每个缓存对象缓存的ByteBuf容量统一

回到allocate方法中

 if(isTinyOrSmall(normCapacity)) 这里是根据规格化后的大小判断是否tiny或者small类型, 我们跟到方法中:

boolean isTinyOrSmall(int normCapacity) {    return (normCapacity & subpageOverflowMask) == 0;}

这里是判断如果normCapacity小于一个page的大小, 也就是8k代表其实tiny或者small

继续看allocate方法:

如果当前大小是tiny或者small, 则isTiny(normCapacity)判断是否是tiny类型, 跟进去:

static boolean isTiny(int normCapacity) {    return (normCapacity & 0xFFFFFE00) == 0;}

这里是判断如果小于512, 则认为是tiny

再继续看allocate方法:

如果是tiny, 则通过cache.allocateTiny(this, buf, reqCapacity, normCapacity)在缓存上进行分配

我们就以tiny类型为例, 分析在缓存上分配ByteBuf的流程

allocateTiny是缓存分配的入口

我们跟进去, 进入到了PoolThreadCache的allocateTiny方法中:

boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {    return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);}

这里有个方法cacheForTiny(area, normCapacity), 这个方法的作用是根据normCapacity找到tiny类型缓存数组中的一个缓存对象

我们跟进cacheForTiny:

private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {     int idx = PoolArena.tinyIdx(normCapacity);    if (area.isDirect()) {        return cache(tinySubPageDirectCaches, idx);    }    return cache(tinySubPageHeapCaches, idx);}

PoolArena.tinyIdx(normCapacity)是找到tiny类型缓存数组的下标

继续跟tinyIdx:

static int tinyIdx(int normCapacity) {    return normCapacity >>> 4;}

这里直接将normCapacity除以16, 通过前面的内容我们知道, tiny类型缓存数组中每个元素规格化的数据都是16的倍数, 所以通过这种方式可以找到其下标, 参考图5-2, 如果是16B会拿到下标为1的元素, 如果是32B则会拿到下标为2的元素

回到acheForTiny方法中

 if (area.isDirect()) 这里判断是否是分配堆外内存, 因为我们是按照堆外内存进行举例, 所以这里为true

再继续跟到cache(tinySubPageDirectCaches, idx)方法中:

private static <T> MemoryRegionCache<T> cache(MemoryRegionCache<T>[] cache, int idx) {    if (cache == null || idx > cache.length - 1) {        return null;    }     return cache[idx];}

这里我们看到直接通过下标的方式拿到了缓存数组中的对象

回到PoolThreadCache的allocateTiny方法中:

boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {    return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);}

拿到了缓存对象之后, 我们跟到allocate(cacheForTiny(area, normCapacity), buf, reqCapacity)方法中:

private boolean allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity) {    if (cache == null) {        return false;    }    boolean allocated = cache.allocate(buf, reqCapacity);    if (++ allocations >= freeSweepAllocationThreshold) {        allocations = 0;        trim();    }    return allocated;}

这里通过cache.allocate(buf, reqCapacity)进行继续进行分配

再继续往里跟, 跟到内部类MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法中:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {    Entry<T> entry = queue.poll();    if (entry == null) {        return false;    }    initBuf(entry.chunk, entry.handle, buf, reqCapacity);    entry.recycle();    ++ allocations;    return true;}

这里首先通过queue.poll()这种方式弹出一个entry, 我们之前的小节分析过, MemoryRegionCache维护着一个队列, 而队列中的每一个值是一个entry

我们简单看下Entry这个类

static final class Entry<T> {    final Handle<Entry<?>> recyclerHandle;    PoolChunk<T> chunk;    long handle = -1;    //代码省略}

这里重点关注chunk和handle的这两个属性, chunk代表一块连续的内存, 我们之前简单介绍过, netty是通过chunk为单位进行内存分配的, 我们之后会对chunk进行剖析

handle相当于一个指针, 可以唯一定位到chunk里面的一块连续的内存, 之后也会详细分析

这样, 通过chunk和handle就可以定位ByteBuf中指定一块连续内存, 有关ByteBuf相关的读写, 都会在这块内存中进行

我们回到MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {    Entry<T> entry = queue.poll();    if (entry == null) {        return false;    }    initBuf(entry.chunk, entry.handle, buf, reqCapacity);    entry.recycle();    ++ allocations;    return true;}

弹出entry之后, 通过initBuf(entry.chunk, entry.handle, buf, reqCapacity)这种方式给ByteBuf初始化, 这里参数传入我们刚才分析过的当前Entry的chunk和hanle

因为我们分析的tiny类型的缓存对象是SubPageMemoryRegionCache类型,所以我们继续跟到SubPageMemoryRegionCache类的initBuf(entry.chunk, entry.handle, buf, reqCapacity)方法中:

protected void initBuf(        PoolChunk<T> chunk, long handle, PooledByteBuf<T> buf, int reqCapacity) {    chunk.initBufWithSubpage(buf, handle, reqCapacity);}

这里的chunk调用了initBufWithSubpage(buf, handle, reqCapacity)方法, 其实就是PoolChunk类中的方法

我们继续跟initBufWithSubpage:

void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int reqCapacity) {    initBufWithSubpage(buf, handle, bitmapIdx(handle), reqCapacity);}

这里有关bitmapIdx(handle)相关的逻辑, 会在后续的章节进行剖析, 这里继续往里跟:

private void initBufWithSubpage(PooledByteBuf<T> buf, long handle, int bitmapIdx, int reqCapacity) {    assert bitmapIdx != 0;    int memoryMapIdx = memoryMapIdx(handle);    PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];    assert subpage.doNotDestroy;    assert reqCapacity <= subpage.elemSize;    buf.init(        this, handle,         runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, reqCapacity, subpage.elemSize,         arena.parent.threadCache());}

这里我们先关注init方法, 因为我们是以PooledUnsafeDirectByteBuf为例, 所以这里走的是PooledUnsafeDirectByteBuf的init方法

跟进init方法

void init(PoolChunk<ByteBuffer> chunk, long handle, int offset, int length, int maxLength,           PoolThreadCache cache) {    super.init(chunk, handle, offset, length, maxLength, cache);    initMemoryAddress();}

首先调用了父类的init方法, 再跟进去:

void init(PoolChunk<T> chunk, long handle, int offset, int length, int maxLength, PoolThreadCache cache) {    //初始化    assert handle >= 0;    assert chunk != null;    //在哪一块内存上进行分配的    this.chunk = chunk;    //这一块内存上的哪一块连续内存    this.handle = handle;    memory = chunk.memory;    this.offset = offset;    this.length = length;    this.maxLength = maxLength;    tmpNioBuf = null;    this.cache = cache;}

这里将PooledUnsafeDirectByteBuf的各个属性进行了初始化

 this.chunk = chunk 这里初始化了chunk, 代表当前的ByteBuf是在哪一块内存中分配的

 this.handle = handle 这里初始化了handle, 代表当前的ByteBuf是这块内存的哪个连续内存

有关offset和length, 我们会在之后的小节进行分析, 在这里我们只需要知道, 通过缓存分配ByteBuf, 我们只需要通过一个chunk和handle, 就可以确定一块内存

以上就是通过缓存分配ByteBuf对象的过程

我们回到MemoryRegionCache的allocate(PooledByteBuf<T> buf, int reqCapacity)方法:

public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {    Entry<T> entry = queue.poll();    if (entry == null) {        return false;    }    initBuf(entry.chunk, entry.handle, buf, reqCapacity);    entry.recycle();    ++ allocations;    return true;}

分析完了initBuf方法, 再继续往下看

entry.recycle()这步是将entry对象进行回收, 因为entry对象弹出之后没有再被引用, 可能gc会将entry对象回收, netty为了将对象进行循环利用, 就将其放在对象回收站进行回收

我们跟进recycle方法

void recycle() {    chunk = null;    handle = -1;    recyclerHandle.recycle(this);}

chunk = null和handle = -1表示当前Entry不指向任何一块内存

 recyclerHandle.recycle(this) 将当前entry回收, 有关对象回收站, 我们会在后面的章节详细剖析

以上就是“Netty分布式ByteBuf怎么使用命中缓存分配”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网行业资讯频道。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Netty分布式ByteBuf怎么使用命中缓存分配

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Netty分布式ByteBuf怎么使用命中缓存分配

今天小编给大家分享一下Netty分布式ByteBuf怎么使用命中缓存分配的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。分析先
2023-06-29

Netty分布式ByteBuf如何使用page级别的内存分配

这篇文章主要为大家展示了“Netty分布式ByteBuf如何使用page级别的内存分配”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Netty分布式ByteBuf如何使用page级别的内存分配”
2023-06-29

Netty分布式ByteBuf使用subPage级别内存分配的方法

这篇文章主要介绍“Netty分布式ByteBuf使用subPage级别内存分配的方法”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Netty分布式ByteBuf使用subPage级别内存分配的方法”
2023-06-29

Netty分布式ByteBuf使用的回收逻辑是什么

这篇文章主要介绍“Netty分布式ByteBuf使用的回收逻辑是什么”,在日常操作中,相信很多人在Netty分布式ByteBuf使用的回收逻辑是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Netty分布
2023-06-29

Hadoop分布式缓存怎么使用

本篇内容介绍了“Hadoop分布式缓存怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1.前言DistributedCache是had
2023-06-19

C#中怎么使用Couchbase实现分布式缓存

C#中怎么使用Couchbase实现分布式缓存,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一、简介 目前C#业界使用得最多的 Cache 系统主要是 Memcached和
2023-06-17

怎么使用SignalR和Redis实现实时分布式缓存

要实现实时分布式缓存,可以结合使用SignalR和Redis。首先,通过SignalR建立一个实时通信的连接,可以让不同的服务之间实时地传递消息和数据。然后,借助Redis作为分布式缓存存储,可以在不同的服务之间共享缓存数据。具体步骤
怎么使用SignalR和Redis实现实时分布式缓存
2024-05-09

分布式缓存在PHP接口性能优化中的应用(PHP接口性能优化中分布式缓存的使用)

分布式缓存(Redis、Memcached、Varnish等)可优化PHP接口性能,其工作原理是存储常用数据的副本,减少数据库访问频率。应用方式包括:存储会话数据、缓存动态内容、减轻数据库负载、改进全局一致性、提高吞吐量。优化策略有:选择合适的缓存技术、设置缓存到期时间、使用一致性哈希、限制缓存大小、实施缓存清除策略、使用缓存封装库。好处包含:降低数据库负载、减少API响应时间、提高吞吐量、增强可扩展性、降低成本。需要注意:数据一致性、缓存开销、缓存穿透、缓存雪崩、缓存停电。通过有效应用分布式缓存,PHP
分布式缓存在PHP接口性能优化中的应用(PHP接口性能优化中分布式缓存的使用)
2024-04-02

PHP开发中如何使用Memcache进行分布式缓存?

随着Web应用程序的日益复杂,性能也成为了一个关键问题。在许多应用程序中,数据库查询是最耗费时间的操作之一。为了避免频繁地从数据库中读取数据,可以使用一个缓存系统,将经常读取的数据存储在内存中,以便快速的访问。在PHP开发中,使用Memca
PHP开发中如何使用Memcache进行分布式缓存?
2023-11-07

PHP开发中如何使用Memcache实现分布式缓存?

PHP开发中如何使用Memcache实现分布式缓存?随着Web应用程序的规模和访问量的增加,缓存的重要性也越来越凸显。使用缓存可以有效减轻数据库的负载,提高网站的响应速度,并减少不必要的服务器请求。在分布式环境中,使用Memcache来实现
PHP开发中如何使用Memcache实现分布式缓存?
2023-11-07

应用实践:如何在分布式缓存中使用RT和WT?

随着应对大规模交易的Web应用程序、SOA和其他服务器应用程序的爆炸式增长,数据存储无法跟上应用增长速度,因为数据存储无法继续添加更多服务器以扩展,这与可扩展性极高的应用架构不同。  在这种情况下,内存分布式缓存为数据存储瓶颈提供了极好
2023-06-05

Spring Cache怎么使用Redisson分布式锁解决缓存击穿问题

本篇内容主要讲解“Spring Cache怎么使用Redisson分布式锁解决缓存击穿问题”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Spring Cache怎么使用Redisson分布式锁解
2023-06-30

Laravel中怎么使用Redis分布式锁

这篇文章主要介绍“Laravel中怎么使用Redis分布式锁”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Laravel中怎么使用Redis分布式锁”文章能帮助大家解决问题。创建锁use Illum
2023-07-04

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录