因为每一版本的源码可能会有差别,现在基于lua 5.2.1来分析,保持一致性。
从虚拟机的大体来看,字符串通过一个结构体存放在global_State里,这个结构stringtable(lstate.h)是:
GCObject(lstate.h)的结构是:
stringtable结构体的字段含义是:
GCObject **hash: GCObject指针的指针,通过Hash值可以指向Hash值存放的GCObject,其中实际引用的是TString.
lu_int32 nuse: 已经创建的TString个数
int size:TString的总个数(初始值为32)
在global_State中,会有一个stringtable,通过它来访问虚拟机中的所有字符串(包括长短字符串),现在5.2.1的版本中,长短字符串的存放有点略不同,后面会细说。
具体看看字符串的实现,主要数据结构体是TString(lobject.h)
TString关联的宏定义, L_Umaxalign(llimits.h)
TString关联的宏定义, CommonHeader(lobject.h),这一个定义很多地方会使用到
这是TString相关的结构体。
下面来是分析如何创建一个字符串,实现的文件是在lstring.c。
在lua里面,创建一个字符串,会根据字符串的长短来区分到底创建长还是短的字符串。
如果长度小于LUAI_MAXSHORTLEN(值为40),它就会创建一个短字符串;
否则的话就会创建长字符串。
短字符串的区别是:如果在创建的过程,发现hash值、长度和内容都一样的短字符串,就会复用它,不再去进行分配。
但是长的字符串就会不管三七二一都会创建一个新的TString.
在创建字符串之前,stringtable的长度会动态扩展,当字符串table分配的size都被用完了,并且当size不大于最大数量的一半时,即需要调用resize进行扩展:
if (tb->nuse >= cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
luaS_resize(L, tb->size*2); /* too crowded */
既然有动态扩展,也有动态缩小,在GC的时候,stringtable也会检查是否需要回收释放空间,当字符串table使用率小于50%的时候,会进行resize。
if (g->gckind != KGC_EMERGENCY) { /* do not change sizes in emergency */
int hs = g->strt.size / 2; /* half the size of the string table */
if (g->strt.nuse < cast(lu_int32, hs)) /* using less than that half? */
luaS_resize(L, hs); /* halve its size */
luaZ_freebuffer(L, &g->buff); /* free concatenation buffer */
}
具体创建逻辑
1. 创建字符串对象:
字符串的内存结构是:TString+字符串内容+'\0','\0'是结尾标识,这一个结构的设计实在太棒棒了,直接省掉一个指针指向具体字符串内容,读取的时候只需要读一块连续内存。
2.最最关键的地方来了,既然都创建了,那怎么存到stringtable里呢?怎么构造?好,前面的介绍其实并不是没用的,那是概览。stringtable里有一个GCObject** hash,其实就是需要通过它来索引到所有的字符串内容。
hash指向hash数组,数组里每一个单元存放的是该hash值下面所有TString链表的队尾元素的地址。(hash值的含义实质为字符串长度,只是通过hash计算和lmod取模)大概示意图如下。
构造链表的函数是luaC_newobj (lgc.c):
最后一个,前面提了很多hash关键字,在lua虚拟机里,hash算法采用的是JSHash,虚拟机启动的时候会生成一个随机种子,这个随机种子会在创建hash的时候一并带进去,以防被猜到hash值。
lstring的设计实在很棒棒,很多巧妙的设计,lua的源码小而精真不是盖的。