1. 程式人生 > >redis原始碼之物件

redis原始碼之物件

未完待續…

物件

簡介:

對於 Redis 資料庫儲存的鍵值對來說, 鍵總是一個字串物件, 而值則可以是:
1.字串物件、
2.列表物件、
3.雜湊物件、
4.集合物件、
5.有序集合物件
其中一種

2.定義

所有的物件都是按照如下的方式定義的,而不同的物件他們對應的值不同而已,但是結構都是如下所示。

typedef struct redisObject {

    // 型別,TYPE命令返回資料庫鍵對應的值物件的型別,而不是鍵物件的型別。
    unsigned type:4;

    // 編碼,OBJECT ENCODING命令時,命令可以檢視一個數據庫鍵的值物件的編碼。
unsigned encoding:4; // 指向實際值的指標,指向實際型別物件 void *ptr; // 引用計數,any量的引用 int refcount; // 物件最後一次被訪問的時間,OBJECT IDLETIME 命令可以打印出給定鍵的空轉時長 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ } robj;

1.type
當我們對一個數據庫鍵執行TYPE命令時,命令返回的結果為資料庫鍵對應的值物件的型別,而不是鍵物件的型別。

物件 物件 type 屬性的值 TYPE 命令的輸出
字串物件 REDIS_STRING “string”
列表物件 REDIS_LIST “list”
雜湊物件 REDIS_HASH “hash”
集合物件 REDIS_SET “set”
有序集合物件 REDIS_ZSET “zset”

2.encoding
encoding 屬性記錄了物件所使用的編碼,同樣,當我們對一個數據庫鍵執行OBJECT 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 跳躍表和字典

每種型別的物件都至少使用了兩種不同的編碼, 下表表示出了每種型別的物件可以使用的編碼。
這裡寫圖片描述
通過 encoding 屬性來設定物件所使用的編碼, 而不是為特定型別的物件關聯一種固定的編碼, 極大地提升了 Redis 的靈活性和效率, 因為 Redis 可以根據不同的使用場景來為一個物件設定不同的編碼, 從而優化物件在某一場景下的效率。
舉個例子, 在列表物件包含的元素比較少時, Redis 使用壓縮列表作為列表物件的底層實現:
因為壓縮列表比雙端連結串列更節約記憶體, 並且在元素數量較少時, 在記憶體中以連續塊方式儲存的壓縮列表比起雙端連結串列可以更快被載入到快取中;
隨著列表物件包含的元素越來越多, 使用壓縮列表來儲存元素的優勢逐漸消失時, 物件就會將底層實現從壓縮列表轉向功能更強、也更適合儲存大量元素的雙端連結串列上面。
在接下來的內容中,我們將分別介紹Redis 中的五種不同型別的物件,說明這些物件底層所使用的編碼方式,列出物件從一種編碼轉換成另一種編碼所需的條件,以及同一個命令在多種不同編碼上的實現方法。
3.refcount
Redis 的物件系統還實現了基於引用計數技術的記憶體回收機制,當程式不再使用某個物件的時候,這個物件所佔用的記憶體就會被自動擇放;另外, Redis 還通過引用計數技術實現了物件共享機制,這一機制可以在適當的條件下,通過讓多個數據庫鍵共事同一個物件來節約記憶體。
4.lru
物件最後一次被訪問的時間,Redis 的物件帶有訪問時間記錄資訊,該資訊可以用於計算資料庫鍵的空轉時長。
5.ptr
物件的ptr 指標指向物件的底層實現資料結構,而這些資料結構由物件的enc。ding
屬性決定。

2. string物件

(一)介紹
字串物件的編碼可以是int, raw 或者embstr 。
1.如果一個字串物件儲存的是整數值,用long 型別來表示,將字串物件的編碼設定為int。
2.如果字串物件儲存的是一個字串值,並且這個字串值的長度大於32 宇節,那麼字串物件將使用一個簡單動態字串( SDS )來儲存這個字串值,並將物件的編碼設定為raw。
3.如果字串物件儲存的是一個字串值,並且這個字串值的長度小於等於32 位元組,那麼字串物件將使用ernbstr 編碼的方式來儲存這個字串值。
注意1: raw 編碼會呼叫兩次記憶體分配函式來分別建立redisObject 結構和sdshdr 結構,而embstr 編碼則通過呼叫一次記憶體分配函式來分配一塊連續的空間,空間中依次包含redisObject 和sdshdr兩個結構。
這裡寫圖片描述
這裡寫圖片描述
注意2:可以用long double 型別表示的浮點數在Redis 中也是作為宇符串值來儲存的。如果我們要儲存一個浮點數到字串物件裡面,那麼程式會先將這個浮點數轉換成字串值,然後再儲存轉換所得的字串值。在有需要的時候,程式會將儲存在字串物件裡面的字串值轉換回浮點數值,執行某些操作,然後再將執行操作所得的撐點數值轉換回字串值,並繼續儲存在字串物件裡面。
注意3: 字串值,或者因為長度太大而沒辦法用long 型別表示的整數,又或者因為長度太太而沒辦法用long double 型別表示的浮點數。
(二)編碼轉換
int 編碼的字串物件和embstr 編碼的字串物件在條件滿足的情況下,會被轉換
為raw 編碼的字串物件。
Redis 沒有為embstr 編碼的字串物件編寫任何相應的修改程式(只有int 編碼的字串物件和raw 編碼的字串物件有這些程式),所以embstr 編碼的字串物件實際上是隻讀的。當我們對embstr 編碼的字串物件執行任何修改命令時,程式會先將物件的編碼從embstr 轉換成raw ,然後再執行修改命令。
因為這個原因, embstr編碼的字串物件在執行修改命令之後,總會變成一個raw 編碼的字串物件。
(三)字串命令的實現

命令 int 編碼的實現方法 embstr 編碼的實現方法 raw 編碼的實現方法
SET 使用int 編碼儲存值 使用embstr 編碼儲存值 使用raw 編碼儲存值
GET 拷貝物件所儲存的整數值,將直接向客戶端返回字串值,然後向客戶端返回這個字串值 直接向客戶端返回字串值 直接向客戶端返回字串值
APPEND 將物件轉換成raw 編碼,然後按raw 編碼的方式執行此操作 將物件轉換成raw 編碼,然後按raw 編碼的方式執行此操作 呼叫sdscatlen 函式,將給定字串追加到現有字串的末尾
INCRBYFLOAT 取出整數值並將其轉換成long double 型別的撐點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來 取出整數值並將其轉換成long double 型別的撐點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來。如果字串值不能被轉換成浮點數,那麼向客戶端返回一個錯誤 取出整數值並將其轉換成long double 型別的撐點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來。如果字串值不能被轉換成浮點數,那麼向客戶端返回一個錯誤
INCRBY 對整數值進行加法計算,得出的計算結果會作為整數被儲存起來 embstr 編碼不能執行此命令,向客戶端返回一個錯誤 raw 編碼不能執行此命令,向客戶端返回一個錯誤
DECRBY 對整數值進行減法計算,得出的計算結果會作為整數被儲存起來 embstr 編碼不能執行此命令,向客戶端返回一個錯誤 aw 編碼不能執行此命令,向客戶端返回一個錯誤
STRLEN 拷貝物件所儲存的整數值,將這個拷貝轉換成字串值,計算並返回這個字串值的長度 呼叫sdslen 函式,返回字元淨的長度 呼叫sdslen 函式,返回字元淨的長度
SETRANGE 將物件轉換成raw 編碼,然後按raw 編碼的方式制於此命令 將物件轉換成raw 編碼,然後按raw 編碼的方式制於此命令 將字串特定索引上的值設定為給定的字元
GE1頁ANGE 拷貝物件所儲存的整數值,將這個拷貝轉換成字串值,然後取出並返回字串指定索引上的字元 直接取出並返回字串指定索引上的字元 直接取出並返回字串指定索引上的字元

3. 列表物件

(一)介紹
列表物件的編碼可以是ziplist 或者linkedlist 。
redis> RPUSH numbers l ” three” 5
(integer) 3
這裡寫圖片描述
這裡寫圖片描述
其中StringObject 字樣的格子具體如下所示:
這裡寫圖片描述
注意: linkedlist 編碼的列表物件在底層的雙端連結串列結構中包含了多個字串物件,這種巢狀字串物件的行為在稍後介紹的晗希物件、集合物件和有序集合物件中都會出現,字串物件是Redis 五種型別的物件中唯一一種會被其他四種類型物件巢狀的物件。
(二)編碼轉換
當列表物件可以同時滿足以下兩個條件時,列表物件使用ziplist 編碼:
- 列表物件儲存的所有字串元素的長度都小於64 位元組;
- 列表物件儲存的元素數量小於512 個;不能滿足這兩個條件的列表物件需要使用
linkedlist 編碼。
以上兩個條件的上限值是可以修改的,具體請看配直檔案中關於list-max-ziplistvalue選項和list-max-ziplist-entries 選項的說明。
(三)列表命令的實現

命令 ziplist 編碼的實現方法 link回list 編碼的實現方法
LPUSH 呼叫ziplistPush 函式,將新元素推入到壓縮列袤的表頭 呼叫listAddNodeHead 函式,將新元素推入到雙端連結串列的表頭
RPUSH 呼叫ziplistPush 函式,將新元素推入到壓縮列袤的表尾 呼叫listAddNodeTail 函式,將新元素推入到雙端連結串列的表尾
LPOP 呼叫ziplistIndex 函式定位壓縮列表的表頭節點,在向用戶返回節點所儲存的元素之後,呼叫ziplistDelete 函式刪除表頭節點 呼叫 listFirst 函式定位雙端連結串列的表頭節點,在向用戶返回節點所儲存的元素之後,呼叫listDelNode 函式刪除表頭節點
RPOP 呼叫ziplistIndex 函式定位壓縮列表的表尾節點,在向用戶返回節點所儲存的元素之後,呼叫ziplistDelete 函式刪除表尾節點 呼叫 listList 函式定位雙端連結串列的表尾節點,在向用戶返回節點所儲存的元素之後,呼叫listDelNode 函式刪除表尾節點
LINDEX 呼叫ziplistIndex 函式定位壓縮列表的指定節點,然後返回節點所儲存的元素 呼叫list Index 函式定位壓縮列表的指定節點,然後返回節點所儲存的元素
LLEN 呼叫ziplistLen 函式返回壓縮列表的長度 呼叫listLength 函式返回雙端鏈袤的長度
LINSERT 插入新節點到壓縮列表的表頭或者表尾時,使用ziplistPush 函式z 插入新節點到壓縮列表的其他位置時,使用ziplistlnsert 函式 呼叫listlnsertNode 函式,將新節點插入到雙端連結串列的指定位置
LREM 遍歷壓縮列表節點,並詞用ziplistDelete函式刪除包含了給定元素的節點 遍歷雙端連結串列節點,並呼叫listDelNode 函式刪除包含了給定元素的節點
LTRIM 詞用ziplistDeleteRange 函式,刪除壓縮列表中所有不在指定索引範圍內的節點 遍歷雙端連結串列節點,並呼叫listDelNode 函式刪除連結串列中所有不在指定索引範圍內的節點
LSET 呼叫ziplistDelete 函式,先刪除壓縮列表指定索引上的現有節點,然後呼叫ziplistInsert函式,將一個包含給定元素的新節點插入到相同索引上面 呼叫listlndex 函式,定位到雙端連結串列指定索引上的節點,然後通過賦值操作更新節點的值

4.雜湊物件

(一)介紹
雜湊物件的編碼可以是ziplist 或者hashtable 。
(1)ziplist 編碼的雜湊物件使用壓縮列表作為底層實現,每當有新的鍵值對要加入到哈
希物件時,程式會先將儲存了鍵的壓縮列表節點推入到壓縮列表表尾,然後再將儲存了值的壓縮列表節點推入到壓縮列表表尾,因此:
- 儲存了同一鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後;
(2)hash table 編碼的雜湊物件使用宇典作為底層實現,雜湊物件中的每個鍵
值對都使用一個字典鍵值對來儲存;
這裡寫圖片描述
這裡寫圖片描述
(二)編碼轉換
當雜湊物件可以同時滿足以下兩個條件時,晗希物件使用ziplist 編碼:
- 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於64 位元組;
- 雜湊物件儲存的鍵值對數量小於512 個;不能滿足這兩個條件的晗希物件需要使用hash table 編碼。
(這兩個條件的上限值是可以修改的,具體請看配直檔案中關於hashmaxziplistvalue選項和hash-max-ziplist-entries 選項的說明。)
(三)雜湊命令的實現
這裡寫圖片描述

5.集合物件

(一)介紹
集合物件的編碼可以是int set 或者hashtable 。
(1)intset 編碼的集合物件使用整數集合作為底層實現
(2)hash table 編碼的集合物件使用字典作為底層實現,字典的每個鍵都是一個字串物件,每個字串物件包含了一個集合元素,而宇典的值則全部被設定為NULL。
這裡寫圖片描述
(二)編碼轉換
當集合物件可以同時滿足以下兩個條件時,物件使用int set 編碼:
- 集合物件儲存的所有元素都是整數值;
- 集合物件儲存的元素數量不超過512 個。
- 不能滿足這兩個條件的集合物件需要使用hash table 編碼。
(第二個條件的上限值是可以修改的,具體請看配置檔案中關於set-max-intsetentries選項的說明。)
(三)雜湊命令的實現

命令 intset 編碼的實現方法 hashtable編碼的實現方法
SADD 呼叫intsetAdd 函式,將所有新元素新增到整數集合裡面 呼叫dictAdd ,以新元素為鍵,NULL為值,將鍵值對新增到字典裡面

這裡寫圖片描述

6.有序集合物件

(一)介紹
有序集合的編碼可以是ziplist 或者skiplist。
有序集合每個元素的成員都是一個字串物件,而每個元素的分值都是一個double 型別的浮點數。值得一提的是,雖然zset 結構同時使用跳躍表和字典來儲存有序集合元素,但這兩種資料結構都會通過指標來共事相同元素的成員和分值,所以同時使用跳躍表和字典來儲存集合元素不會產生任何重複成員或者分值,也不會因此而浪費額外的記憶體。
(1)ziplist 編碼的壓縮列表物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存,第一個節點儲存元素的成員( member ),而二個元素則儲存元素的分值( score )
壓縮列表內的集合元素按分值從小到大進行排序,分值較小的元素被放置在靠近表頭的方向,而分值較大的元素則被放置在靠近表尾的方向。
(2)skiplist 編碼的有序集合物件使用zset 結構作為底層實現,一個zset 結構同時包含一個字典和一個跳躍表:

typedef struct zset {
    zskiplist *zsl;
    diet *dict;

(2-1)zset 結構中的zsl 跳躍表按分值從小到大儲存了所有集合元素,每個跳躍表節點都儲存了一個集合元素:跳躍表節點的object 屬性儲存了元素的成員,而跳躍表節點的score 屬性則儲存了元素的分值。通過這個跳躍表,程式可以對有序集合進行範圍型操作,比如ZRANK、ZRANGE 等命令就是基於跳躍表API 來實現的。
(2-2)zset 結構中的diet 字典為有序集合建立了一個從成員到分值的對映,字典中的每個鍵值對都儲存了一個集合元素:字典的鍵儲存了元素的成員,而字典的值則儲存了元素的分值。通過這個字典,程式可以用O(1)複雜度查詢給定成員的分值,ZSCORE 命令就是根據這一特性實現的,而很多其他有序集合命令都在實現的內部用到了這一特性。
這裡寫圖片描述這裡寫圖片描述
這裡寫圖片描述

(二)編碼轉換
當有序集合物件可以同時滿足以下兩個條件時,物件使用ziplist 編碼:

  • 有序集合儲存的元素數量小於128 個
  • 有序集合儲存的所有元素成員的長度都小於64 位元組

不能滿足以上兩個條件的有序集合物件將使用skiplist 編碼。
(以上兩個條件的上限值是可以修改的,具體請看配直檔案中關於zset-max-ziplistentries選項和zset-max-ziplist-value 選項的說明。)

注意:為什麼有序集合需要同時使用跳躍表和字典來實現?
在理論上,有序集合可以單放使用字典或者跳躍在的其中一種資料結構來實現,但
無論單放使用字典還是跳躍在,在效能上對比起同時使用字典和跳躍表都會有所降低。舉個例子,如果我們只使用字典來實現有序集合,那麼雖然以O(1) 複雜度查詢成員的分值這一特性會被保留,但是,因為字典以無序的方式來儲存集合元素,所以每次在執行範圍型操作一一比如ZRANK、ZRANGE 等命令時,程式都需要對字典儲存的所有元素進行排序,完成這種排序需要至少O(NlogN)時間複雜度,以及額外的O(N) 記憶體空間(因為要建立一個數紐來儲存排序後的元素)。
另一方面,如果我們只使用跳躍在來實現,有序集合,那麼跳躍在執行範圍型操作的所有優點都會被保留,但因為沒有了字典,所以根據成員查詢分值這一操作的複雜度將從0(1) 上升為O(logN)。因為以上原因,為了讓有序集合的查詢和範圍型操作都儘可能快地執行, Redis 選擇了同時使用字典和跳躍在兩種資料結構來實現有序集合。
(三)有序集合命令的實現
這裡寫圖片描述

7.型別檢查與命令多型

1.型別檢查
Redis 中用於操作鍵的命令基本上可以分為兩種型別。
其中一種命令可以對任何型別的鍵執行,比如說DEL 命令EXPIRE 命令RENAME命令TYPE 命令OBJECT命令等。

而另一種命令只能對特定型別的鍵執行,比如說:
SET 、 GET 、 APPEND 、 STRLEN 等命令只能對字串物件的鍵執行;
HDEL 、 HSET 、 HGET 、 HLEN 等命令只能對雜湊物件的鍵執行;
RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能對列表物件的鍵執行;
SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能對集合物件的鍵執行;
ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能對有序集合物件的鍵執行。

在執行一個型別特定命令之前,伺服器會先檢查輸入資料庫鍵的值物件是否為執行命令所需的型別,如果是的話,伺服器就對鍵執行指定的命令;否則,伺服器將拒絕執行命令,並向客戶端返回一個型別錯誤。

2.命令多型
現在,考慮這樣一個情況,如果我們對一個鍵執行LLEN命令,那麼伺服器除了要確保執行命令的是列表鍵之外,還需要根據鍵的值物件所使用的編碼來選擇正確LLEN 命令實現;因為列表物件有ziplist 和linkedlist兩種編碼可用,其中前者使用壓縮列表API 來實現列表命令,而後者則使用雙端連結串列API來實現列表命令。

  • 如果列表物件的編碼為ziplist ,那麼說明列表物件的實現為壓縮列表,程式將使
    用ziplistLen 函式來返回列表的長度;
  • 如果列表物件的編碼為linkedlist ,那麼說明列表物件的實現為雙端連結串列,程式
    將使用listLength 畫數來返回雙端連結串列的長度;

借用面向物件方面的術語來說,我們可以認為LLEN 命令是多型( polymorphism )的,只要執行LLEN 命令的是列表鍵,那麼無論值物件使用的是ziplist 編碼還是linkedlist 編碼,命令都可以正常執行。

實際上,我們可以將DEL 、EXPIRE、π’PE 等命令也稱為多型命令,因為無論輸入的鍵是什麼型別,這些命令都可以正確地執行。DEL 、EXPIRE 等命令和LLEN 等命令的區別在於,前者是基於型別的多型一一一個命令可以同時用於處理多種不同型別的鍵,而後者是基於編碼的多型一一一個命令可以同時用於處理多種不同碼。

這裡寫圖片描述

8.記憶體回收

Redis 在自己的物件系統中構建了一個引用計數( reference counting )技術實現的記憶體回收機制。
每個物件的引用計數資訊由redisObject 結構的ref count 屬性記錄:

int refcount;
  • 在建立一個新物件時,引用計數的值會被初始化為 1;
  • 當物件被一個新程式使用時,它的引用計數值會被增 1;
  • 當物件不再被一個程式使用時,它的引用計數值會被減 1;
  • 當物件的引用計數值變為0 時,物件所佔用的記憶體會被釋放 。

修改物件引用計數的API:

函式 作用
incrRefCount 將物件的引用計數值增一
decrRefCount 將物件的引用計數值減一,當物件的引用計數值等於 0 時,釋放物件
resetRefCount 將物件的引用計數值設定為0,但並不釋放物件,這個函式通常在需要重新設定物件的引用計數時使用
//建立一個字串物件s ,物件的引用計數為1
robj *s = createStringObject( ... )

//物件 s 執行各種操作...

//將物件s 的引用計數減一,使得物件的引用計數變為0
//導致物件s 被釋放
decrRefCount(s)

9.物件共享

在 Redis 中, 讓多個鍵共享同一個值物件需要執行以下兩個步驟:

  • 將資料庫鍵的值指標指向一個現有的值物件;
  • 將被共享的值物件的引用計數增一。

這裡寫圖片描述
需要注意的是:
目前來說, Redis 會在初始化伺服器時,建立一萬個字串物件,這些物件包含了從0到9999 的所有整數值,當伺服器需要用到值為0 到9999 的字串物件時,伺服器就會使用這些共享物件,而不是新建立物件。
(建立共享字串物件的數量可以通過修改redis.h/REDIS SHARED INTEGERS 常量來修改。)

舉例說明這一萬個字串物件:
OBJECT REFCOUNT 命令檢視鍵A的值物件的引用計數,此時會發現值物件的引用計數為2;

引用這個值物件的兩個程式分別是持有這個值物件的伺服器程式 以及共事這個值物件的鍵A;如果這時我們再建立一個值為100 的鍵B,那麼鍵B 也會指向包含整數值100 的共事物件,使得共事物件的引用計數值變為3。

另外,這些共事物件不單單隻有字串鍵可以使用,那些在資料結構中嵌套了字串物件的物件( linkedlist 編碼的列表物件、hashtable 編碼的晴希物件、hashtable 編碼的集合物件,以及zset 編碼的有序集合物件)都可以使用這些共事物件。

10.物件的空轉時長

unsigned lru:REDIS_LRU_BITS; 

lru 屬性記錄了物件最後一次被命令程式訪問的時間:
OBJECT IDLETIME 命令可以打印出給定鍵的空轉時長,這一空轉時長就是通過將當前時間減去鍵的值物件的 lru 時間計算得出的。

除了可以被OBJECT IDLETIME 命令打印出來之外,鍵的空轉時長還有另外一項作用:如果伺服器打開了maxmemory 選項,並且伺服器用於回收記憶體的演算法為volatile-lru或者allkeys-lru ,那麼當伺服器佔用的記憶體數超過了maxmemory 選項所設定的上限值時,空轉時長較高的那部分鍵會優先被伺服器釋放,從而回收記憶體
配置檔案的maxmemory 選項和maxmemory-policy 選項的說明介紹了關於這方面
的更多資訊。

小結

  • Redis 資料庫中的每個鍵值對的鍵和值都是一個物件。
  • Redis 共有字串、列表、晗希、集合、有序集合五種型別的物件,每種型別的物件至少都有兩種或以上的編碼方式,不同的編碼可以在不同的使用場景上優化物件的使用效率。
  • 伺服器在執行某些命令之前,會先檢查結定鍵的型別能否執行指定的命令,而檢查
    一個鍵的型別就是檢查鍵的值物件的型別。
  • Redis 的物件系統帶有引用計數實現的記憶體回收機制,當一個物件不再被使用時,該物件所佔用的記憶體就會被自動釋放。
  • Redis 會預設共享值為0 到9999 的字串物件,從第一次使用,refcount就為 2。
  • 物件會記錄自己的最後一次被訪問的時間,這個時間可以用於計算物件的空轉時間。