简单解释CAP原理

关于CAP原理的术语描述很多,其实给理解带来了障碍。(以下是维基百科:)

一致性(Consistency): (等同于所有节点访问同一份最新的数据副本)
可用性(Availability):(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
分区容错性(Partition tolerance):(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)

如图所示,CAP最多只能取两者。(如果每项要彻底达标,只能取2个,但也可以三者都不完全实现)

改成自然语言,通俗解释一下,就容易理解多了:

C: 一致性。达成一致的速度足够快、或者每次更新是整个集群原子性的操作。如果有一个中控节点(单点瓶颈),很容易让值变更是原子性的。(但也牺牲了A)
A: 可用性。 没有单点瓶颈,挂1,2个节点也能正常工作。
P: 可分性。 把集群切两半,让它们失联,让两半都能正常工作。

具体案例的CAP取舍

大部分的分布式系统(或者说集群)都是先取P,然后取舍A或者C(引入单点,或者使用P2P的协议)。

lease机制: CP。例如hdfs中pipeline写入,client先申请租约lease,然后写入3备份后才返回。一致性由client单点控制完成,P由3副本完成,牺牲了A(有单点)。
Quorum机制:C+0.5A+0.5P。N个副本,更新W个,读R个。
2PC/两阶段提交: C+0.1A+0.1P。
Paxos: C+0.8C+0.8P。

redis-cluster: AP。key的分布:用gossip协议(类似于p2p)达成一致性,因此C比较慢。
redis-sentile: CP。key的分布:各节点完全独立,数据存在哪里完全由客户端单点维护。因此没有A。
codis:CP。由代理单点维护key的分布,因此也没有A。

一个实际的系统可能涉及到很多分布式协议,因此每个部分对于CAP的取舍可能会有不同。
HDFS系统: CP。大部分看起来是有单点。
ceph系统: AP。大部分看起来是p2p的,

redis的quicklist

列表对象: REDIS_LIST

列表对象以前有两种编码: linkedlistzipList
linkedList: 双向链表,灵活扩展,存取效率低。
zipList: 连续存储,存取效率高,对缓存友好,扩展性差,修改插入需要级联移动.甚至可以粗略看做数组。
小数据量的时候会使用zipList,大数据量的时候会使用linkedList
但它们其实是两个极端,各有优缺点。因此之后的版本就把它们混血了一下,出了一个quickList,平衡优缺点。

quickList设计

粗略地看,把双向链表的每一个节点变成zipList,就是quickList了。
在原来的两个数据结构之间做一个折中,量化折中的参数:

1
2
3
4
# 负数表示存储容量等级,每个节点上的ziplist大小不能超过8Kb(也就是2^3KB,以此类推)
list-max-ziplist-size -2
# 正数表示存储个数,每个节点上的zipList内元素不能超过5个:
list-max-ziplist-size 5

还有一个压缩参数:

1
2
3
4
# 不进行压缩:
list-compress-depth 0
# 除了首尾1个,中间的压缩:
list-compress-depth 1

由于双向链表一般用LPush,Rpush,两端的数据访问比较频繁,

quickList源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct quicklist {
quicklistNode *head; // 头结点
quicklistNode *tail; // 尾节点
unsigned long count; // 所有数据的数量
unsigned int len; // quicklist节点数量
int fill:16; // 单个ziplist的大小限制
unsigned int compress:16; // 压缩深度
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前一个节点
struct quicklistNode *next; // 后一个节点
unsigned char *zl; // ziplist
unsigned int sz; // ziplist的内存大小
unsigned int count:16; // zpilist中数据项的个数
unsigned int encoding:2; // 1为ziplist 2是LZF压缩存储方式
unsigned int container:2;
unsigned int recompress:1; // 压缩标志, 为1 是压缩
unsigned int attempted_compress:1; // 节点是否能够被压缩,只用在测试
unsigned int extra:10; /* more bits to steal for future usage */
} quicklistNode;

redis的embstr为什么是39B

回顾字符串的三种实现:

  • int: long能存下的数会用数字编码;
  • embstr: <=39B的字符串,或者浮点数,会用embstr编码;
  • raw: >39B的字符串,会用raw编码,也就是简单动态字符串(SDS)。

embstr顾名思义就是嵌入式字符串,把元数据和实际数据存放在一起,好处是这样可以对缓存友好,可以减少一次寻址。坏处是只能支持短字符串。
这里为啥是39B作为切换条件呢?(而且没有看到参数进行调节),这其实是存储结构决定的。

embstr本质上是利用了元数据存储空间里的剩余空间存储字符串。

所以当元数据剩了40B,embstr就最多只能存39B了(还要留1B存\0)。
回顾元数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define LRU_BITS 24
typedef struct redisObject { // redis对象
unsigned type:4; // 类型,4bit
unsigned encoding:4; // 编码,4bit
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ // 24bit
int refcount; // 引用计数
void *ptr; // 指向各种基础类型的指针
} robj;
typedef struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
}sds;

redisObject占16B

16B=(4bit+4bit+24bit)+4B+8B= (32/8)B+12B

sdshdr占8B

8=4+4+0
注意这里的char buf[]不占空间,当里面存放字符串的时候才占空间,而且末尾需要1B存放\0

按最小分配64B来算,剩余空间就是64B-16B-8B-1B=39B。

但是,如果仔细看看这里sdshdr里的,两个4B的长度:

1
2
unsigned int len;
unsigned int free;

想一下,这里最大长度也才39,根本不需要这么大的数据结构来存长度,因此其实还是有压榨空间的。

从39B到44B

新版本的embstr已经不是39B了,改成了44B。本质上是元数据的剩余空间再次发生了变化。
新版本的元数据(从SDS_TYPE_5SDS_TYPE_645个):
(https://github.com/antirez/redis/blob/unstable/src/sds.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
}

其中sdshdr5其实没有用,用的是sdshdr8,因此这里的元数据结构从8B下降到3B,embstr可用空间也就从39B上升到44B了。

redis4的LFU

LRU实现太复杂,一般用LFU近似。
LFU的关键是统计访问频次。
可以看参考资料: https://redis.io/topics/lru-cache

里面提到为了节省空间,用8位来统计1000次访问或更多。
直接存肯定不行,因为8位=>256,最多存256次。
所以8位肯定只能存分级,256个等级。
比如1000次hit才增加一次等级。
那么怎么知道有1000次hit了呢?
可以用概率算法,rand()一个数,如果小于1/1000,则加1。

类似的概率算法还有:count min sketch算法,用一大堆布隆过滤器记录每个key的访问频次,
由于肯定会有碰撞,因此这个估计值是虚高的。
取的时候,从这些布隆里取一个最小的频次作为估计值即可。
主要思想如此,类似的还有HyperLogLog,但是实际代码有很多边界修正。
比如谷歌的HLL++算法,保证平均、最坏情况下的精度损失不会太大。

Redis设计与实现笔记2-对象

参考\搬运资料: http://redisbook.com/index.html

对象

上一节的数据结构都是底层数据结构的实现,也就是object encoding xxx的结果。(redis称之为编码
封装一下的话,逻辑上看做一个对象:

1
2
3
4
5
6
7
8
#define LRU_BITS 24
typedef struct redisObject { // redis对象
unsigned type:4; // 类型,4bit
unsigned encoding:4; // 编码,4bit
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ // 24bit
int refcount; // 引用计数
void *ptr; // 指向各种基础类型的指针
} robj;

lru: 上次访问的时间(从服务器最初时钟时间开始);(秒)
要计算对象多久没访问了:
空转时长=当前时间-lru时间。
查看对象空闲时间:(秒)

1
OBJECT IDLETIME msg

2^24s=>大概194天。
redis中lru的具体实现逻辑:
https://zhuanlan.zhihu.com/p/34133067

成本较高,redis4后引入lfu,效率近似于原来的lru.(因为原来就是偏概率的算法,不是标准的lru)

类型:

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

五种基本对象(type,类型):字符串、列表、哈希、集合、有序集合

获取类型:

1
2
SET msg "hello world"
TYPE msg # 返回string

编码encoding:

编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long 类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

5种类型可能的8种编码。

类型和编码的可能对应:

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_STRING REDIS_ENCODING_RAW 简单动态字符串
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

类型与编码

类型5种:
字符串、列表、哈希表,集合、有序集合

每种类型底层可以有不同实现,也就是不同的编码。

对象之——字符串

三种实现: intembstrraw

  • int: long能存下的数会用数字编码;
  • embstr: <=39B的字符串,或者浮点苏,会用embstr编码;
  • raw: >39B的字符串,会用raw编码,也就是简单动态字符串(SDS)。

embstr顾名思义就是嵌套字符串,把元数据和实际数据存放在一起,好处是这样可以对缓存友好,可以减少一次寻址。坏处是只能支持短字符串。

上述3种字符串的底层实现会自动根据需要进行切换,无需我们操心,非常nice。

对象之——列表

2种实现: ziplist,linkedlist
压缩列表和链表。

可以看出ziplist顾名思义是压缩链表,申请了一大块连续空间来存放列表。
优点:

  • 对缓存友好;
  • 可以快速访问尾节点。
  • 可以快速访问前一个节点(通过计算地址)。可以达到双向链表的效果。
    缺点:不灵活。

使用 ziplist 编码的必要条件:

  1. 所有字符串元素的长度<64B;(对应参数list-max-ziplist-value)
  2. 元素数量< 512个。(对应参数list-max-ziplist-entries)
    任一不满足,使用普通linkedlist

ziplist

压缩列表的数据节点由previous_entry_length、 encoding 、 content 三个部分组成。
由于是连续空间,可以通过previous_entry_length知道前一个节点的长度,用当前地址减去这个长度即为前一个节点的地址,因此有双向链表的效果。

previous_entry_length:
1个字节: 不以0XFE开头,直接存放长度。(可见这个长度小于0XFE,也就是小于254)
5个字节: 以0XFE开头,去掉首部的0XFE后才是长度。

previous_entry_length而不是直接存前一个节点地址对比:

  1. 依赖连续空间;
  2. 存地址起码4B,64位机器是8B,空间开销大于1B和5B. 而且连续空间下如果存地址,插入节点后所有地址都更改了。

previous_entry_length引起的连锁更新

由于previous_entry_length可能是1B或者5B,如果所有节点原先都是253B,长度字段都只需1B。这个时候如果第一个节点变长为255B,则后一个节点的长度字段变长为5B、变长了4B,总长度变为257B。然后,再后一个节点存放的长度也要变化。依次类推,连锁影响后面所有节点的长度字段,引发连锁更新。

对象之——哈希表

哈希对象的编码可以是ziplist 或者 hashtable
转换参数:
hash-max-ziplist-value: 默认64B;
hash-max-ziplist-entries: 默认512。

对象之——集合

集合对象的编码可以是 intset(整数集合) 或者 hashtable

对象之-有序集合

有序集合的编码可以是 ziplist 或者 skiplist

skiplist

1
2
3
4
typedef struct zset {
zskiplist *zsl; // 保存实际元素和score 服务的命令: ZRange/ZRank (O(LogN))
dict *dict; // 保持(元素=>score)的映射 服务的命令: ZSCORE (O(1))
} zset;

ZSet的成员: (Obj: String,Score: Double)
本来是两倍存储空间(zsl+dict),但因为用指针,只有指针地址两倍了,实际数据还是1倍。
(数据在跳表,dict里只有指针。先插入跳表,再保存到dict。)

skiplist和ziplist切换参数

zset-max-ziplist-entries: 元素数量,默认128.
zset-max-ziplist-value: 元素大小,默认64B。

一些操作复杂度:
ZCARD: O(1). 直接有存长度。
ZCOUNT key min max: logN+M,索引到min以后一直读到max.这里的min和max是分值。
ZRANGE key start stop [WITHSCORES]: logN+M,这里的start,stop是分值的排名。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]: logN+M,分值范围。
ZRANK key member: logN, 返回排名。
ZREM key member [member ...]: logN,删除元素

共享对象

Redis 只对包含整数值的字符串对象进行共享.
整数范围配置:

1
redis.h/REDIS_SHARED_INTEGERS 10000

所以默认共享0 到 9999 的字符串对象。

查看对象的引用计数:

1
OBJECT REFCOUNT A

Redis设计与实现笔记1-数据结构

参考\搬运资料: http://redisbook.com/index.html

设计哲学

设计简单优于巧妙实现:

表现 原因
不处理循环引用 不产生循环引用。数据结构足够简单,最多2层。
不处理事务回滚 程序员自己处理代码错误
换个角度就是,没把握做好的事情就干脆不做了。
解决问题的方法就是不产生问题。不写错代码,很科学,很彻底XD。

简单动态字符串(simple dynamic string,SDS)

1
2
3
4
5
6
7
8
9
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];//\0结尾,不计入len
};

类似head-body结构的消息。比\0分割的C字符串简单易用多了,性能更高,代价是付出了更多的内存。
之所以最后用上了\0结尾,是为了重用一部分c库里的字符串库,进一步降低了使用成本。

性能优势点:

  1. 获取字符串长度: O(1)
  2. 空间预分配,减少系统调用。
  3. 惰性空间释放,减少系统调用。

安全性优势点:

  1. 有效防止缓冲区溢出
  2. 二进制安全,可以保存\0。

缺点:

  1. 如果有大量字符串缩短,内存泄露严重。

sdscat

字符串拼接:

计算拼接后总长度len,扩展空间为2*len,拼入字符串。最后一定会有一半free空间。

空间预分配

假如现有空间不足以存放字符串:

  • 总长度len<1MB: 总空间为2*len+1;
  • 总长度len>=1MB: 总空间为len+1MB+1
    换句话说,预分配的空间上限是1MB,尽量为len。
    这么做的目的是减少空间申请、回收的系统调用性能开销。

惰性空间释放

字符串缩短的时候,默认不进行内存回收(可能内存泄露)。
需要手动调API回收。

链表(RPush)

单个节点:

1
2
3
4
5
6
7
8
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;

可以看出是一个简单的双向链表。

整个列表容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
} list;

哈希表(HSet)

单个节点:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct dictEntry {
// 键
void *key;
// 值
union { // 三者取其1
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;

整个表:

1
2
3
4
5
6
7
8
9
10
11
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;

对外接口:

1
2
3
4
5
6
7
8
9
10
11
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表,有两个,第二个扩容时候用
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1。 扩容时候用。
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

dictType:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;

sizemask为3的话,也就是0x11,就是说取最后两位作为哈希序号,也就是0~3,4个数。
所以此时hash表长度为4。以此类推,sizemask有n位,哈希表长度就有2^n。

渐进式hash

具体还是要参见: http://redisbook.com/preview/dict/rehashing.html

逐步把ht[0]中的数据迁移到ht[1]中。rehashidx为0的时候,表示0号位置已经全部迁往ht[1],依次类推,直到全部迁完。
交换ht[0],ht[1]以后,rehashidx改为-1.
在此过程中ht[0]不会有新插入,新请求全部去ht[1]。

扩容/rehash的触发

由时间事件serverCron根据负载因子来决定是否进行扩容。
扩容的实际执行是由1个专门的单线程来进行; (类似cms一样高响应,一次resh一个桶,尽量不阻塞线上读写请求)
相比之下,concurrentHashMap的扩容则由多线程进行,阻塞所有读写,高吞吐低响应。

跳表

有序集合使用。(ZSET)
节点源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;

跳表容器:

1
2
3
4
5
6
7
8
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;

从高层向低层找:

  1. 在找的过程中用update[]数组记录每一层插入位置的上一个节点,
  2. 用rank[]数组记录每一层插入位置的上一个节点在跳跃表中的排名。
  3. 根据update[]插入新节点,插入完成后再根据rank[]更新跨度信息。

整数集合

整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。

1
2
SADD numbers 1 3 5 7 9
OBJECT ENCODING numbers # 查看底层实现 "intset"

升级

默认用最小数据类型,新数据太大的话,会对整个集合数据类型升级,并且移动全部元素。
因此最坏时间复杂度为O(n)。

升级后不会降级。

压缩列表(ziplist)

压缩列表(ziplist)是列表键和哈希键的底层实现之一。
适用条件: 整数、小字符串。

1
2
RPUSH lst 1 3 5 10086 "hello" "world"
OBJECT ENCODING lst # 底层结构: ziplist

mysql意向锁

意向锁: 对表内现有锁的概括,可以看作一种状态信息,很有用但并不必要,用于优化性能。

意向锁特点: 均为表级(因为是信息概括),意向锁分为读意向锁和写意向锁,但意向锁之间不互斥。

场景1:(如果没有意向锁)

事务A: 对表的某一行加了行锁(互斥锁);
事务B: 希望对整表加锁,需要知道是否已经有表锁,并且需要知道表中任意一样是否有行锁(因为锁定范围有重合,而且是互斥锁),这个时候如果没有意向锁,事务B就只能扫全表了。

场景2:(引入意向锁后)

事务A: 先申请表的意向锁,然后申请行锁;
事务B:先申请表的意向锁(成功),然后申请表锁,这个时候由于上一步,立即能知道表中存在别的行锁,因此阻塞。

意向锁的兼容互斥关系:

神仙与树獭的等价性

普通程序员如何理解神话世界观

1
太白金星:“大圣须知天上一日,地上一年。”

   根据太白金星的说法易得——时间流速(天上):时间流速(地上)=1:365.25
也就是说神仙在天上看1分钟电视的时候,地上的人已经看了365.25分钟的电视了,如果不考虑双方寿命的话明显是地上的人赚了。

   由于孙悟空经常在天上和人间之间来回穿梭,可以看出天上的慢时空和地上的快时空是互通的,为了便于换算,不妨以地上的时间作为基准参照系,此时观测天上的神仙(假设用大圣爷的千里眼能够看到),由于天上的时间流速很慢,可以看到神仙都是以极慢的速度在运动的,从这个角度来说他们与树懒具有一定的等价性。

   如果再加入寿命的考虑,可以用繁殖速度来进行类比,以天上的时间为基准参照系的话,此时观测人间,看个几十天人类就繁殖一代了,然后很快就死了。而从人类的角度看细菌的话,可能二十分钟就完成了一代的繁殖,细菌的寿命往往也不会很长。人体构成比细菌复杂得多,完成一次全部复制的时间成本也高得多,可以类比得想象,神仙可能具有极高的组成,复制的时间成本极高,运动的代价也极大,可能是以巨人的形态存在着的。

   从个人的角度来说,假设寿命是定长的,时间的流速其实反映在生命的质量上。假设地上有一只与人类寿命一致的蚂蚁,人类3秒钟能完成的思考,假设蚂蚁需要花一天的时间,换句话说蚂蚁要花一天的时间才能获得人类3秒钟的生命体验,这样生命的质量就很差了,即使这只蚂蚁与人类一样活了100年,但它实际体验到的人生只有人类的1.26天。

   从cpu时钟的角度来说,刚才例子这只思考速度很慢的蚂蚁就像一台cpu时钟频率极低的计算机。而天上的神仙掐指一算,就能尽知天下事,同时又是以极慢的速度运作,可以看作是一台元件极其复杂的巨型计算机,需要365.25倍的时间才能完成一次脉冲信号,每次运算数据量极大。

   延长自己的寿命是很难达到的,但是好好锻炼大脑,提高自己的cpu时钟频率,提高自己的反应速度,提高生命质量是可行的,从这个角度来说也算是延长了生命的长度。

如何阅读heap dump文件

参考资料: http://www.importnew.com/24393.html

首先可以尝试用google-perftools查看统计结果,然后用MAT查看heap dump的详细内容。

谷歌的性能分析工具google-perftools

/usr/local/gperftools/bin/pprof --text $JAVA_HOME/bin/java /data/gperf/ worker.0001.heap
查看统计结果。

下载MAT

https://eclipse.org/mat/downloads.php
注意打开的时候要选打开heap dump,而不是open file,不然会打开失败。

术语

Retained Heap

Retained: 持有,阻拦
对象以及它所持有的其它引用(包括直接和间接)所占的总内存。

tips

1
如果A,B都持有C对象引用,则C的内存不计入A或B。

Shallow heap

Shallow: 浅
对象本身占用内存的大小,不包含其引用的对象。

  • 常规对象(非数组)的shallow size有其成员变量的数量和类型决定。
  • 数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定

    Shallow size最大一般是byte[], char[], int[]

Action面板的功能

Histogram

默认Shallow size排序
列出每个class的实例数量、大小(内存中的对象统计),可以查看某些对象的具体值:

进一步操作: group by package:

Dominator Tree

默认Retained Heap排序
列出最大的对象以及其依赖存活的Object。
TIPS: 这里有黄点的是GC Root。

Top Consumers

从3个角度列出最占用内存的对象,展现形式有表格和图。
三个角度: class,class loader,包。

Duplicate Class

列出由多个class loader加载的class。

图例

带有黄点的对象: GCRoot(如果是System class可以不用管)

常用操作

Path to GC Roots -> exclude weak references: 看看为啥没有回收。

List Object->With incoming reference: 看看都有谁用了这个对象;

List Object->With outcoming reference: 看看里面都有啥。

快速查看大量gc日志的gui工具

之前学习了GC日志的格式,能理解每行日志的大致含义。
然而对于实际生产项目,日志量庞大,逐行看很低效,这个时候借助一下gui工具就很nice了。

经过海淘了谷歌,比较后留下这2个比较好用的工具:

  1. GCViewer,
  2. http://gceasy.io

被抛弃的工具:

  1. GCHisto: 界面好看,功能有点弱;
  2. visualgc(jvisualvm的插件): jvisualvm已经1.8,visualgc版本才1.4,完全安装不上,应该已经不维护了。

GCViewer

https://sourceforge.net/projects/gcviewer/
缺点: 字超小
优点: 功能全

查看堆空间的变化

view菜单栏可以看到各种颜色的线的含义,例如蓝色线是堆空间的使用。如果全打开的话眼睛会花的,可以逐步打开几个关心的。

像下面这种情况图中黑色柱状较多的时候,可以看出堆空间不足了,gc时间占用较多:

gc回收的各项统计数据

如果搭配一个放大镜,GCViewer就是一个非常好的gc日志查看工具了。

http://gceasy.io

上传日志到网站。
优点:

  1. 有诊断功能,自动检测问题;
  2. 界面很美观。

诊断功能

它能帮忙检测gc日志中反映出的问题:

比如能检测到fullgc:

有些还能针对性地给出建议:

统计信息

它提供了各个角度的统计信息。
新生代、老生代的峰值使用大小:

各种统计图表: