標籤: 銷售文案

  • 深入理解JVM(③)各種垃圾收集算法

    深入理解JVM(③)各種垃圾收集算法

    前言

    從如何判定對象消亡的角度出發,垃圾收集算法可以劃分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”。由於主流Java虛擬機中使用 的都是“追蹤式垃圾收集”,所以後續介紹的垃圾收集算法都是屬於追蹤式的垃圾收集。

    分代式收集理論

    當前商業虛擬機的垃圾收集器,大多數都遵循了“分代收集”的理論進行設計。
    主要簡歷在兩個分代假說之上:
    1、弱分代假說:絕大多數對象都是“朝生夕滅”的。
    2、強分代假說:熬過越多此垃圾收集過程的對象就越難以消亡。
    這兩個分代假說奠定了多款常用的垃圾收集器的一致設計原則:收集器應該將Java堆劃分出不同的區域,然後將回收對象依據其年齡(對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。
    把分代收集理論具體放到現在商用的Java虛擬機里,設計者一般至少會把Java堆劃分為新生代(Young Generation) 和 老年代(Old Generation兩個區域。在新生代中,每次垃圾收集時都有大批對象死去,而每次回收后存活的少量對象,將會逐步晉陞到老年代中存放。

    標記-清除算法

    標記-清除算法,分為“標記”和“清除”兩個階段:首先標記所有需要回收的對象,標記完成后,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。
    這個算法有兩個主要的缺點:
    第一個是執行效率不穩定,如果Java堆中有大部分是需要回收的對象,這個會進行大量標記和清除動作,導致標記和清除兩個過程的執行效率隨着對象數量增長而降低。
    第二個是內存碎片化問題,標記、清除之後會產生大量不連續的內存碎片,空間碎片太多會導致當需要大對象時找不到足夠的連續內存,而提前觸發另一次垃圾收集動作。
    因為這兩個缺點的原因,才會產生後續一些針對於修復這兩個缺點的算法。
    標記清除算法示意圖:

    標記複製算法

    標記複製算法也被簡稱Wie複製算法,為了解決標記清除算法面對大量可回收對象時執行效率低的問題,而產生的一種稱為“半區複製”的垃圾收集算法。
    原理是:將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊當這一塊內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
    這種算法不用考慮空間碎片化,只需要移動堆指針,按順序分配即可,實現簡單,運行高效,但缺點也是顯而易見的,就是將可用內存縮小了原來的一半。
    標記複製算法示意圖:

    由於新生代里的對象“朝生夕滅”,針對這個特點,又產生了一種更優化的半區複製分代策略,稱為“Appel式回收”。具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只是用Eden和其中一塊Survivor。當發生垃圾收集時,將Eden和Survivor中任然存活的對象一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和Survivor空間。
    HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說每次可利用的空間為新生代的90%,只有10%的空間會暫時“浪費”。
    如果另外一塊兒Survivor沒有足夠的空間存放存活的對象了,這些對象將通過分配擔保機制直接進入到老年代。

    標記整理算法

    標記複製算法在對象存活率較高時就要進行較多的複製操作,效率將會降低。更關鍵的是,如果不浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
    針對老年代對象的存亡特徵,產生了另外一種有針對性的“標記整理”算法。標記的過程和“標記-清除”算法一樣,也是判斷對象是否屬於垃圾的過程。但後續步驟是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。
    標記整理算法示意圖:

    在這種算法中,在移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種移動操作必須在暫停用戶應用程序才能進行(也就是“Stop The World”)。但是不移動又會造成內存空間碎片化。所以各有利弊,從垃圾收集的停頓時間來看,不移動對象停頓時間更短,但從整個程序的吞吐量來看,移動對象會更划算。所以要依情況而定。
    還有一種“和稀泥”的解決方案,就是平時採用標記清除算法,直到內存空間碎片化程度已經大到影響對象分配時,再採用標記整理算法收集一次,以獲得規整的內存空間。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • Python3 源碼閱讀 – 垃圾回收機制

    Python3 源碼閱讀 – 垃圾回收機制

    Python的垃圾回收機制包括了兩大部分:

    • 引用計數(大部分在 Include/object.h 中定義)
    • 標記清除+隔代回收(大部分在 Modules/gcmodule.c 中定義)

    1. 引用計數機制

    python中萬物皆對象,他的核心結構是:PyObject

    typedef __int64 ssize_t;
    
    typedef ssize_t         Py_ssize_t;
    
    typedef struct _object {
        _PyObject_HEAD_EXTRA
        Py_ssize_t ob_refcnt;   // Py_ssize_t __int64
        struct _typeobject *ob_type;
    } PyObject;
    
    typedef struct {
        PyObject ob_base;
        Py_ssize_t ob_size; /* Number of items in variable part */
    } PyVarObject;
    

    PyObject是每個對象的底層數據結構,其中ob_refcnt就是作為引用計數。當一個對象有新的引用時, 它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少,當引用技術為0時,該對象的生命結束了。

    1. 引用計數+1的情況
      • 對象被創建 eg: a=2
      • 對象被引用 eg: b=a
      • 對象被作為參數,傳入到一個函數中,例如func(a)
      • 對象作為一個元素,存儲在容器中,例如list1=[a, b]
    2. 引用計數-1的情況
      • 對象的別名被显示的銷毀 eg: del a
      • 對象的別名被賦予新的對象 eg: a=34
      • 一個對象離開它的作用域, 例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
      • 對象所在的容器被銷毀,或者從容器中刪除

    如何查看對象的引用計數

    import sys
    a = 'hello'
    sys.getrefcount(a)   
    // 注意: getrefcount(a) 傳入a時, a的引用計數會加1
    

    1.1 什麼時候觸發回收

    當一個對象的引用計數變為了 0, 會直接進入釋放空間的流程

    /* cpython/Include/object.h */
    static inline void _Py_DECREF(const char *filename, int lineno,
                                  PyObject *op)
    {
        _Py_DEC_REFTOTAL;
        if (--op->ob_refcnt != 0) {
    #ifdef Py_REF_DEBUG
            if (op->ob_refcnt < 0) {
                _Py_NegativeRefcount(filename, lineno, op);
            }
    #endif
        }
        else {
        	/* // _Py_Dealloc 會找到對應類型的 descructor, 並且調用這個 descructor
            destructor dealloc = Py_TYPE(op)->tp_dealloc;
            (*dealloc)(op);
            */
            _Py_Dealloc(op);
        }
    }
    

    2. 常駐內存對象

    引用計數機制所帶來的維護引用計數的額外操作,與python運行中所進行的內存分配、釋放、引用賦值的次數是成正比的,這一點,相對於主流的垃圾回收技術,比如標記–清除(mark--sweep)、停止–複製(stop--copy)等方法相比是一個弱點,因為它們帶來額外操作只和內存數量有關,至於多少人引用了這塊內存則不關心。因此為了與引用計數搭配、在內存的分配和釋放上獲得最高的效率,python設計了大量的內存池機制,比如小整數對象池、字符串的intern機制,列表的freelist緩衝池等等,這些大量使用的面向特定對象的內存池機制正是為了彌補引用計數的軟肋。

    2.1 小整數對象池

    #ifndef NSMALLPOSINTS
    #define NSMALLPOSINTS           257
    #endif
    #ifndef NSMALLNEGINTS
    #define NSMALLNEGINTS           5
    #endif
    
    #if NSMALLNEGINTS + NSMALLPOSINTS > 0
    /* Small integers are preallocated in this array so that they
       can be shared.
       The integers that are preallocated are those in the range
       -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
    */
    static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
    
    Py_INCREF(op)  增加對象引用計數
    
    Py_DECREF(op)  減少對象引用計數, 如果計數位0, 調用_Py_Dealloc
    
    _Py_Dealloc(op) 調用對應類型的 tp_dealloc 方法
    

    小整數對象池就是一個PyLongObject 數組, 大小=257+5=262, 範圍是[-5, 257) 注意左閉右開.

    python對小整數的定義是[-5, 257), 這些整數對象是提前建立好的,不會被垃圾回收,在一個python程序中,所有位於這個範圍內的整數使用的都是同一個對象

    2.2 大整數對象池

    疑惑:《Python源碼剖析》提到的整數對象池block_list應該已經不存在了(因為PyLongObject為變長對象)。Python2中的PyIntObject實際是對c中的long的包裝。所以Python2也提供了專門的緩存池,供大整數輪流使用,避免每次使用不斷的malloc分配內存帶來的效率損耗,可參考劉志軍老師的講解。既然沒有池了,malloc/free會帶來的不小性能損耗。Guido認為Py3.0有極大的優化空間,在字符串和整形操作上可以取得很好的優化結果。

    /* Allocate a new int object with size digits.
       Return NULL and set exception if we run out of memory. */
    
    #define MAX_LONG_DIGITS \
        ((PY_SSIZE_T_MAX - offsetof(PyLongObject, ob_digit))/sizeof(digit))
    
    PyLongObject *
    _PyLong_New(Py_ssize_t size)
    {
        PyLongObject *result;
        /* Number of bytes needed is: offsetof(PyLongObject, ob_digit) +
           sizeof(digit)*size.  Previous incarnations of this code used
           sizeof(PyVarObject) instead of the offsetof, but this risks being
           incorrect in the presence of padding between the PyVarObject header
           and the digits. */
        if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
            PyErr_SetString(PyExc_OverflowError,
                            "too many digits in integer");
            return NULL;
        }
        result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
                                 size*sizeof(digit));
        if (!result) {
            PyErr_NoMemory();
            return NULL;
        }
        return (PyLongObject*)PyObject_INIT_VAR(result, &PyLong_Type, size);
    }
    

    result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) + size*sizeof(digit));

    每一個大整數,均創建一個新的對象。id(num)均不同。

    2.4 字符串的intern機制

    Objects/unicodeobject.c
    Objects/codeobject.c
    

    PyStringObject對象的intern機制之目的是:對於被intern之後的字符串,比如“Ruby”,在整個Python的運行期間,系統中都只有唯一的一個與字符串“Ruby”對應的PyStringObject對象。這樣當判斷兩個PyStringObject對象是否相同時,如果它們都被intern了,那麼只需要簡單地檢查它們對應的PyObject*是否相同即可。這個機制既節省了空間,又簡化了對PyStringObject對象的比較,嗯,可謂是一箭雙鵰哇。

    摘自:《Python源碼剖析》 — 陳儒

    Python3中PyUnicodeObject對象的intern機制和Python2的PyStringObject對象intern機制一樣,主要為了節省內存的開銷,利用字符串對象的不可變性,對存在的字符串對象重複利用

    In [50]: a = 'python'
    
    In [51]: b = 'python'
    
    In [52]: id(a)
    Out[52]: 442782398256
    
    In [53]: id(b)
    Out[53]: 442782398256
    
    In [54]: b = 'hello python'
    
    In [55]: a = 'hello python'
    
    In [56]: id(a)
    Out[56]: 442808585520
    
    In [57]: id(b)
    Out[57]: 442726541488
    

    什麼樣的字符串會使用intern機制?

    intern機制跟編譯時期有關,相關代碼在Objects/codeobject.c

    /* Intern selected string constants */
    static int
    intern_string_constants(PyObject *tuple)
    {
        int modified = 0;
        Py_ssize_t i;
    
        for (i = PyTuple_GET_SIZE(tuple); --i >= 0; ) {
            PyObject *v = PyTuple_GET_ITEM(tuple, i);
            if (PyUnicode_CheckExact(v)) {
                if (PyUnicode_READY(v) == -1) {
                    PyErr_Clear();
                    continue;
                }
                if (all_name_chars(v)) {
                    PyObject *w = v;
                    PyUnicode_InternInPlace(&v);
                    if (w != v) {
                        PyTuple_SET_ITEM(tuple, i, v);
                        modified = 1;
                    }
                }
            }
            /*....*/
    }
        
    /* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */
    static int
    all_name_chars(PyObject *o)
    {
        const unsigned char *s, *e;
    
        if (!PyUnicode_IS_ASCII(o))
            return 0;
    
        s = PyUnicode_1BYTE_DATA(o);
        e = s + PyUnicode_GET_LENGTH(o);
        for (; s != e; s++) {
            if (!Py_ISALNUM(*s) && *s != '_')
                return 0;
        }
        return 1;
    }
    
    

    可見 all_name_chars 決定了是否會 intern,簡單來說就是 ascii 字母,数字和下劃線組成的字符串會被緩存。但是不僅如此。2.5還會說

    /* This dictionary holds all interned unicode strings.  Note that references
       to strings in this dictionary are *not* counted in the string's ob_refcnt.
       When the interned string reaches a refcnt of 0 the string deallocation
       function will delete the reference from this dictionary.
    
       Another way to look at this is that to say that the actual reference
       count of a string is:  s->ob_refcnt + (s->state ? 2 : 0)
    */
    static PyObject *interned = NULL;
    /*省略*/
    void
    PyUnicode_InternInPlace(PyObject **p)
    {
        PyObject *s = *p;
        PyObject *t;
    #ifdef Py_DEBUG
        assert(s != NULL);
        assert(_PyUnicode_CHECK(s));
    #else
        if (s == NULL || !PyUnicode_Check(s))
            return;
    #endif
        /* If it's a subclass, we don't really know what putting
           it in the interned dict might do. */
        if (!PyUnicode_CheckExact(s))
            return;
        // [1]
        if (PyUnicode_CHECK_INTERNED(s))
            return;
        if (interned == NULL) {
            interned = PyDict_New();
            if (interned == NULL) {
                PyErr_Clear(); /* Don't leave an exception */
                return;
            }
        }
        Py_ALLOW_RECURSION
        // [2]
        t = PyDict_SetDefault(interned, s, s);
        Py_END_ALLOW_RECURSION
        if (t == NULL) {
            PyErr_Clear();
            return;
        }
        // [3]
        if (t != s) {
            Py_INCREF(t);
            Py_SETREF(*p, t);
            return;
        }
        // [4]
        /* The two references in interned are not counted by refcnt.
           The deallocator will take care of this */
        Py_REFCNT(s) -= 2;
        _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
    }
    

    通過函數我們可以得知,python中維護這一個interned變量的指針,這個變量指向PyDict_New創建的對象,而PyDict_New實際上創建了一個PyDictObject對象,是Python中dict類型的對象。實際上intern機制就是維護一個字典,這個字典中記錄著被intern機制處理過的字符串對象,[1]PyUnicode_CHECK_INTERNED宏檢查字符串對象的state.interned是否被標記,

    如果字符串對象的state.interned被標記了,就直接返回;[2]嘗試把沒有被標記的字符串對象s作為key-value加入interned字典中;[3]處表示字符串對象s已經在interned字典中(對應的value值是字符串對象t),(通過Py_SETREF宏來改變p指針的指向),且原字符串對象p會因引用計數為零被回收。Py_SETREF宏在Include/object.h定義着:

    /* Safely decref `op` and set `op` to `op2`.
     *
     * As in case of Py_CLEAR "the obvious" code can be deadly:
     *
     *     Py_DECREF(op);
     *     op = op2;
     *
     * The safe way is:
     *
     *      Py_SETREF(op, op2);
     *
     * That arranges to set `op` to `op2` _before_ decref'ing, so that any code
     * triggered as a side-effect of `op` getting torn down no longer believes
     * `op` points to a valid object.
     *
     * Py_XSETREF is a variant of Py_SETREF that uses Py_XDECREF instead of
     * Py_DECREF.
     */
    
    #define Py_SETREF(op, op2)                      \
        do {                                        \
            PyObject *_py_tmp = (PyObject *)(op);   \
            (op) = (op2);                           \
            Py_DECREF(_py_tmp);                     \
        } while (0)
    

    [4]中把新加入interned字典中的字符串對象做減引用操作,並把state.interned標記成SSTATE_INTERNED_MORTALSSTATE_INTERNED_MORTAL表示字符串對象被intern機制處理,但會隨着引用計數被回收;interned標記還有另外一種SSTATE_INTERNED_IMMORTAL,表示被intern機制處理但對象不可銷毀,會與Python解釋器同在。PyUnicode_InternInPlace只能創建SSTATE_INTERNED_MORTAL狀態的字符串,要想創建SSTATE_INTERNED_IMMORTAL狀態的字符串需要通過另外一個接口,強制改變intern的狀態

    void
    PyUnicode_InternImmortal(PyObject **p)
    {
        PyUnicode_InternInPlace(p);
        if (PyUnicode_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
            _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL;
            Py_INCREF(*p);
        }
    }
    

    為什麼引用Py_REFCNT(s) -= 2;要-2呢?

    PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *defaultobj)
    {
        PyDictObject *mp = (PyDictObject *)d;
        PyObject *value;
        Py_hash_t hash;
    
        /*...*/
        if (ix == DKIX_EMPTY) {
            /*...*/
            Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
            ep0 = DK_ENTRIES(mp->ma_keys);
            ep = &ep0[mp->ma_keys->dk_nentries];
            dictkeys_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
            Py_INCREF(key);
            Py_INCREF(value);
            /*...*/
        return value;
    }
    

    對於被intern機制處理了的PyStringObject對象,Python採用了特殊的引用計數機制。在將一個PyStringObject對象a的PyObject指針作為key和value添加到interned中時,PyDictObject對象會通過這兩個指針對a的引用計數進行兩次加1的操作。但是Python的設計者規定在interned中a的指針不能被視為對象a的有效引用,因為如果是有效引用的話,那麼a的引用計數在Python結束之前永遠都不可能為0,因為interned中至少有兩個指針引用了a,那麼刪除a就永遠不可能了,這顯然是沒有道理的。
    摘自:《Python源碼剖析》 — 陳儒

    注意:實際上,即使Python會對一個字符串進行intern機制的處理,也會先創建一個PyUnicodeObject對象,然後檢查在interned字典中是否有值和其相同,存在的話就將interned字典保存的value值返回,之前臨時創建的字符串對象會由於引用計數為零而回收。

    是否可以直接對C原生對象做intern的動作呢?不需要創建臨時對象

    事實上CPython確實提供了以char * 為參數的intern機制相關函數,但是,也是一樣的創建temp在設置intern.

    PyUnicode_InternImmortal(PyObject **p)
    {
        PyUnicode_InternInPlace(p);
        if (PyUnicode_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
            _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL;
            Py_INCREF(*p);
        }
    }
    

    為什麼需要臨時對象?

    因為PyDict_SetDefault() 操作的是PyDictObject對象,而該對象必須以PyObject*指針作為鍵

    2.5 字符緩衝池(單字符)

    python為小整數對象準備了小整數對象池,當然對於常用的字符,python對應的也建了字符串緩衝池,因為 python3 中通過 unicode_latin1[256] 將長度為 1 的 ascii 的字符也緩存了

    /* Single character Unicode strings in the Latin-1 range are being
       shared as well. */
    static PyObject *unicode_latin1[256] = {NULL};
    
    unicode_decode_utf8(){
        /*省略*/
        /* ASCII is equivalent to the first 128 ordinals in Unicode. */
        if (size == 1 && (unsigned char)s[0] < 128) {
            if (consumed)
                *consumed = 1;
            return get_latin1_char((unsigned char)s[0]);
        }
        /*省略*/
    }
    
    
    static PyObject*
    get_latin1_char(unsigned char ch)
    {
        PyObject *unicode = unicode_latin1[ch];
        if (!unicode) {
            unicode = PyUnicode_New(1, ch);
            if (!unicode)
                return NULL;
            PyUnicode_1BYTE_DATA(unicode)[0] = ch;
            assert(_PyUnicode_CheckConsistency(unicode, 1));
            unicode_latin1[ch] = unicode;
        }
        Py_INCREF(unicode);
        return unicode;
    }
    
    In [46]: a = 'p'
    
    In [47]: b = 'p'
    
    In [48]: id(a)
    Out[48]: 442757120384
    
    In [49]: id(b)
    Out[49]: 442757120384
    

    當然單字符也包括空字符。

    /* The empty Unicode object is shared to improve performance. */
    static PyObject *unicode_empty = NULL;
    
    In [8]: a = 'hello' + 'python'
    
    In [9]: b = 'hellopython'
    
    In [10]: a is b
    Out[10]: True
    
    In [11]: a = 'hello ' + 'python'
    
    In [12]: b = 'hello python'
    
    In [13]: id(a)
    Out[13]: 118388503536
    
    In [14]: id(b)
    Out[14]: 118387544240
    
    In [15]: 'hello ' + 'python' is 'hello python'
    Out[15]: False
    
    In [16]: 'hello_' + 'python' is 'hello_python'
    Out[16]: True
    

    2.6 小結:

    • 小整數[-5, 257)共用對象,常駐內存

    • 單個字母,長度為 1 的 ascii 的字符latin1會被interned, 包括空字符,共用對象,常駐內存

    • 由字母、数字、下劃線([a-zA-Z0-9_])組成的字符串,不可修改,默認開啟intern機制,共用對象,引用計數為0時,銷毀

    • 字符串(含有空格),不可修改,沒開啟intern機制,不共用對象,引用計數為0,銷毀

    3. 標記清除+分代回收

    為了防止出現循環引用的致命性問題,python採用的是引用計數機製為主,標記-清除和分代收集兩種機製為輔的策略

    我們設置 n1.next 指向 n2,同時設置 n2.prev 指回 n1,現在,我們的兩個節點使用循環引用的方式構成了一個`雙向鏈表`,同時請注意到 ABC 以及 DEF 的引用計數值已經增加到了2,現在,假定我們的程序不再使用這兩個節點了,我們將 n1 和 n2 都設置為None,Python會像往常一樣將每個節點的引用計數減少到1。

    ### 3.1 在python中的零代(Generation Zero)

    Ruby使用一個鏈表(free_list)來持續追蹤未使用的、自由的對象,Python使用一種不同的鏈表來持續追蹤活躍的對象。而不將其稱之為“活躍列表”,Python的內部C代碼將其稱為零代(Generation Zero)。每次當你創建一個對象或其他什麼值的時候,Python會將其加入零代鏈表:

    從上邊可以看到當我們創建ABC節點的時候,Python將其加入零代鏈表。請注意到這並不是一個真正的列表,並不能直接在你的代碼中訪問,事實上這個鏈表是一個完全內部的Python運行時。

    疑惑1:對於容器對象(比如list、dict、class、instance等等),是在什麼時候綁定GC,放入第0鏈表呢?

    相似的,當我們創建DEF節點的時候,Python將其加入同樣的鏈表:

    現在零代包含了兩個節點對象。(他還將包含Python創建的每個其他值,與一些Python自己使用的內部值。)

    3.2 標記循環引用

    當達到某個 閾值之後 解釋器會循環遍歷,循環遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放對象。以下例子便於理解:

    從上面可以看到 ABC 和 DEF 節點包含的引用數為1.有三個其他的對象同時存在於零代鏈表中,藍色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。

    通過識別內部引用,Python能夠減少許多零代鏈表對象的引用計數。在上圖的第一行中你能夠看見ABC和DEF的引用計數已經變為零了,這意味着收集器可以釋放它們並回收內存空間了。剩下的活躍的對象則被移動到一個新的鏈表:一代鏈表。

    疑惑2: 內部如何識別零代的循環引用計數,在什麼閾值下會觸發GC執行?

    3.3 在源碼中摸索答案

    Python通過PyGC_Head來跟蹤container對象,PyGC_Head信息位於PyObject_HEAD之前,定義在Include/objimpl.h

    typedef union _gc_head {
        struct {
            union _gc_head *gc_next;
            union _gc_head *gc_prev;
            Py_ssize_t gc_refs;
        } gc;
        double dummy;  /* force worst-case alignment */
    } PyGC_Head;
    

    表頭數據結構

    //Include/internal/mem.h
    struct gc_generation {
          PyGC_Head head;
          int threshold; /* collection threshold */  // 閾值
          int count; /* count of allocations or collections of younger
                        generations */    // 實時個數
      };
    

    Python中用於分代垃圾收集的三個“代”由_gc_runtime_state.generations數組所表示着:

    解答疑惑2,三個代的閾值如下數組

    /* If we change this, we need to cbhange the default value in the
       signature of gc.collect. */
    #define NUM_GENERATIONS 3
    
    _PyGC_Initialize(struct _gc_runtime_state *state)
    {
        state->enabled = 1; /* automatic collection enabled? */
    
    #define _GEN_HEAD(n) (&state->generations[n].head)
        struct gc_generation generations[NUM_GENERATIONS] = {
            /* PyGC_Head,                                 threshold,      count */
            {{{_GEN_HEAD(0), _GEN_HEAD(0), 0}},           700,            0},
            {{{_GEN_HEAD(1), _GEN_HEAD(1), 0}},           10,             0},
            {{{_GEN_HEAD(2), _GEN_HEAD(2), 0}},           10,             0},
        };
        for (int i = 0; i < NUM_GENERATIONS; i++) {
            state->generations[i] = generations[i];
        };
        state->generation0 = GEN_HEAD(0);
        struct gc_generation permanent_generation = {
              {{&state->permanent_generation.head, &state->permanent_generation.head, 0}}, 0, 0
        };
        state->permanent_generation = permanent_generation;
    }
    

    **解答疑惑1:那container對象是什麼時候加入第0“代”的container對象鏈表呢?**

    對於python內置對象的創建,container對象是通過PyObject_GC_New函數來創建的,而非container對象是通過PyObject_Malloc函數來創建的。

    // Include/objimpl.h
    #define PyObject_GC_New(type, typeobj) \
                    ( (type *) _PyObject_GC_New(typeobj) )
    
    
    // 調用了Modules/gcmodule.c中的_PyObject_GC_New函數:
    PyObject *
    _PyObject_GC_New(PyTypeObject *tp)
    {
        PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
        if (op != NULL)
            op = PyObject_INIT(op, tp);
        return op;
    }
    
    static PyObject *
    _PyObject_GC_Alloc(int use_calloc, size_t basicsize)
    {
        PyObject *op;
        PyGC_Head *g;
        size_t size;
        if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
            return PyErr_NoMemory();
        size = sizeof(PyGC_Head) + basicsize;
        // [1]  申請PyGC_Head和對象本身的內存
        if (use_calloc)
            g = (PyGC_Head *)PyObject_Calloc(1, size);
        else
            g = (PyGC_Head *)PyObject_Malloc(size);
        if (g == NULL)
            return PyErr_NoMemory();
        // [2] 設置gc_refs的值
        g->gc.gc_refs = 0;
        _PyGCHead_SET_REFS(g, GC_UNTRACKED);
        // [3]
        generations[0].count++; /* number of allocated GC objects */
        if (generations[0].count > generations[0].threshold &&
            enabled &&
            generations[0].threshold &&
            !collecting &&
            !PyErr_Occurred()) {
            collecting = 1;
            collect_generations();
            collecting = 0;
        }
        // [4]  FROM_GC宏定義可以通過PyGC_Head地址轉換PyObject_HEAD地址,逆運算是AS_GC宏定義。
        op = FROM_GC(g);
        return op;
    }
    
    PyObject *
    _PyObject_GC_Malloc(size_t basicsize)
    {
        return _PyObject_GC_Alloc(0, basicsize);
    }
    

    [4] FROM_GC宏定義可以通過PyGC_Head地址轉換PyObject_HEAD地址,逆運算是AS_GC宏定義。

    /* Get an object's GC head */
    #define AS_GC(o) ((PyGC_Head *)(o)-1)
    
    /* Get the object given the GC head */
    #define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
    

    當觸發閾值后,是如何進行GC回收的?

    collect是垃圾回收的主入口函數。特別注意 finalizers 與 python 的__del__綁定了

    /* This is the main function.  Read this to understand how the
     * collection process works. */
    static Py_ssize_t
    collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
            int nofail)
    {
        int i;
        Py_ssize_t m = 0; /* # objects collected */
        Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
        PyGC_Head *young; /* the generation we are examining */
        PyGC_Head *old; /* next older generation */
        PyGC_Head unreachable; /* non-problematic unreachable trash */
        PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
        PyGC_Head *gc;
        _PyTime_t t1 = 0;   /* initialize to prevent a compiler warning */
    
        struct gc_generation_stats *stats = &_PyRuntime.gc.generation_stats[generation];
        
        ...
    
        // “標記-清除”前的準備
        
        // 垃圾標記
    
        // 垃圾清除
      
        ...
    
        /* Update stats */
        if (n_collected)
            *n_collected = m;
        if (n_uncollectable)
            *n_uncollectable = n;
        stats->collections++;
        stats->collected += m;
        stats->uncollectable += n;
    
        if (PyDTrace_GC_DONE_ENABLED())
            PyDTrace_GC_DONE(n+m);
    
        return n+m;
    }
    

    3.3.1 標記-清除前的準備

        // [1]
        /* update collection and allocation counters */
        if (generation+1 < NUM_GENERATIONS)
            _PyRuntime.gc.generations[generation+1].count += 1;
        for (i = 0; i <= generation; i++)
            _PyRuntime.gc.generations[i].count = 0;
    
        // [2]
        /* merge younger generations with one we are currently collecting */
        for (i = 0; i < generation; i++) {
            gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
        }
    
        // [3]
        /* handy references */
        young = GEN_HEAD(generation);
        if (generation < NUM_GENERATIONS-1)
            old = GEN_HEAD(generation+1);
        else
            old = young;
    
        // [4]
        /* Using ob_refcnt and gc_refs, calculate which objects in the
         * container set are reachable from outside the set (i.e., have a
         * refcount greater than 0 when all the references within the
         * set are taken into account).
         */
        update_refs(young);
        subtract_refs(young);
    

    [1] 先更新了將被回收的“代”以及老一“代”的count計數器。
    這邊對老一“代”的count計數器增量1就可以看出來在第1“代”和第2“代”的count值其實表示的是該代垃圾回收的次數。
    [2] 通過gc_list_merge函數將這些“代”合併成一個鏈表。

    /* append list `from` onto list `to`; `from` becomes an empty list */
    static void
    gc_list_merge(PyGC_Head *from, PyGC_Head *to)
    {
        PyGC_Head *tail;
        assert(from != to);
        if (!gc_list_is_empty(from)) {
            tail = to->gc.gc_prev;
            tail->gc.gc_next = from->gc.gc_next;
            tail->gc.gc_next->gc.gc_prev = tail;
            to->gc.gc_prev = from->gc.gc_prev;
            to->gc.gc_prev->gc.gc_next = to;
        }
        gc_list_init(from);
    }
    
    static void
    gc_list_init(PyGC_Head *list)
    {
        list->gc.gc_prev = list;
        list->gc.gc_next = list;
    }
    

    gc_list_merge函數將from鏈錶鏈接到to鏈表末尾並把from鏈表置為空鏈表。

    [3] 經過合併操作之後,所有需要被進行垃圾回收的對象都鏈接到young“代”(滿足超過閾值的最老“代”),並記錄old“代”,後面需要將不可回收的對象移到old“代”。

    鏈表的合併操作:

    [4] 尋找root object集合

    要對合併的鏈表進行垃圾標記,首先需要尋找root object集合。
    所謂的root object即是一些全局引用和函數棧中的引用。這些引用所用的對象是不可被刪除的。

    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)
    a = list1
    del list1
    del list2
    

    上面的Python中循環引用的代碼,變量a所指向的對象就是root object。

    三色標記模型

    3.3.2 垃圾標記

    // [1]
    /* Leave everything reachable from outside young in young, and move
         * everything else (in young) to unreachable.
         * NOTE:  This used to move the reachable objects into a reachable
         * set instead.  But most things usually turn out to be reachable,
         * so it's more efficient to move the unreachable things.
         */
    gc_list_init(&unreachable);
    move_unreachable(young, &unreachable);
    
    // [2]
    /* Move reachable objects to next generation. */
    if (young != old) {
        if (generation == NUM_GENERATIONS - 2) {
            _PyRuntime.gc.long_lived_pending += gc_list_size(young);
        }
        gc_list_merge(young, old);
    }
    else {
        /* We only untrack dicts in full collections, to avoid quadratic
               dict build-up. See issue #14775. */
        untrack_dicts(young);
        _PyRuntime.gc.long_lived_pending = 0;
        _PyRuntime.gc.long_lived_total = gc_list_size(young);
    }
    

    [1] 初始化不可達鏈表,調用move_unreachable函數將循環引用的對象移動到不可達鏈表中:

    /* Move the unreachable objects from young to unreachable.  After this,
     * all objects in young have gc_refs = GC_REACHABLE, and all objects in
     * unreachable have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All tracked
     * gc objects not in young or unreachable still have gc_refs = GC_REACHABLE.
     * All objects in young after this are directly or indirectly reachable
     * from outside the original young; and all objects in unreachable are
     * not.
     */
    static void
    move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
    {
        PyGC_Head *gc = young->gc.gc_next;
    
        /* Invariants:  all objects "to the left" of us in young have gc_refs
         * = GC_REACHABLE, and are indeed reachable (directly or indirectly)
         * from outside the young list as it was at entry.  All other objects
         * from the original young "to the left" of us are in unreachable now,
         * and have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All objects to the
         * left of us in 'young' now have been scanned, and no objects here
         * or to the right have been scanned yet.
         */
    
        while (gc != young) {
            PyGC_Head *next;
    
            if (_PyGCHead_REFS(gc)) {
                /* gc is definitely reachable from outside the
                 * original 'young'.  Mark it as such, and traverse
                 * its pointers to find any other objects that may
                 * be directly reachable from it.  Note that the
                 * call to tp_traverse may append objects to young,
                 * so we have to wait until it returns to determine
                 * the next object to visit.
                 */
                PyObject *op = FROM_GC(gc);
                traverseproc traverse = Py_TYPE(op)->tp_traverse;
                assert(_PyGCHead_REFS(gc) > 0);
                _PyGCHead_SET_REFS(gc, GC_REACHABLE);
                (void) traverse(op,
                                (visitproc)visit_reachable,
                                (void *)young);
                next = gc->gc.gc_next;
                if (PyTuple_CheckExact(op)) {
                    _PyTuple_MaybeUntrack(op);
                }
            }
            else {
                /* This *may* be unreachable.  To make progress,
                 * assume it is.  gc isn't directly reachable from
                 * any object we've already traversed, but may be
                 * reachable from an object we haven't gotten to yet.
                 * visit_reachable will eventually move gc back into
                 * young if that's so, and we'll see it again.
                 */
                next = gc->gc.gc_next;
                gc_list_move(gc, unreachable);
                _PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE);
            }
            gc = next;
        }
    }
    

    這邊遍歷young“代”的container對象鏈表,_PyGCHead_REFS(gc)判斷是不是root object或從某個root object能直接/間接引用的對象,由於root object集合中的對象是不能回收的,因此,被這些對象直接或間接引用的對象也是不能回收的。

    _PyGCHead_REFS(gc)為0並不能斷定這個對象是可回收的,但是還是先移動到unreachable鏈表中,設置了GC_TENTATIVELY_UNREACHABLE標誌表示暫且認為是不可達的,如果是存在被root object直接或間接引用的對象,這樣的對象還會被移出unreachable鏈表中。

    [2] 將可達的對象移到下一“代”。

    3.3.3 垃圾清除

    // [1]
        /* All objects in unreachable are trash, but objects reachable from
         * legacy finalizers (e.g. tp_del) can't safely be deleted.
         */
        gc_list_init(&finalizers);
        move_legacy_finalizers(&unreachable, &finalizers);
        /* finalizers contains the unreachable objects with a legacy finalizer;
         * unreachable objects reachable *from* those are also uncollectable,
         * and we move those into the finalizers list too.
         */
        move_legacy_finalizer_reachable(&finalizers);
    
        // [2]
        /* Collect statistics on collectable objects found and print
         * debugging information.
         */
        for (gc = unreachable.gc.gc_next; gc != &unreachable;
                        gc = gc->gc.gc_next) {
            m++;
        }
    
        // [3]
        /* Clear weakrefs and invoke callbacks as necessary. */
        m += handle_weakrefs(&unreachable, old);
    
        // [4]
        /* Call tp_finalize on objects which have one. */
        finalize_garbage(&unreachable);
    
        // [5]
        if (check_garbage(&unreachable)) {
            revive_garbage(&unreachable);
            gc_list_merge(&unreachable, old);
        }
        else {
            /* Call tp_clear on objects in the unreachable set.  This will cause
             * the reference cycles to be broken.  It may also cause some objects
             * in finalizers to be freed.
             */
            delete_garbage(&unreachable, old);
        }
        
        // [6]
        /* Collect statistics on uncollectable objects found and print
         * debugging information. */
        for (gc = finalizers.gc.gc_next;
             gc != &finalizers;
             gc = gc->gc.gc_next) {
            n++;
        }
        
        ...
    
        // [7]
        /* Append instances in the uncollectable set to a Python
         * reachable list of garbage.  The programmer has to deal with
         * this if they insist on creating this type of structure.
         */
        (void)handle_legacy_finalizers(&finalizers, old);
        
        /* Clear free list only during the collection of the highest
         * generation */
        if (generation == NUM_GENERATIONS-1) {
            clear_freelists();
        }
    

    [1] 處理unreachable鏈表中有finalizer的對象。即python中 實現了__del__魔法方法的對象

    /* Move the objects in unreachable with tp_del slots into `finalizers`.
     * Objects moved into `finalizers` have gc_refs set to GC_REACHABLE; the
     * objects remaining in unreachable are left at GC_TENTATIVELY_UNREACHABLE.
     */
    static void
    move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
    {
        PyGC_Head *gc;
        PyGC_Head *next;
    
        /* March over unreachable.  Move objects with finalizers into
         * `finalizers`.
         */
        for (gc = unreachable->gc.gc_next; gc != unreachable; gc = next) {
            PyObject *op = FROM_GC(gc);
    
            assert(IS_TENTATIVELY_UNREACHABLE(op));
            next = gc->gc.gc_next;
    
            if (has_legacy_finalizer(op)) {
                gc_list_move(gc, finalizers);
                _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            }
        }
    }
    

    遍歷unreachable鏈表,將擁有finalizer的實例對象移到finalizers鏈表中,並標示為GC_REACHABLE

    /* Return true if object has a pre-PEP 442 finalization method. */
    static int
    has_legacy_finalizer(PyObject *op)
    {
        return op->ob_type->tp_del != NULL;
    }
    

    擁有finalizer的實例對象指的就是實現了tp_del函數的對象。

    /* Move objects that are reachable from finalizers, from the unreachable set
     * into finalizers set.
     */
    static void
    move_legacy_finalizer_reachable(PyGC_Head *finalizers)
    {
        traverseproc traverse;
        PyGC_Head *gc = finalizers->gc.gc_next;
        for (; gc != finalizers; gc = gc->gc.gc_next) {
            /* Note that the finalizers list may grow during this. */
            traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
            (void) traverse(FROM_GC(gc),
                            (visitproc)visit_move,
                            (void *)finalizers);
        }
    }
    

    finalizers鏈表中擁有finalizer的實例對象遍歷其引用對象,調用visit_move訪問者,這些被引用的對象也不應該被釋放。

    /* A traversal callback for move_legacy_finalizer_reachable. */
    static int
    visit_move(PyObject *op, PyGC_Head *tolist)
    {
        if (PyObject_IS_GC(op)) {
            if (IS_TENTATIVELY_UNREACHABLE(op)) {
                PyGC_Head *gc = AS_GC(op);
                gc_list_move(gc, tolist);
                _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            }
        }
        return 0;
    }
    
    #define IS_TENTATIVELY_UNREACHABLE(o) ( \
        _PyGC_REFS(o) == GC_TENTATIVELY_UNREACHABLE)
    

    visit_move函數將引用對象還在unreachable鏈表的對象移到finalizers鏈表中。

    [2] 統計unreachable鏈表數量。
    [3] 處理弱引用。
    [4] [5] 開始清除垃圾對象,我們先只看delete_garbage函數:

    /* Break reference cycles by clearing the containers involved.  This is
     * tricky business as the lists can be changing and we don't know which
     * objects may be freed.  It is possible I screwed something up here.
     */
    static void
    delete_garbage(PyGC_Head *collectable, PyGC_Head *old)
    {
        inquiry clear;
    
        while (!gc_list_is_empty(collectable)) {
            PyGC_Head *gc = collectable->gc.gc_next;
            PyObject *op = FROM_GC(gc);
    
            if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {
                PyList_Append(_PyRuntime.gc.garbage, op);
            }
            else {
                if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                    Py_INCREF(op);
                    clear(op);
                    Py_DECREF(op);
                }
            }
            if (collectable->gc.gc_next == gc) {
                /* object is still alive, move it, it may die later */
                gc_list_move(gc, old);
                _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            }
        }
    }
    

    遍歷unreachable鏈表中的container對象,調用其類型對象的tp_clear指針指向的函數,我們以list對象為例:

    static int
    _list_clear(PyListObject *a)
    {
        Py_ssize_t i;
        PyObject **item = a->ob_item;
        if (item != NULL) {
            /* Because XDECREF can recursively invoke operations on
               this list, we make it empty first. */
            i = Py_SIZE(a);
            Py_SIZE(a) = 0;
            a->ob_item = NULL;
            a->allocated = 0;
            while (--i >= 0) {
                Py_XDECREF(item[i]);
            }
            PyMem_FREE(item);
        }
        /* Never fails; the return value can be ignored.
           Note that there is no guarantee that the list is actually empty
           at this point, because XDECREF may have populated it again! */
        return 0;
    }
    

    _list_clear函數對container對象的每個元素進行引用數減量操作並釋放container對象內存。

    delete_garbage在對container對象進行clear操作之後,還會檢查是否成功,如果該container對象沒有從unreachable鏈表上摘除,表示container對象還不能銷毀,需要放回到老一“代”中,並標記GC_REACHABLE

    [6] 統計finalizers鏈表數量。
    [7] 處理finalizers鏈表的對象。

    /* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable
     * only from such cycles).
     * If DEBUG_SAVEALL, all objects in finalizers are appended to the module
     * garbage list (a Python list), else only the objects in finalizers with
     * __del__ methods are appended to garbage.  All objects in finalizers are
     * merged into the old list regardless.
     * Returns 0 if all OK, <0 on error (out of memory to grow the garbage list).
     * The finalizers list is made empty on a successful return.
     */
    static int
    handle_legacy_finalizers(PyGC_Head *finalizers, PyGC_Head *old)
    {
        PyGC_Head *gc = finalizers->gc.gc_next;
    
        if (_PyRuntime.gc.garbage == NULL) {
            _PyRuntime.gc.garbage = PyList_New(0);
            if (_PyRuntime.gc.garbage == NULL)
                Py_FatalError("gc couldn't create gc.garbage list");
        }
        for (; gc != finalizers; gc = gc->gc.gc_next) {
            PyObject *op = FROM_GC(gc);
    
            if ((_PyRuntime.gc.debug & DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
                if (PyList_Append(_PyRuntime.gc.garbage, op) < 0)
                    return -1;
            }
        }
    
        gc_list_merge(finalizers, old);
        return 0;
    }
    

    遍歷finalizers鏈表,將擁有finalizer的實例對象放到一個名為garbage的PyListObject對象中,可以通過gc模塊查看。

    >>> import gc
    >>> gc.garbage
    

    並把finalizers鏈表晉陞到老一“代”。

    注意:__del__給gc帶來的影響, gc模塊唯一處理不了的是循環引用的類都有__del__方法,所以項目中要避免定義__del__方法 官方警告

    3.4 小結

    1. GC的流程:

      -> 發現超過閾值了
      -> 觸發垃圾回收
      -> 將所有可達對象鏈表放到一起
      -> 遍歷, 計算有效引用計數
      -> 分成 有效引用計數=0 和 有效引用計數 > 0 兩個集合
      -> 大於0的, 放入到更老一代
      -> =0的, 執行回收
      -> 回收遍歷容器內的各個元素, 減掉對應元素引用計數(破掉循環引用)
      -> 執行-1的邏輯, 若發現對象引用計數=0, 觸發內存回收
      -> 由python底層內存管理機制回收內存
      
    2. 觸發GC的條件

      • 主動調用gc.collect(),

      • 當gc模塊的計數器達到閥值的時候

      • 程序退出的時候

    4. GC閾值

    分代回收 以空間換時間

    重要思想:將系統中的所有內存塊根據其存活的時間劃分為不同的集合, 每個集合就成為一個”代”, 垃圾收集的頻率隨着”代”的存活時間的增大而減小(活得越長的對象, 就越不可能是垃圾, 就應該減少去收集的頻率)

    弱代假說

    分代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創建的,而一個來的對象則是經過了幾個時間周期之後仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。

    為什麼要這麼做?這種算法的根源來自於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:

    首先是年親的對象通常死得也快,而老對象則很有可能存活更長的時間。

    假定我們創建了一個Python創建:

    n1 = Node("ABC")
    

    根據假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,並且隨着方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。

    通過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對象。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變量。

    5. Python中的gc模塊使用

    gc模塊默認是開啟自動回收垃圾的,gc.isenabled()=True

    常用函數:

    • gc.set_debug(flags) 設置gc的debug日誌,一般設置為gc.DEBUG_LEAK
    """
    DEBUG_STATS - 在垃圾收集過程中打印所有統計信息
    DEBUG_COLLECTABLE - 打印發現的可收集對象
    DEBUG_UNCOLLECTABLE - 打印unreachable對象(除了uncollectable對象)
    DEBUG_SAVEALL - 將對象保存到gc.garbage(一個列表)裏面,而不是釋放它
    DEBUG_LEAK - 對內存泄漏的程序進行debug (everything but STATS).
        
    """
    
    • gc.collect([generation]) 顯式進行垃圾回收,可以輸入參數,0代表只檢查第一代的對象,1代表檢查一,二代的對象,2代表檢查一,二,三代的對象,如果不傳參數,執行一個full collection,也就是等於傳2。 返回不可達(unreachable objects)對象的數目

    • gc.get_threshold() 獲取的gc模塊中自動執行垃圾回收的頻率

    • gc.get_stats()查看每一代的具體信息

    • gc.set_threshold(threshold0[, threshold1[, threshold2]) 設置自動執行垃圾回收的頻率

    • gc.get_count() 獲取當前自動執行垃圾回收的計數器,返回一個長度為3的列表

      例如(488,3,0),其中488是指距離上一次一代垃圾檢查,Python分配內存的數目減去釋放內存的數目,注意是內存分配,而不是引用計數的增加。

      3是指距離上一次二代垃圾檢查,一代垃圾檢查的次數,同理,0是指距離上一次三代垃圾檢查,二代垃圾檢查的次數。

    計數器和閾值關係解釋:

    當計數器從(699,3,0)增加到(700,3,0),gc模塊就會執行gc.collect(0),即檢查一代對象的垃圾,並重置計數器為(0,4,0)
    當計數器從(699,9,0)增加到(700,9,0),gc模塊就會執行gc.collect(1),即檢查一、二代對象的垃圾,並重置計數器為(0,0,1)
    當計數器從(699,9,9)增加到(700,9,9),gc模塊就會執行gc.collect(2),即檢查一、二、三代對象的垃圾,並重置計數器為(0,0,0)
    

    6. 工作中如何避免循環引用?

    To avoid circular references in your code, you can use weak references, that are implemented in the weakref module. Unlike the usual references, the weakref.ref doesn’t increase the reference count and returns None if an object was destroyed. rushter

    import weakref
    
    
    class Node():
        def __init__(self, value):
            self.value = value
            self._parent = None
            self.children = []
    
        def __repr__(self):
            return 'Node({!r:})'.format(self.value)
    
        @property
        def parent(self):
            return None if self._parent is None else self._parent()
    
        @parent.setter
        def parent(self, node):
            self._parent = weakref.ref(node)
    
        def add_child(self, child):
            self.children.append(child)
            child.parent = self
    
    
    if __name__ == '__main__':
    
        a = Data()
        del a
    
        a = Node()
        del a
    
        a = Node()
        a.add_child(Node())
        del a
    

    弱引用消除了引用循環的這個問題,本質來講,弱引用就是一個對象指針,它不會增加它的引用計數

    弱引用的主要用途是實現保存大對象的高速緩存或映射,但又並希望大對象僅僅因為它出現在高速緩存或映射中而保持存活

    為了訪問弱引用所引用的對象,你可以像函數一樣去調用它即可。如果那個對象還存在就會返回它,否則就返回一個None。 由於原始對象的引用計數沒有增加,那麼就可以去刪除它了

    並非所有對象都可以被弱引用;可以被弱引用的對象包括類實例,用 Python(而不是用 C)編寫的函數,實例方法、集合、凍結集合,某些 文件對象,生成器,類型對象,套接字,數組,雙端隊列,正則表達式模式對象以及代碼對象等。

    幾個內建類型如 listdict 不直接支持弱引用,但可以通過子類化添加支持:

    class Dict(dict):
        pass
    
    obj = Dict(red=1, green=2, blue=3)   # this object is weak referenceable
    

    其他內置類型例如 tupleint 不支持弱引用,即使通過子類化也不支持

    python Cookbook 書中推薦弱引用來處理循環引用

    假設我們想創建一個類,用它的實例來代表臨時目錄。 當以下事件中的某一個發生時,這個目錄應當與其內容一起被刪除:

    • 對象被作為垃圾回收,
    • 對象的 remove() 方法被調用,或
    • 程序退出。

    原本用__del__()方法

    class TempDir:
        def __init__(self):
            self.name = tempfile.mkdtemp()
           
       	def __remove(self):
            if self.name is not None:
                shutil.rmtree(self.name)
                self.name = None
        
        @property
        def removed(self):
            return self.name is None
       
    	def __del__(self):
            self.__remove()
    

    更健壯的替代方式可以是定義一個終結器,只引用它所需要的特定函數和對象,而不是獲取對整個對象狀態的訪問權:

    class TempDir:
        def __init__(self):
            self.name = tempfile.mkdtemp()
            self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
           
       	def remove(self):
            self._finalizer()
        
        @property
        def removed(self):
            return not self._finalizer.alive
    

    像這樣定義后,我們的終結器將只接受一個對其完成正確清理目錄任務所需細節的引用。 如果對象一直未被作為垃圾回收,終結器仍會在退出時被調用.weakref

    參考文章和書籍:

    1. visualizing garbage collection in ruby and python
    2. 膜拜的大佬-Junnplus’blog
    3. wklken前輩
    4. The Garbage Collector
    5. Garbage collection in Python: things you need to know
    6. Python-CookBook-循環引用數據結構的內存管理
    7. 《python源碼剖析》
    8. Python-3.8.3/Modules/gcmodule.c

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

    ※教你寫出一流的銷售文案?

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

  • 【譯】Introducing YARP Preview 1

    1 YARP

        YARP是一個項目,用於創建反向代理服務器。它開始於我們注意到來自微軟內部團隊的一系列問題。他們要麼為其服務構建反向代理,要麼詢問 API 和用於構建 API 的技術。因此我們決定讓他們聚在一起開發一個通用解決方案,該解決方案形成了YARP。

        YARP是一個反向代理工具包,用於使用 ASP.NET 和 .NET 中的基礎設施在 .NET 中構建代理服務器。YARP 的主要區別是,它被設計為易於自定義和調整,以滿足不同方案的特定需求。YARP 插入ASP.NET管道以處理傳入請求,然後它擁有自己的子管道,用於執行將請求代理到後端服務器的步驟。客戶可以添加其他module,或根據需要更換常備module。

        隨着其開發已基本到位,我們製作了 YARP 的第一個正式版本(Preview 1),以便更好地協作並獲得反饋。

    2 Preview 1 是什麼

      • 核心代理的基礎結構
      • 基於配置的路由定義
      • 擴展性的管道模型
      • Forwarded標頭(硬編碼)
      • 目標 .NET Core 3.1 和 .NET Core 5

    3 Preview 1 不包括

      • 會話親和性(又稱會話保持)
      • Forwarded標頭(可配置)
      • 基於代碼的路由定義和預請求路由
      • 指標和日誌
      • 性能調整
      • 連接篩選

    4 快速開始

    Step 01 下載.net framework

        YARP 適用於 .NET Core 3.1 或 .NET 5 Preview 4(或更高版本)。

    Step 02 創建一個ASP.NET Core項目

    Step 03 打開項目,添加引用,確保其包含

    <PropertyGroup>
        <TargetFramework>netcoreapp5.0</TargetFramework>
    </PropertyGroup>

      和

    <ItemGroup>
        <PackageReference Include="Microsoft.ReverseProxy" Version="1.0.0-preview.1.*" />
    </ItemGroup>

    Step 04 Startup.cs

      YARP 當前使用配置文件來定義代理的路由和終結點。在ConfigureServices方法中加載。

    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddReverseProxy()
            .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
    }

      Configure方法定義ASP.NET的請求處理管道。反向代理插入到ASP.NET的終結點路由,然後具有其自己的代理子管道。在這裏,可以添加代理管道模塊(如負載均衡)來自定義請求的處理。

    /// <summary>
    /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    /// </summary>
    public void Configure(IApplicationBuilder app)
    {
        app.UseHttpsRedirection();
    
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapReverseProxy(proxyPipeline =>
            {
                proxyPipeline.UseProxyLoadBalancing();
            });
        });
    }

    Step 05 配置

      YARP 的配置定義在appsettings.json中:

    "ReverseProxy": {
        "Routes": [
          {
            "RouteId": "app1",
            "BackendId": "backend1",
            "Match": {
              "Methods": [ "GET", "POST" ],
              "Host": "localhost",
              "Path": "/app1/"
            }
          },
          {
            "RouteId": "route2",
            "BackendId": "backend2",
            "Match": {
              "Host": "localhost"
            }
          }
        ],
        "Backends": {
          "backend1": {
            "LoadBalancing": {
              "Mode": "Random"
            },
            "Destinations": {
              "backend1_destination1": {
                "Address": "https://example.com:10000/"
              },
              "backend1_destination2": {
                "Address": "http://example.com:10001/"
              }
            }
          },
          "backend2": {
            "Destinations": {
              "backend2_destination1": {
                "Address": "https://example.com:10002/"
              }
            }
          }
        }
      }
      • Backends:請求可以路由到的服務器群集。
      • Destinations:是用於指標、日誌記錄和會話保持的標識符。
      • Address:URL前綴(基地址)
      • Routes:根據請求的各個方面(如主機名、路徑、方法、請求標頭等)將傳入請求映射到後端群集。路由是有序的,因此,需要首先定義 app1 路由,因為 route2 將作為尚未匹配的所有路徑的 catchall。

      好啦,先介紹到這裏。

    原文鏈接

      https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/?utm_source=vs_developer_news&utm_medium=referral

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • C#9.0 終於來了,您還學的動嗎? 帶上VS一起解讀吧!(應該是全網第一篇)

    C#9.0 終於來了,您還學的動嗎? 帶上VS一起解讀吧!(應該是全網第一篇)

    一:背景

    1. 講故事

    好消息,.NET 5.0 終於在2020年6月10日發布了第五個預覽版,眼尖的同學一定看到了在這個版本中終於支持了 C# 9.0,此處有掌聲,太好了!!!

    .Net5官方鏈接

    可以看到目前的C#9還是預覽版,實現了一部分新語法供開發者提前嘗鮮,從github的roslyn倉庫上可以看到目前準備實現 17個新特性,現階段已經實現了8個,其中的 In Progress 表示正在開發中。

    新特性預覽

    2. 安裝必備

    • 下載最新的net5 sdk吧: dotnet-sdk-5.0.100-preview.5.20279.10-win-x64.exe

    • 下載最新的 visual studio 2019 preview 2

    找好你自己的vs版本類型哦。。。

    二:新特性研究

    1. Target-typed new

    這個取名一定要留給學易經的大師傅,沒見過世面的我不敢造次,取得不佳影響時運,所謂 運去金成鐵, 時來鐵似金 ,不過大概意思就是說直接new你定義的局部變量的類型,用issues中總結的話就是:

    
    Summary: Allow Point p = new (x, y);
    Shipped in preview in 16.7p1.
    
    

    接下來就是全部代碼,看看使用前使用后 的具體差別。

    
        class Program
        {
            static void Main(string[] args)
            {
                //老語法
                var person = new Person("mary", "123456");
    
                //新語法
                Person person2 = new("mary", "123456");
    
                Console.WriteLine($"person={person}person2={person2}");
            }
        }
    
        public class Person
        {
            private string username;
            private string password;
    
            public Person(string username, string password)
            {
                this.username = username;
                this.password = password;
            }
    
            public override string ToString()
            {
                return $"username={username},password={password} \n";
            }
        }
    
    

    然後用ilspy去看看下面的il代碼,是不是省略了Person,讓自己心裏踏實一點。

    總的來說這語法還行吧,能起到延長鍵盤使用壽命的功效。

    2. Lambda discard parameters

    從字面上看大概就是說可以在lambda上使用取消參數,聽起來怪怪的,那本意是什麼呢?有時候lambda上的匿名方法簽名的參數是不需要的,但在以前必須實打實的定義,這樣就會污染方法體,也就是可以在body中被訪問,如下圖:

    但有時候因為客觀原因必須使用Func<int,int,int>這樣的委託,而且還不想讓方法簽名的參數污染方法體,我猜測在函數式編程中有這樣的場景吧,可能有點類似MVC中的EmptyResult效果。

    好了,我想你大概知道啥意思了,接下來實操一把。。。

    
        Func<int, int, int> func = (_, _) =>
        {
            return 0;
        };
    
        var result = func(10, 20);
    
    

    從圖中可以看到,我在方法體中是找不到所謂的 _ 變量的,這就神奇了,怎麼做到的呢? 帶着這個好奇心看看它的IL代碼是個什麼樣子。

    
    .method private hidebysig static 
    	void Main (
    		string[] args
    	) cil managed 
    {
    	// Method begins at RVA 0x2048
    	// Code size 45 (0x2d)
    	.maxstack 3
    	.entrypoint
    	.locals init (
    		[0] class [System.Runtime]System.Func`3<int32, int32, int32> func,
    		[1] int32 result
    	)
    
    	IL_0000: nop
    	IL_0001: ldsfld class [System.Runtime]System.Func`3<int32, int32, int32> ConsoleApp1.Program/'<>c'::'<>9__0_0'
    	IL_0006: dup
    	IL_0007: brtrue.s IL_0020
    
    	IL_0009: pop
    	IL_000a: ldsfld class ConsoleApp1.Program/'<>c' ConsoleApp1.Program/'<>c'::'<>9'
    	IL_000f: ldftn instance int32 ConsoleApp1.Program/'<>c'::'<Main>b__0_0'(int32, int32)
    	IL_0015: newobj instance void class [System.Runtime]System.Func`3<int32, int32, int32>::.ctor(object, native int)
    	IL_001a: dup
    	IL_001b: stsfld class [System.Runtime]System.Func`3<int32, int32, int32> ConsoleApp1.Program/'<>c'::'<>9__0_0'
    
    	IL_0020: stloc.0
    	IL_0021: ldloc.0
    	IL_0022: ldc.i4.s 10
    	IL_0024: ldc.i4.s 20
    	IL_0026: callvirt instance !2 class [System.Runtime]System.Func`3<int32, int32, int32>::Invoke(!0, !1)
    	IL_002b: stloc.1
    	IL_002c: ret
    } // end of method Program::Main
    
    

    從上面的IL代碼來看 匿名方法 變成了<>c類的<Main>b__0_0方法,完整簽名: ConsoleApp1.Program/'<>c'::'<Main>b__0_0'(int32, int32),然後再找一下 <Main>b__0_0 方法的定義。

    
    .class nested private auto ansi sealed serializable beforefieldinit '<>c'
    	extends [System.Runtime]System.Object
    	.method assembly hidebysig 
    		instance int32 '<Main>b__0_0' (
    			int32 _,
    			int32 _
    		) cil managed 
    	{
    		// Method begins at RVA 0x2100
    		// Code size 7 (0x7)
    		.maxstack 1
    		.locals init (
    			[0] int32
    		)
    
    		IL_0000: nop
    		IL_0001: ldc.i4.0
    		IL_0002: stloc.0
    		IL_0003: br.s IL_0005
    
    		IL_0005: ldloc.0
    		IL_0006: ret
    	} // end of method '<>c'::'<Main>b__0_0'
    
    

    這說明什麼呢? 說明兩個參數是真實存在的,但編譯器搗了鬼,做了語法上的限制,不讓你訪問所謂的 _

    等等。。。有一個問題,IL中的方法簽名怎麼是這樣的: <Main>b__0_0 (int32 _,int32 _) , 大家應該知道方法簽名中不可以出現重複的參數名,比如下面這樣定義肯定是報錯的。

    這說明什麼? 說明這個語法糖不僅需要編譯器支持,更需要底層的JIT支持,那怎麼證明呢?我們用windbg去底層挖一挖。。。為了方便調試,修改如下:

    
            static void Main(string[] args)
            {
                Func<int, int, int> func = (_, _) =>
                {
                    Console.WriteLine("進入方法體了!!!");
                    Console.ReadLine();
                    return 0;
                };
    
                var result = func(10, 20);
            }
    
    0:000> !clrstack -p
    OS Thread Id: 0x52e8 (0)
    0000007035F7E5C0 00007ffaff362655 ConsoleApp1.Program+c.b__0_0(Int32, Int32) [C:\5\ConsoleApp1\ConsoleApp1\Program.cs @ 13]
        PARAMETERS:
            this (0x0000007035F7E600) = 0x000001968000cb48
            _ (0x0000007035F7E608) = 0x000000000000000a
            _ (0x0000007035F7E610) = 0x0000000000000014
    

    從圖中可以看到,雖然都是 _ ,但在線程棧上是完完全全的兩個棧地址。 0x0000007035F7E6080x0000007035F7E610

    三:總結

    總的來說,C#是越來越向函數式編程靠攏,越來越像Scala,就像Jquery的口號一樣: Write less,do more。

    好了,先就說這兩個吧,大家先安裝好工具,明天繼續解剖~~~

    如您有更多問題與我互動,掃描下方進來吧~

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • Accord.NET重啟4.0 開發

    Accord.NET重啟4.0 開發

    Accord.NET Framework是在AForge.NET基礎上封裝和進一步開發來的。功能也很強大,因為AForge.NET更注重與一些底層和廣度,而Accord.NET Framework更注重與機器學習這個專業,在其基礎上提供了更多統計分析和處理函數,包括圖像處理和計算機視覺算法,所以側重點不同,但都非常有用。 官方網站:http://accord-framework.net/

    在項目中斷2年時間之後,作者cesarsouza 在2020年5月1日更新了項目狀態, 他在歐洲完成博士,雖然他的工作中主要使用Python完成他的工作,但是他喜歡C#/.NET,一直在考慮Accprd.NET的發展問題,5月15日重新設定了4.0 版本的路線圖https://github.com/accord-net/framework/issues/2123,  其中他寫道:“我看到這個項目仍然被認為對許多人有用,我不認為讓項目消亡符合任何人的利益。我最初認為這個項目將由ML.NET取代,但事實並非如此。我們可以轉換框架,轉而與它合作。”

    我們在ML.NET的最初宣布文章中有Accord.NET的影子:

    CNTK 已經死了,目前只有 Tensoflow.NET在蓬勃發展,發展的情況很不錯,隨着Accord.NET的加入,這個生態又重新激活,期待大家一起加入,推動.NET機器學習生態的發展。

    (一)框架的三大功能模塊

    Accord.NET框架主要有三個大的功能性模塊。

    • 分別為科學技術,
    • 信號與圖像處理,
    • 支持組件。

    下面將對3個模型的命名空間和功能進行簡單介紹。可以讓大家更快的接觸和了解其功能是否是自己想要的,下面是主要的命名空間介紹。

    (二) 科學計算

    Accord.Math:包括矩陣擴展程序,以及一組矩陣數值計算和分解的方法,也包括一些約束和非約束問題的數值優化算法,還有一些特殊函數以及其他一些輔助工具。

    Accord.Statistics:包含概率分佈、假設檢驗、線性和邏輯回歸等統計模型和方法,隱馬爾科夫模型,(隱藏)條件隨機域、主成分分析、偏最小二乘判別分析、內核方法和許多其他相關的技術。

    Accord.MachineLearning: 為機器學習應用程序提供包括支持向量機,決策樹,樸素貝恭弘=叶 恭弘斯模型,k-means聚類算法,高斯混合模型和通用算法如Ransac,交叉驗證和網格搜索等算法。

    Accord.Neuro:包括大量的神經網絡學習算法,如Levenberg-Marquardt,Parallel Resilient Backpropagation,Nguyen-Widrow初始化算法,深層的信念網絡和許多其他神經網絡相關的算法。具體看參考幫助文檔。

    (三)信號與圖像處理

    Accord.Imaging:包含特徵點探測器(如Harris, SURF, FAST and  FREAK),圖像過濾器、圖像匹配和圖像拼接方法,還有一些特徵提取器。

    Accord.Audio:包含一些機器學習和統計應用程序說需要的處理、轉換過濾器以及處理音頻信號的方法。

    Accord.Vision:實時人臉檢測和跟蹤,以及對人流圖像中的一般的檢測、跟蹤和轉換方法,還有動態模板匹配追蹤器。

    (四) 支持組件

    主要是為上述一些組件提供數據显示,繪圖的控件,分為以下幾個命名空間:

    Accord.Controls:包括科學計算應用程序常見的柱狀圖、散點圖和表格數據瀏覽。

    Accord.Controls.Imaging:包括用來显示和處理的圖像的WinForm控件,包含一個方便快速显示圖像的對話框。

    Accord.Controls.Audio:显示波形和音頻相關性信息的WinForm控件。

    Accord.Controls.Vision:包括跟蹤頭部,臉部和手部運動以及其他計算機視覺相關的任務WinForm控件。

    (五) 支持的算法介紹

    下面將Accord.NET框架包括的主要功能算法按照類別進行介紹。來源主要是官網介紹,進行了簡單的翻譯和整理。

    1、分類(Classification)

    SVM(支持向量機,類SupportVectorMachine、類KernelSupportVectorMachine、類SequentialMinimalOptimization—序列最小優化算法)、

    K-NN鄰近算法(類KNearestNeighbors);

    Logistic Regression(邏輯回歸)、

    Decision Trees(決策樹,類DecisionTree、ID3Learning、C45Learning)、

    Neural Networks(神經網絡)、

    Deep Learning(深度學習)

    (Deep Neural Networks深層神經網絡)、

    Levenberg-Marquardt with Bayesian Regularization、

    Restricted Boltzmann Machines(限制玻耳茲曼機)、

    Sequence classification (序列分類),

    Hidden Markov Classifiers and Hidden Conditional Random Fields(隱馬爾科夫分類器和隱藏條件隨機域)。

    2、回歸(Regression)

    Multiple linear regression(多元線性回歸-單因變量多自變量)、

    SimpleLinearRegression(線性回歸,類SimpleLinearRegression)、

    Multivariate linear regression(多元線性回歸-多因變量多自變量)、polynomial regression (多項式回歸)、logarithmic regression(對數回歸)、Logistic regression(邏輯回歸)、multinomial logistic regression(多項式邏輯回歸)(softmax) and generalized linear models(廣義線性模型)、L2-regularized L2-loss logistic regression , L2-regularized logistic regression , L1-regularized logistic regression , L2-regularized logistic regression in the dual form and regression support vector machines。

    3、聚類(Clustering)

    K-Means、K-Modes、Mean-Shift(均值漂移)、Gaussian Mixture Models(高斯混合模型)、Binary Split(二元分裂)、Deep Belief Networks(深層的信念網絡)、 Restricted Boltzmann Machines(限制玻耳茲曼機)。聚類算法可以應用於任意數據,包括圖像、數據表、視頻和音頻。

    4、概率分佈(Distributions)

    包括40多個分佈的參數和非參數估計。包括一些常見的分佈如正態分佈、柯西分佈、超幾何分佈、泊松分佈、伯努利;也包括一些特殊的分佈如Kolmogorov-Smirnov , Nakagami、Weibull、and Von-Mises distributions。也包括多元分佈如多元正態分佈、Multinomial 、Independent 、Joint and Mixture distributions。

    5、假設檢驗(Hypothesis Tests)

    超過35統計假設測試,包括單向和雙向方差分析測試、非參數測試如Kolmogorov-Smirnov測試和媒體中的信號測試。contingency table tests such as the Kappa test,with variations for multiple tables , as well as the Bhapkar and Bowker tests; and the more traditional Chi-Square , Z , F , T and Wald tests .

    6、核方法(Kernel Methods)

    內核支持向量機,多類和多標籤向量機、序列最小優化、最小二乘學習、概率學習。Including special methods for linear machines such as LIBLINEAR’s methods for Linear Coordinate Descent , Linear Newton Method , Probabilistic Coordinate Descent , Probabilistic Coordinate Descent in the Dual , Probabilistic Newton Method for L1 and L2 machines in both the dual and primal formulations .

    7、圖像(Imaging)

    興趣和特徵點探測器如Harris,FREAK,SURF,FAST。灰度共生矩陣,Border following,Bag-of-Visual-Words (BoW),RANSAC-based homography estimation , integral images , haralick textural feature extraction , and dense descriptors such as histogram of oriented gradients (HOG) and Local Binary Pattern (LBP).Several image filters for image processing applications such as difference of Gaussians , Gabor , Niblack and Sauvola thresholding。還有幾個圖像處理中經常用到的圖像過濾器。

    8、音頻信號(Audio and Signal)

    音頻信號的加載、解析、保存、過濾和轉換,如在空間域和頻域應用音頻過濾器。WAV文件、音頻捕捉、時域濾波器,高通,低通,波整流過濾器。Frequency-domain operators such as differential rectification filter and comb filter with Dirac’s delta functions . Signal generators for Cosine , Impulse , Square signals.

    9、視覺(Vision)

    實時人臉檢測和跟蹤,以及圖像流中檢測、跟蹤、轉換的一般的檢測方法。Contains cascade definitions , Camshift and Dynamic Template Matching trackers . Includes pre-created classifiers for human faces and some facial features such as noses。

    10、降維技術

    SVD奇異值分解(OctaveEnvironment.svd方法);

    PCA主成分分析(類PrincipalComponent);

    ICA獨立成份分析(類IndependentComponetAnalysis)

    11、算法精度測算

    混淆矩陣(類ConfusionMatrix);

    ROC曲線評估(類ReceiverOperatingCharacteristic);

    Bootstrap算法(自助算法;類(Bootstrap));

    CrossValidation算法(交叉檢驗;類(CrossValidation));

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • redis 數據刪除策略和逐出算法

    redis 數據刪除策略和逐出算法

    數據存儲和有效期

    redis 工作流程中,過期的數據並不需要馬上就要執行刪除操作。因為這些刪不刪除只是一種狀態表示,可以異步的去處理,在不忙的時候去把這些不緊急的刪除操作做了,從而保證 redis 的高效

    數據的存儲

    在redis中數據的存儲不僅僅需要保存數據本身還要保存數據的生命周期,也就是過期時間。在redis 中 數據的存儲結構如下圖:

    獲取有效期

    Redis是一種內存級數據庫,所有數據均存放在內存中,內存中的數據可以通過TTL指令獲取其狀態

    刪除策略

    在內存佔用與CPU佔用之間尋找一種平衡,顧此失彼都會造成整體redis性能的下降,甚至引發服務器宕機或內存泄漏。

    定時刪除

    創建一個定時器,當key設置過期時間,且過期時間到達時,由定時器任務立即執行對鍵的刪除操作

    優點

    節約內存,到時就刪除,快速釋放掉不必要的內存佔用

    缺點

    CPU壓力很大,無論CPU此時負載多高,均佔用CPU,會影響redis服務器響應時間和指令吞吐量

    總結

    用處理器性能換取存儲空間

    惰性刪除

    數據到達過期時間,不做處理。等下次訪問該數據,如果未過期,返回數據。發現已經過期,刪除,返回不存在。這樣每次讀寫數據都需要檢測數據是否已經到達過期時間。也就是惰性刪除總是在數據的讀寫時發生的。

    expireIfNeeded函數

    對所有的讀寫命令進行檢查,檢查操作的對象是否過期。過期就刪除返回過期,不過期就什麼也不做~。

    執行數據寫入過程中,首先通過expireIfNeeded函數對寫入的key進行過期判斷。

    /*
     * 為執行寫入操作而取出鍵 key 在數據庫 db 中的值。
     *
     * 和 lookupKeyRead 不同,這個函數不會更新服務器的命中/不命中信息。
     *
     * 找到時返回值對象,沒找到返回 NULL 。
     */
    robj *lookupKeyWrite(redisDb *db, robj *key) {
    
        // 刪除過期鍵
        expireIfNeeded(db,key);
    
        // 查找並返回 key 的值對象
        return lookupKey(db,key);
    }
    

    執行數據讀取過程中,首先通過expireIfNeeded函數對寫入的key進行過期判斷。

    /*
     * 為執行讀取操作而取出鍵 key 在數據庫 db 中的值。
     *
     * 並根據是否成功找到值,更新服務器的命中/不命中信息。
     *
     * 找到時返回值對象,沒找到返回 NULL 。
     */
    robj *lookupKeyRead(redisDb *db, robj *key) {
        robj *val;
    
        // 檢查 key 釋放已經過期
        expireIfNeeded(db,key);
    
        // 從數據庫中取出鍵的值
        val = lookupKey(db,key);
    
        // 更新命中/不命中信息
        if (val == NULL)
            server.stat_keyspace_misses++;
        else
            server.stat_keyspace_hits++;
    
        // 返回值
        return val;
    }
    

    執行過期動作expireIfNeeded其實內部做了三件事情,分別是:

    • 查看key判斷是否過期
    • 向slave節點傳播執行過期key的動作併發送事件通知
    • 刪除過期key
    /*
     * 檢查 key 是否已經過期,如果是的話,將它從數據庫中刪除。
     *
     * 返回 0 表示鍵沒有過期時間,或者鍵未過期。
     *
     * 返回 1 表示鍵已經因為過期而被刪除了。
     */
    int expireIfNeeded(redisDb *db, robj *key) {
    
        // 取出鍵的過期時間
        mstime_t when = getExpire(db,key);
        mstime_t now;
    
        // 沒有過期時間
        if (when < 0) return 0; /* No expire for this key */
    
        /* Don't expire anything while loading. It will be done later. */
        // 如果服務器正在進行載入,那麼不進行任何過期檢查
        if (server.loading) return 0;
    
        // 當服務器運行在 replication 模式時
        // 附屬節點並不主動刪除 key
        // 它只返回一個邏輯上正確的返回值
        // 真正的刪除操作要等待主節點發來刪除命令時才執行
        // 從而保證數據的同步
        if (server.masterhost != NULL) return now > when;
    
        // 運行到這裏,表示鍵帶有過期時間,並且服務器為主節點
    
        /* Return when this key has not expired */
        // 如果未過期,返回 0
        if (now <= when) return 0;
    
        /* Delete the key */
        server.stat_expiredkeys++;
    
        // 向 AOF 文件和附屬節點傳播過期信息
        propagateExpire(db,key);
    
        // 發送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
            "expired",key,db->id);
    
        // 將過期鍵從數據庫中刪除
        return dbDelete(db,key);
    }
    

    判斷key是否過期的數據結構是db->expires,也就是通過expires的數據結構判斷數據是否過期。
    內部獲取過期時間並返回。

    /*
     * 返回字典中包含鍵 key 的節點
     *
     * 找到返回節點,找不到返回 NULL
     *
     * T = O(1)
     */
    dictEntry *dictFind(dict *d, const void *key)
    {
        dictEntry *he;
        unsigned int h, idx, table;
    
        // 字典(的哈希表)為空
        if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */
    
        // 如果條件允許的話,進行單步 rehash
        if (dictIsRehashing(d)) _dictRehashStep(d);
    
        // 計算鍵的哈希值
        h = dictHashKey(d, key);
        // 在字典的哈希表中查找這個鍵
        // T = O(1)
        for (table = 0; table <= 1; table++) {
    
            // 計算索引值
            idx = h & d->ht[table].sizemask;
    
            // 遍歷給定索引上的鏈表的所有節點,查找 key
            he = d->ht[table].table[idx];
            // T = O(1)
            while(he) {
    
                if (dictCompareKeys(d, key, he->key))
                    return he;
    
                he = he->next;
            }
    
            // 如果程序遍歷完 0 號哈希表,仍然沒找到指定的鍵的節點
            // 那麼程序會檢查字典是否在進行 rehash ,
            // 然後才決定是直接返回 NULL ,還是繼續查找 1 號哈希表
            if (!dictIsRehashing(d)) return NULL;
        }
    
        // 進行到這裏時,說明兩個哈希表都沒找到
        return NULL;
    }
    

    優點

    節約CPU性能,發現必須刪除的時候才刪除。

    缺點

    內存壓力很大,出現長期佔用內存的數據。

    總結

    用存儲空間換取處理器性能

    定期刪除

    周期性輪詢redis庫中時效性數據,採用隨機抽取的策略,利用過期數據佔比的方式刪除頻度。

    優點

    CPU性能佔用設置有峰值,檢測頻度可自定義設置

    內存壓力不是很大,長期佔用內存的冷數據會被持續清理

    缺點

    需要周期性抽查存儲空間

    定期刪除詳解

    redis的定期刪除是通過定時任務實現的,也就是定時任務會循環調用serverCron方法。然後定時檢查過期數據的方法是databasesCron。定期刪除的一大特點就是考慮了定時刪除過期數據會佔用cpu時間,所以每次執行databasesCron的時候會限制cpu的佔用不超過25%。真正執行刪除的是 activeExpireCycle方法。

    時間事件

    對於持續運行的服務器來說, 服務器需要定期對自身的資源和狀態進行必要的檢查和整理, 從而讓服務器維持在一個健康穩定的狀態, 這類操作被統稱為常規操作(cron job

    在 Redis 中, 常規操作由 redis.c/serverCron() 實現, 它主要執行以下操作

    1 更新服務器的各類統計信息,比如時間、內存佔用、數據庫佔用情況等。

    2 清理數據庫中的過期鍵值對。

    3 對不合理的數據庫進行大小調整。

    4 關閉和清理連接失效的客戶端。

    5 嘗試進行 AOF 或 RDB 持久化操作。

    6 如果服務器是主節點的話,對附屬節點進行定期同步。

    7 如果處於集群模式的話,對集群進行定期同步和連接測試。

    因為 serverCron() 需要在 Redis 服務器運行期間一直定期運行, 所以它是一個循環時間事件: serverCron() 會一直定期執行,直到服務器關閉為止。

    在 Redis 2.6 版本中, 程序規定 serverCron() 每秒運行 10 次, 平均每 100 毫秒運行一次。 從 Redis 2.8 開始, 用戶可以通過修改 hz選項來調整 serverCron() 的每秒執行次數, 具體信息請參考 redis.conf 文件中關於 hz 選項的說明

    查看hz

    way1 : config get hz  # "hz" "10"
    way2 : info server  # server.hz 10
    

    serverCron()

    serverCron()會定期的執行,在serverCron()執行中會調用databasesCron() 方法(serverCron()還做了其他很多事情,但是現在不討論,只談刪除策略)

    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        // 略去多無關代碼
    
        /* We need to do a few operations on clients asynchronously. */
        // 檢查客戶端,關閉超時客戶端,並釋放客戶端多餘的緩衝區
        clientsCron();
    
        /* Handle background operations on Redis databases. */
        // 對數據庫執行各種操作
        databasesCron();   /* !我們關注的方法! */
    

    databasesCron()

    databasesCron() 中 調用了 activeExpireCycle()方法,來對過期的數據進行處理。(在這裏還會做一些其他操作~ 調整數據庫大小,主動和漸進式rehash)

    // 對數據庫執行刪除過期鍵,調整大小,以及主動和漸進式 rehash
    void databasesCron(void) {
    
        // 判斷是否是主服務器 如果是 執行主動過期鍵清除
        if (server.active_expire_enabled && server.masterhost == NULL)
            // 清除模式為 CYCLE_SLOW ,這個模式會盡量多清除過期鍵
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    
        // 在沒有 BGSAVE 或者 BGREWRITEAOF 執行時,對哈希表進行 rehash
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
            static unsigned int resize_db = 0;
            static unsigned int rehash_db = 0;
            unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
            unsigned int j;
    
            /* Don't test more DBs than we have. */
            // 設定要測試的數據庫數量
            if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
    
            /* Resize */
            // 調整字典的大小
            for (j = 0; j < dbs_per_call; j++) {
                tryResizeHashTables(resize_db % server.dbnum);
                resize_db++;
            }
    
            /* Rehash */
            // 對字典進行漸進式 rehash
            if (server.activerehashing) {
                for (j = 0; j < dbs_per_call; j++) {
                    int work_done = incrementallyRehash(rehash_db % server.dbnum);
                    rehash_db++;
                    if (work_done) {
                        /* If the function did some work, stop here, we'll do
                         * more at the next cron loop. */
                        break;
                    }
                }
            }
        }
    }
    

    activeExpireCycle()

    大致流程如下

    1 遍歷指定個數的db(默認的 16 )進行刪除操作

    2 針對每個db隨機獲取過期數據每次遍歷不超過指定數量(如20),發現過期數據並進行刪除。

    3 如果有多於25%的keys過期,重複步驟 2

    除了主動淘汰的頻率外,Redis對每次淘汰任務執行的最大時長也有一個限定,這樣保證了每次主動淘汰不會過多阻塞應用請求,以下是這個限定計算公式:

    #define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ``... ``timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    

    也就是每次執行時間的25%用於過期數據刪除。

    void activeExpireCycle(int type) {
        // 靜態變量,用來累積函數連續執行時的數據
        static unsigned int current_db = 0; /* Last DB tested. */
        static int timelimit_exit = 0;      /* Time limit hit in previous call? */
        static long long last_fast_cycle = 0; /* When last fast cycle ran. */
    
        unsigned int j, iteration = 0;
        // 默認每次處理的數據庫數量
        unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
        // 函數開始的時間
        long long start = ustime(), timelimit;
    
        // 快速模式
        if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
            // 如果上次函數沒有觸發 timelimit_exit ,那麼不執行處理
            if (!timelimit_exit) return;
            // 如果距離上次執行未夠一定時間,那麼不執行處理
            if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
            // 運行到這裏,說明執行快速處理,記錄當前時間
            last_fast_cycle = start;
        }
    
        /* 
         * 一般情況下,函數只處理 REDIS_DBCRON_DBS_PER_CALL 個數據庫,
         * 除非:
         *
         * 1) 當前數據庫的數量小於 REDIS_DBCRON_DBS_PER_CALL
         * 2) 如果上次處理遇到了時間上限,那麼這次需要對所有數據庫進行掃描,
         *     這可以避免過多的過期鍵佔用空間
         */
        if (dbs_per_call > server.dbnum || timelimit_exit)
            dbs_per_call = server.dbnum;
    
        // 函數處理的微秒時間上限
        // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默認為 25 ,也即是 25 % 的 CPU 時間
        timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
        timelimit_exit = 0;
        if (timelimit <= 0) timelimit = 1;
    
        // 如果是運行在快速模式之下
        // 那麼最多只能運行 FAST_DURATION 微秒 
        // 默認值為 1000 (微秒)
        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
            timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
    
        // 遍曆數據庫
        for (j = 0; j < dbs_per_call; j++) {
            int expired;
            // 指向要處理的數據庫
            redisDb *db = server.db+(current_db % server.dbnum);
    
            // 為 DB 計數器加一,如果進入 do 循環之後因為超時而跳出
            // 那麼下次會直接從下個 DB 開始處理
            current_db++;
    
            do {
                unsigned long num, slots;
                long long now, ttl_sum;
                int ttl_samples;
    
                /* If there is nothing to expire try next DB ASAP. */
                // 獲取數據庫中帶過期時間的鍵的數量
                // 如果該數量為 0 ,直接跳過這個數據庫
                if ((num = dictSize(db->expires)) == 0) {
                    db->avg_ttl = 0;
                    break;
                }
                // 獲取數據庫中鍵值對的數量
                slots = dictSlots(db->expires);
                // 當前時間
                now = mstime();
    
                // 這個數據庫的使用率低於 1% ,掃描起來太費力了(大部分都會 MISS)
                // 跳過,等待字典收縮程序運行
                if (num && slots > DICT_HT_INITIAL_SIZE &&
                    (num*100/slots < 1)) break;
    
                /* 
                 * 樣本計數器
                 */
                // 已處理過期鍵計數器
                expired = 0;
                // 鍵的總 TTL 計數器
                ttl_sum = 0;
                // 總共處理的鍵計數器
                ttl_samples = 0;
    
                // 每次最多只能檢查 LOOKUPS_PER_LOOP 個鍵
                if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                    num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
    
                // 開始遍曆數據庫
                while (num--) {
                    dictEntry *de;
                    long long ttl;
    
                    // 從 expires 中隨機取出一個帶過期時間的鍵
                    if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                    // 計算 TTL
                    ttl = dictGetSignedIntegerVal(de)-now;
                    // 如果鍵已經過期,那麼刪除它,並將 expired 計數器增一
                    if (activeExpireCycleTryExpire(db,de,now)) expired++;
                    if (ttl < 0) ttl = 0;
                    // 累積鍵的 TTL
                    ttl_sum += ttl;
                    // 累積處理鍵的個數
                    ttl_samples++;
                }
    
                /* Update the average TTL stats for this database. */
                // 為這個數據庫更新平均 TTL 統計數據
                if (ttl_samples) {
                    // 計算當前平均值
                    long long avg_ttl = ttl_sum/ttl_samples;
                    
                    // 如果這是第一次設置數據庫平均 TTL ,那麼進行初始化
                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                    /* Smooth the value averaging with the previous one. */
                    // 取數據庫的上次平均 TTL 和今次平均 TTL 的平均值
                    db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
                }
    
                // 我們不能用太長時間處理過期鍵,
                // 所以這個函數執行一定時間之後就要返回
    
                // 更新遍歷次數
                iteration++;
    
                // 每遍歷 16 次執行一次
                if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
                    (ustime()-start) > timelimit)
                {
                    // 如果遍歷次數正好是 16 的倍數
                    // 並且遍歷的時間超過了 timelimit
                    // 那麼斷開 timelimit_exit
                    timelimit_exit = 1;
                }
    
                // 已經超時了,返回
                if (timelimit_exit) return;
    
                // 如果已刪除的過期鍵占當前總數據庫帶過期時間的鍵數量的 25 %
                // 那麼不再遍歷
            } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
        }
    }
    

    hz調大將會提高Redis主動淘汰的頻率,如果你的Redis存儲中包含很多冷數據佔用內存過大的話,可以考慮將這個值調大,但Redis作者建議這個值不要超過100。我們實際線上將這個值調大到100,觀察到CPU會增加2%左右,但對冷數據的內存釋放速度確實有明顯的提高(通過觀察keyspace個數和used_memory大小)。

    可以看出timelimit和server.hz是一個倒數的關係,也就是說hz配置越大,timelimit就越小。換句話說是每秒鐘期望的主動淘汰頻率越高,則每次淘汰最長佔用時間就越短。這裏每秒鐘的最長淘汰佔用時間是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰頻率和每次淘汰的最長時間是通過hz參數控制的。

    因此當redis中的過期key比率沒有超過25%之前,提高hz可以明顯提高掃描key的最小個數。假設hz為10,則一秒內最少掃描200個key(一秒調用10次*每次最少隨機取出20個key),如果hz改為100,則一秒內最少掃描2000個key;另一方面,如果過期key比率超過25%,則掃描key的個數無上限,但是cpu時間每秒鐘最多佔用250ms。

    當REDIS運行在主從模式時,只有主結點才會執行上述這兩種過期刪除策略,然後把刪除操作”del key”同步到從結點。

    if (server.active_expire_enabled && server.masterhost == NULL)  // 判斷是否是主節點 從節點不需要執行activeExpireCycle()函數。
            // 清除模式為 CYCLE_SLOW ,這個模式會盡量多清除過期鍵
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
    

    隨機個數

    redis.config.ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 決定每次循環從數據庫 expire中隨機挑選值的個數

    逐出算法

    如果不限制 reids 對內存使用的限制,它將會使用全部的內存。可以通過 config.memory 來指定redis 對內存的使用量 。

    下面是redis 配置文件中的說明

     543 # Set a memory usage limit to the specified amount of bytes.
     544 # When the memory limit is reached Redis will try to remove keys
     545 # according to the eviction policy selected (see maxmemory-policy).
     546 #
     547 # If Redis can't remove keys according to the policy, or if the policy is
     548 # set to 'noeviction', Redis will start to reply with errors to commands
     549 # that would use more memory, like SET, LPUSH, and so on, and will continue
     550 # to reply to read-only commands like GET.
     551 #
     552 # This option is usually useful when using Redis as an LRU or LFU cache, or to
     553 # set a hard memory limit for an instance (using the 'noeviction' policy).
     554 #
     555 # WARNING: If you have replicas attached to an instance with maxmemory on,
     556 # the size of the output buffers needed to feed the replicas are subtracted
     557 # from the used memory count, so that network problems / resyncs will
     558 # not trigger a loop where keys are evicted, and in turn the output
     559 # buffer of replicas is full with DELs of keys evicted triggering the deletion
     560 # of more keys, and so forth until the database is completely emptied.
     561 #
     562 # In short... if you have replicas attached it is suggested that you set a lower
     563 # limit for maxmemory so that there is some free RAM on the system for replica
     564 # output buffers (but this is not needed if the policy is 'noeviction').
     
    將內存使用限制設置為指定的字節。當已達到內存限制Redis將根據所選的逐出策略(請參閱maxmemory策略)嘗試刪除數據。
    
    如果Redis無法根據逐出策略移除密鑰,或者策略設置為“noeviction”,Redis將開始對使用更多內存的命令(如set、LPUSH等)進行錯誤回復,並將繼續回復只讀命令,如GET。
    
    當將Redis用作LRU或LFU緩存或設置實例的硬內存限制(使用“noeviction”策略)時,此選項通常很有用。
    
    警告:如果將副本附加到啟用maxmemory的實例,則將從已用內存計數中減去饋送副本所需的輸出緩衝區的大小,這樣,網絡問題/重新同步將不會觸發收回密鑰的循環,而副本的輸出緩衝區將充滿收回的密鑰增量,從而觸發刪除更多鍵,依此類推,直到數據庫完全清空。
    
    簡而言之。。。如果附加了副本,建議您設置maxmemory的下限,以便系統上有一些空閑RAM用於副本輸出緩衝區(但如果策略為“noeviction”,則不需要此限制)。
    

    驅逐策略的配置

    Maxmemery-policy volatile-lru
    

    當前已用內存超過 maxmemory 限定時,觸發主動清理策略

    易失數據清理

    volatile-lru:只對設置了過期時間的key進行LRU(默認值)

    volatile-random:隨機刪除即將過期key

    volatile-ttl : 刪除即將過期的

    volatile-lfu:挑選最近使用次數最少的數據淘汰

    全部數據清理

    allkeys-lru : 刪除lru算法的key

    allkeys-lfu:挑選最近使用次數最少的數據淘汰

    allkeys-random:隨機刪除

    禁止驅逐

    (Redis 4.0 默認策略)

    noeviction : 永不過期,返回錯誤當mem_used內存已經超過maxmemory的設定,對於所有的讀寫請求都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的內存。注意這個清理過程是阻塞的,直到清理出足夠的內存空間。所以如果在達到maxmemory並且調用方還在不斷寫入的情況下,可能會反覆觸發主動清理策略,導致請求會有一定的延遲。

    清理時會根據用戶配置的maxmemory-policy來做適當的清理(一般是LRU或TTL),這裏的LRU或TTL策略並不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。

    maxmemory-samples在redis-3.0.0中的默認配置為5,如果增加,會提高LRU或TTL的精準度,redis作者測試的結果是當這個配置為10時已經非常接近全量LRU的精準度了,並且增加maxmemory-samples會導致在主動清理時消耗更多的CPU時間,建議:

    1 盡量不要觸發maxmemory,最好在mem_used內存佔用達到maxmemory的一定比例后,需要考慮調大hz以加快淘汰,或者進行集群擴容。

    2 如果能夠控制住內存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為LRU cache服務(這種服務一般長時間處於maxmemory狀態,由Redis自動做LRU淘汰),可以適當調大maxmemory-samples。

    這裏提一句,實際上redis根本就不會準確的將整個數據庫中最久未被使用的鍵刪除,而是每次從數據庫中隨機取5個鍵並刪除這5個鍵里最久未被使用的鍵。上面提到的所有的隨機的操作實際上都是這樣的,這個5可以用過redis的配置文件中的maxmemeory-samples參數配置。

    數據逐出策略配置依據

    使用INFO命令輸出監控信息,查詢緩存int和miss的次數,根據業務需求調優Redis配置。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)

    基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)

    系列文章

    1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
    2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
    3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
    4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
    5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
    6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
    7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
    8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
    9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
    10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
    11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
    12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
    13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
    14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
    15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
    16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
    17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
    18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
    19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
    20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
    21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
    22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
    23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
    24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
    25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
    26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
    27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
    28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
    29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
    30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

    上一篇完成了博客的分頁查詢文章列表頁面的數據綁定和分頁功能,本篇將繼續完成剩下的幾個頁面。

    在開始主題之前重新解決上一篇的最後一個問題,當點擊了頭部組件的/posts鏈接時直接強制刷新了頁面,經過查看文檔和實踐有了更好的解決方案。

    先將頭部組件Header.razor中的NavLink恢復成<NavLink class="menu-item" href="posts">Posts</NavLink>,不需要點擊事件了。

    然後在Posts.razor中添加生命周期函數OnParametersSetAsync(),在初始化完成后執行。

    /// <summary>
    /// 初始化完成后執行
    /// </summary>
    /// <returns></returns>
    protected override async Task OnParametersSetAsync()
    {
        if (!page.HasValue)
        {
            page = 1;
            await RenderPage(page);
        }
    }
    

    判斷當前page參數是否有值,有值的話說明請求肯定是來自於翻頁,當page沒有值的時候就說明是頭部的菜單點進來的。那麼此時給page賦值為1,調用API加載數據即可。

    分類列表

    Categories.razor是分類列表頁面,上篇文章已經實現了從API獲取數據的方法,所以這裏就很簡單了,指定接受類型,然後在生命周期初始化OnInitializedAsync()中去獲取數據。

    @code{
        /// <summary>
        /// categories
        /// </summary>
        private ServiceResult<IEnumerable<QueryCategoryDto>> categories;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryDto>>>($"/blog/categories");
        }
    }
    

    當獲取到數據的時候進行綁定,沒有數據的時候還是显示加載中的組件<Loading />讓他轉圈圈。

    @if (categories == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap categories">
                <h2 class="post-title">-&nbsp;Categories&nbsp;-</h2>
                <div class="categories-card">
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <div class="card-item">
                                <div class="categories">
                                    <a href="/category/@item.DisplayName/">
                                        <h3>
                                            <i class="iconfont iconcode" style="padding-right:3px"></i>
                                            @item.CategoryName
                                        </h3>
                                        <small>(@item.Count)</small>
                                    </a>
                                </div>
                            </div>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    直接循環返回的數據列表categories.Result,綁定數據就好,當獲取失敗或者沒有返回數據的時候显示錯誤提示組件<ErrorTip />

    標籤列表

    Categories.razor是標籤列表頁面,和分類列表HTML結構差不多一樣的,除了返回類型和接口地址不一樣,將上面代碼複製過來改改即可。

    @code{
        /// <summary>
        /// tags
        /// </summary>
        private ServiceResult<IEnumerable<QueryTagDto>> tags;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            tags = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryTagDto>>>($"/blog/tags");
        }
    }
    
    @if (tags == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                <h2 class="post-title">-&nbsp;Tags&nbsp;-</h2>
                <div class="tag-cloud-tags">
                    @if (tags.Success && tags.Result.Any())
                    {
                        @foreach (var item in tags.Result)
                        {
                            <a href="/tag/@item.DisplayName/">@item.TagName<small>(@item.Count)</small></a>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    友鏈列表

    FriendLinks.razor是友情鏈接列表頁面,實現方式和上面兩個套路一模一樣。

    @code {
        /// <summary>
        /// friendlinks
        /// </summary>
        private ServiceResult<IEnumerable<FriendLinkDto>> friendlinks;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // 獲取數據
            friendlinks = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<FriendLinkDto>>>($"/blog/friendlinks");
        }
    }
    
    @if (friendlinks == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap categories">
                <h2 class="post-title">-&nbsp;FriendLinks&nbsp;-</h2>
                <div class="categories-card">
                    @if (friendlinks.Success && friendlinks.Result.Any())
                    {
                        @foreach (var item in friendlinks.Result)
                        {
                            <div class="card-item">
                                <div class="categories">
                                    <a target="_blank" href="@item.LinkUrl">
                                        <h3>@item.Title</h3>
                                    </a>
                                </div>
                            </div>
                        }
                    }
                    else
                    {
                        <ErrorTip />
                    }
                </div>
            </div>
        </div>
    }
    

    文章列表(分類)

    Posts.Category.razor是根據分類查詢文章列表頁面,他接受一個參數name,我們要根據name去API查詢數據然後綁定頁面即可。

    這裏的參數name實際上就是從標籤列表傳遞過來的DisplayName的值,它是一個比較友好的名稱,我們還要通過這個值去查詢真正的分類名稱進行展示,所以這裏需要調用兩個API,這點在設計API的時候沒有考慮好,我們其實可以將這兩個API合併變成一個,後續再進行優化吧,這裏就請求兩次。

    添加兩個接收參數:分類名稱和返回的文章列表數據。

    /// <summary>
    /// 分類名稱
    /// </summary>
    private string categoryName;
    
    /// <summary>
    /// 文章列表數據
    /// </summary>
    private ServiceResult<IEnumerable<QueryPostDto>> posts;
    

    然後在OnInitializedAsync()初始化方法中調用API獲取數據,賦值給變量。

    /// <summary>
    /// 初始化
    /// </summary>
    protected override async Task OnInitializedAsync()
    {
        // TODO:獲取數據,可以在API中合併這兩個請求。
        var category = await Http.GetFromJsonAsync<ServiceResult<string>>($"/blog/category?name={name}");
        posts = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryPostDto>>>($"/blog/posts/category?name={name}");
    
        if (category.Success)
        {
            categoryName = category.Result;
        }
    }
    

    有了數據,直接在頁面上進行循環綁定。

    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                @if (categoryName != null)
                {
                    <h2 class="post-title">-&nbsp;Category&nbsp;·&nbsp;@categoryName&nbsp;-</h2>
                }
            </div>
            <div class="post-wrap archive">
                @if (posts.Success && posts.Result.Any())
                {
                    @foreach (var item in posts.Result)
                    {
                        <h3>@item.Year</h3>
                        @foreach (var post in item.Posts)
                        {
                            <article class="archive-item">
                                <NavLink href="@("/post"+post.Url)">@post.Title</NavLink>
                                <span class="archive-item-date">@post.CreationTime</span>
                            </article>
                        }
                    }
                }
                else
                {
                    <ErrorTip />
                }
            </div>
        </div>
    }
    

    文章列表(標籤)

    Posts.Tag.razor是根據標籤查詢文章列表,這個和分類查詢文章列表實現方式一樣,直接上代碼。

    @code {
        /// <summary>
        /// 標籤名稱參數
        /// </summary>
        [Parameter]
        public string name { get; set; }
    
        /// <summary>
        /// 標籤名稱
        /// </summary>
        private string tagName;
    
        /// <summary>
        /// 文章列表數據
        /// </summary>
        private ServiceResult<IEnumerable<QueryPostDto>> posts;
    
        /// <summary>
        /// 初始化
        /// </summary>
        protected override async Task OnInitializedAsync()
        {
            // TODO:獲取數據,可以在API中合併這兩個請求。
            var tag = await Http.GetFromJsonAsync<ServiceResult<string>>($"/blog/tag?name={name}");
            posts = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryPostDto>>>($"/blog/posts/tag?name={name}");
    
            if (tag.Success)
            {
                tagName = tag.Result;
            }
        }
    }
    
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="container">
            <div class="post-wrap tags">
                @if (tagName != null)
                {
                    <h2 class="post-title">-&nbsp;Tag&nbsp;·&nbsp;@tagName&nbsp;-</h2>
                }
            </div>
            <div class="post-wrap archive">
                @if (posts.Success && posts.Result.Any())
                {
                    @foreach (var item in posts.Result)
                    {
                        <h3>@item.Year</h3>
                        @foreach (var post in item.Posts)
                        {
                            <article class="archive-item">
                                <NavLink href="@("/post"+post.Url)">@post.Title</NavLink>
                                <span class="archive-item-date">@post.CreationTime</span>
                            </article>
                        }
                    }
                }
                else
                {
                    <ErrorTip />
                }
            </div>
        </div>
    }
    

    以上完成了以上幾個頁面的數據綁定,頁面之間的跳轉已經關聯起來了,然後還剩下文章詳情頁,大家可以先自己動手完成它,今天就到這裏,未完待續…

    開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※超省錢租車方案

    ※教你寫出一流的銷售文案?

    網頁設計最專業,超強功能平台可客製化

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 小師妹學JavaIO之:MappedByteBuffer多大的文件我都裝得下

    小師妹學JavaIO之:MappedByteBuffer多大的文件我都裝得下

    目錄

    • 簡介
    • 虛擬地址空間
    • 詳解MappedByteBuffer
      • MapMode
    • MappedByteBuffer的最大值
    • MappedByteBuffer的使用
    • MappedByteBuffer要注意的事項
    • 總結

    簡介

    大大大,我要大!小師妹要讀取的文件越來越大,該怎麼幫幫她,讓程序在性能和速度上面得到平衡呢?快來跟F師兄一起看看吧。

    虛擬地址空間

    小師妹:F師兄,你有沒有發現,最近硬盤的價格真的是好便宜好便宜,1T的硬盤大概要500塊,平均1M五毛錢。現在下個電影都1G起步,這是不是意味着我們買入了大數據時代?

    沒錯,小師妹,硬件技術的進步也帶來了軟件技術的進步,兩者相輔相成,缺一不可。

    小師妹:F師兄,如果要是去讀取G級的文件,有沒有什麼快捷簡單的方法?

    更多精彩內容且看:

    • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
    • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
    • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
    • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

    還記得上次我們講的虛擬地址空間嗎?

    再把上次講的圖搬過來:

    通常來說我們的應用程序調用系統的接口從磁盤空間獲取Buffer數據,我們把自己的應用程序稱之為用戶空間,把系統的底層稱之為系統空間。

    傳統的IO操作,是操作系統講磁盤中的文件讀入到系統空間裏面,然後再拷貝到用戶空間中,供用戶使用。

    這中間多了一個Buffer拷貝的過程,如果這個量夠大的話,其實還是挺浪費時間的。

    於是有人在想了,拷貝太麻煩太耗時了,我們單獨劃出一塊內存區域,讓系統空間和用戶空間同時映射到同一塊地址不就省略了拷貝的步驟嗎?

    這個被劃出來的單獨的內存區域叫做虛擬地址空間,而不同空間到虛擬地址的映射就叫做Buffer Map。 Java中是有一個專門的MappedByteBuffer來代表這種操作。

    小師妹:F師兄,那這個虛擬地址空間和內存有什麼區別呢?有了內存還要啥虛擬地址空間?

    虛擬地址空間有兩個好處。

    第一個好處就是虛擬地址空間對於應用程序本身而言是獨立的,從而保證了程序的互相隔離和程序中地址的確定性。比如說一個程序如果運行在虛擬地址空間中,那麼它的空間地址是固定的,不管他運行多少次。如果直接使用內存地址,那麼可能這次運行的時候內存地址可用,下次運行的時候內存地址不可用,就會導致潛在的程序出錯。

    第二個好處就是虛擬空間地址可以比真實的內存地址大,這個大其實是對內存的使用做了優化,比如說會把很少使用的內存寫如磁盤,從而釋放出更多的內存來做更有意義的事情,而之前存儲到磁盤的數據,當真正需要的時候,再從磁盤中加載到內存中。

    這樣物理內存實際上可以看做虛擬空間地址的緩存。

    詳解MappedByteBuffer

    小師妹:MappedByteBuffer聽起來好神奇,怎麼使用它呢?

    我們先來看看MappedByteBuffer的定義:

    public abstract class MappedByteBuffer
        extends ByteBuffer
    

    它實際上是一個抽象類,具體的實現有兩個:

    class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
    
    class DirectByteBufferR extends DirectByteBuffer
    implements DirectBuffer
    

    分別是DirectByteBuffer和DirectByteBufferR。

    小師妹:F師兄,這兩個ByteBuffer有什麼區別呢?這個R是什麼意思?

    R代表的是ReadOnly的意思,可能是因為本身是個類的名字就夠長了,所以搞了個縮寫。但是也不寫個註解,讓人看起來十分費解….

    我們可以從RandomAccessFile的FilChannel中調用map方法獲得它的實例。

    我們看下map方法的定義:

     public abstract MappedByteBuffer map(MapMode mode, long position, long size)
            throws IOException;
    

    MapMode代表的是映射的模式,position表示是map開始的地址,size表示是ByteBuffer的大小。

    MapMode

    小師妹:F師兄,文件有隻讀,讀寫兩種模式,是不是MapMode也包含這兩類?

    對的,其實NIO中的MapMode除了這兩個之外,還有一些其他很有趣的用法。

    • FileChannel.MapMode.READ_ONLY 表示只讀模式
    • FileChannel.MapMode.READ_WRITE 表示讀寫模式
    • FileChannel.MapMode.PRIVATE 表示copy-on-write模式,這個模式和READ_ONLY有點相似,它的操作是先對原數據進行拷貝,然後可以在拷貝之後的Buffer中進行讀寫。但是這個寫入並不會影響原數據。可以看做是數據的本地拷貝,所以叫做Private。

    基本的MapMode就這三種了,其實除了基礎的MapMode,還有兩種擴展的MapMode:

    • ExtendedMapMode.READ_ONLY_SYNC 同步的讀
    • ExtendedMapMode.READ_WRITE_SYNC 同步的讀寫

    MappedByteBuffer的最大值

    小師妹:F師兄,既然可以映射到虛擬內存空間,那麼這個MappedByteBuffer是不是可以無限大?

    當然不是了,首先虛擬地址空間的大小是有限制的,如果是32位的CPU,那麼一個指針佔用的地址就是4個字節,那麼能夠表示的最大值是0xFFFFFFFF,也就是4G。

    另外我們看下map方法中size的類型是long,在java中long能夠表示的最大值是0x7fffffff,也就是2147483647字節,換算一下大概是2G。也就是說MappedByteBuffer的最大值是2G,一次最多只能map 2G的數據。

    MappedByteBuffer的使用

    小師妹,F師兄我們來舉兩個使用MappedByteBuffer讀寫的例子吧。

    善!

    先看一下怎麼使用MappedByteBuffer來讀數據:

    public void readWithMap() throws IOException {
            try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r"))
            {
                //get Channel
                FileChannel fileChannel = file.getChannel();
                //get mappedByteBuffer from fileChannel
                MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
                // check buffer
                log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
                log.info("capacity {}",buffer.capacity());
                //read the buffer
                for (int i = 0; i < buffer.limit(); i++)
                {
                    log.info("get {}", buffer.get());
                }
            }
        }
    

    然後再看一個使用MappedByteBuffer來寫數據的例子:

    public void writeWithMap() throws IOException {
            try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw"))
            {
                //get Channel
                FileChannel fileChannel = file.getChannel();
                //get mappedByteBuffer from fileChannel
                MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 );
                // check buffer
                log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
                log.info("capacity {}",buffer.capacity());
                //write the content
                buffer.put("www.flydean.com".getBytes());
            }
        }
    

    MappedByteBuffer要注意的事項

    小師妹:F師兄,MappedByteBuffer因為使用了內存映射,所以讀寫的速度都會有所提升。那麼我們在使用中應該注意哪些問題呢?

    MappedByteBuffer是沒有close方法的,即使它的FileChannel被close了,MappedByteBuffer仍然處於打開狀態,只有JVM進行垃圾回收的時候才會被關閉。而這個時間是不確定的。

    總結

    本文再次介紹了虛擬地址空間和MappedByteBuffer的使用。

    本文的例子https://github.com/ddean2009/learn-java-io-nio

    本文作者:flydean程序那些事

    本文鏈接:http://www.flydean.com/io-nio-mappedbytebuffer/

    本文來源:flydean的博客

    歡迎關注我的公眾號:程序那些事,更多精彩等着您!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 【深度思考】JDK8中日期類型該如何使用?

    【深度思考】JDK8中日期類型該如何使用?

    在JDK8之前,處理日期時間,我們主要使用3個類,DateSimpleDateFormatCalendar

    這3個類在使用時都或多或少的存在一些問題,比如SimpleDateFormat不是線程安全的,

    比如DateCalendar獲取到的月份是0到11,而不是現實生活中的1到12,關於這一點,《阿里巴巴Java開發手冊》中也有提及,因為很容易犯錯:

    不過,JDK8推出了全新的日期時間處理類解決了這些問題,比如InstantLocalDateLocalTimeLocalDateTimeDateTimeFormatter,在《阿里巴巴Java開發手冊》中也推薦使用Instant

    LocalDateTimeDateTimeFormatter

    但我發現好多項目中其實並沒有使用這些類,使用的還是之前的DateSimpleDateFormatCalendar,所以本篇博客就講解下JDK8新推出的日期時間類,主要是下面幾個:

    1. Instant
    2. LocalDate
    3. LocalTime
    4. LocalDateTime
    5. DateTimeFormatter

    1. Instant

    1.1 獲取當前時間

    既然Instant可以代替Date類,那它肯定可以獲取當前時間:

    Instant instant = Instant.now();
    System.out.println(instant);
    

    輸出結果:

    2020-06-10T08:22:13.759Z

    細心的你會發現,這個時間比北京時間少了8個小時,如果要輸出北京時間,可以加上默認時區:

    System.out.println(instant.atZone(ZoneId.systemDefault()));
    

    輸出結果:

    2020-06-10T16:22:13.759+08:00[Asia/Shanghai]

    1.2 獲取時間戳

    Instant instant = Instant.now();
    
    // 當前時間戳:單位為秒
    System.out.println(instant.getEpochSecond());
    // 當前時間戳:單位為毫秒
    System.out.println(instant.toEpochMilli());
    

    輸出結果:

    1591777752

    1591777752613

    當然,也可以通過System.currentTimeMillis()獲取當前毫秒數。

    1.3 將long轉換為Instant

    1)根據秒數時間戳轉換:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    long epochSecond = instant.getEpochSecond();
    System.out.println(Instant.ofEpochSecond(epochSecond));
    System.out.println(Instant.ofEpochSecond(epochSecond, instant.getNano()));
    

    輸出結果:

    2020-06-10T08:40:54.046Z

    2020-06-10T08:40:54Z

    2020-06-10T08:40:54.046Z

    2)根據毫秒數時間戳轉換:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    long epochMilli = instant.toEpochMilli();
    System.out.println(Instant.ofEpochMilli(epochMilli));
    

    輸出結果:

    2020-06-10T08:43:25.607Z

    2020-06-10T08:43:25.607Z

    1.4 將String轉換為Instant

    String text = "2020-06-10T08:46:55.967Z";
    Instant parseInstant = Instant.parse(text);
    System.out.println("秒時間戳:" + parseInstant.getEpochSecond());
    System.out.println("豪秒時間戳:" + parseInstant.toEpochMilli());
    System.out.println("納秒:" + parseInstant.getNano());
    

    輸出結果:

    秒時間戳:1591778815

    豪秒時間戳:1591778815967

    納秒:967000000

    如果字符串格式不對,比如修改成2020-06-10T08:46:55.967,就會拋出java.time.format.DateTimeParseException異常,如下圖所示:

    2. LocalDate

    2.1 獲取當前日期

    使用LocalDate獲取當前日期非常簡單,如下所示:

    LocalDate today = LocalDate.now();
    System.out.println("today: " + today);
    

    輸出結果:

    today: 2020-06-10

    不用任何格式化,輸出結果就非常友好,如果使用Date,輸出這樣的格式,還得配合SimpleDateFormat指定yyyy-MM-dd進行格式化,一不小心還會出個bug,比如去年年底很火的1個bug,我當時還是截了圖的:

    這2個好友是2019/12/31關注我的,但我2020年1月2號查看時,卻显示成了2020/12/31,為啥呢?格式化日期時格式寫錯了,應該是yyyy/MM/dd,卻寫成了YYYY/MM/dd,剛好那周跨年,就显示成下一年,也就是2020年了,當時好幾個博主寫過文章解析原因,我這裏就不做過多解釋了。

    划重點:都說到這了,給大家安利下我新註冊的公眾號「申城異鄉人」,歡迎大家關注,更多原創文章等着你哦,哈哈。

    2.2 獲取年月日

    LocalDate today = LocalDate.now();
    
    int year = today.getYear();
    int month = today.getMonthValue();
    int day = today.getDayOfMonth();
    
    System.out.println("year: " + year);
    System.out.println("month: " + month);
    System.out.println("day: " + day);
    

    輸出結果:

    year: 2020

    month: 6

    day: 10

    獲取月份終於返回1到12了,不像java.util.Calendar獲取月份返回的是0到11,獲取完還得加1。

    2.3 指定日期

    LocalDate specifiedDate = LocalDate.of(2020, 6, 1);
    System.out.println("specifiedDate: " + specifiedDate);
    

    輸出結果:

    specifiedDate: 2020-06-01

    如果確定月份,推薦使用另一個重載方法,使用枚舉指定月份:

    LocalDate specifiedDate = LocalDate.of(2020, Month.JUNE, 1);
    

    2.4 比較日期是否相等

    LocalDate localDate1 = LocalDate.now();
    LocalDate localDate2 = LocalDate.of(2020, 6, 10);
    if (localDate1.equals(localDate2)) {
        System.out.println("localDate1 equals localDate2");
    }
    

    輸出結果:

    localDate1 equals localDate2

    2.5 獲取日期是本周/本月/本年的第幾天

    LocalDate today = LocalDate.now();
    
    System.out.println("Today:" + today);
    System.out.println("Today is:" + today.getDayOfWeek());
    System.out.println("今天是本周的第" + today.getDayOfWeek().getValue() + "天");
    System.out.println("今天是本月的第" + today.getDayOfMonth() + "天");
    System.out.println("今天是本年的第" + today.getDayOfYear() + "天");
    

    輸出結果:

    Today:2020-06-11

    Today is:THURSDAY

    今天是本周的第4天

    今天是本月的第11天

    今天是本年的第163天

    2.6 判斷是否為閏年

    LocalDate today = LocalDate.now();
    
    System.out.println(today.getYear() + " is leap year:" + today.isLeapYear());
    

    輸出結果:

    2020 is leap year:true

    3. LocalTime

    3.1 獲取時分秒

    如果使用java.util.Date,那代碼是下面這樣的:

    Date date = new Date();
    
    int hour = date.getHours();
    int minute = date.getMinutes();
    int second = date.getSeconds();
    
    System.out.println("hour: " + hour);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    

    輸出結果:

    注意事項:這幾個方法已經過期了,因此強烈不建議在項目中使用:

    如果使用java.util.Calendar,那代碼是下面這樣的:

    Calendar calendar = Calendar.getInstance();
    
    // 12小時制
    int hourOf12 = calendar.get(Calendar.HOUR);
    // 24小時制
    int hourOf24 = calendar.get(Calendar.HOUR_OF_DAY);
    int minute = calendar.get(Calendar.MINUTE);
    int second = calendar.get(Calendar.SECOND);
    int milliSecond = calendar.get(Calendar.MILLISECOND);
    
    System.out.println("hourOf12: " + hourOf12);
    System.out.println("hourOf24: " + hourOf24);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    System.out.println("milliSecond: " + milliSecond);
    

    輸出結果:

    注意事項:獲取小時時,有2個選項,1個返回12小時制的小時數,1個返回24小時制的小時數,因為現在是晚上8點,所以calendar.get(Calendar.HOUR)返回8,而calendar.get(Calendar.HOUR_OF_DAY)返回20。

    如果使用java.time.LocalTime,那代碼是下面這樣的:

    LocalTime localTime = LocalTime.now();
    System.out.println("localTime:" + localTime);
    
    int hour = localTime.getHour();
    int minute = localTime.getMinute();
    int second = localTime.getSecond();
    
    System.out.println("hour: " + hour);
    System.out.println("minute: " + minute);
    System.out.println("second: " + second);
    

    輸出結果:

    可以看出,LocalTime只有時間沒有日期。

    4. LocalDateTime

    4.1 獲取當前時間

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime:" + localDateTime);
    

    輸出結果:

    localDateTime: 2020-06-11T11:03:21.376

    4.2 獲取年月日時分秒

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    System.out.println("year: " + localDateTime.getYear());
    System.out.println("month: " + localDateTime.getMonthValue());
    System.out.println("day: " + localDateTime.getDayOfMonth());
    System.out.println("hour: " + localDateTime.getHour());
    System.out.println("minute: " + localDateTime.getMinute());
    System.out.println("second: " + localDateTime.getSecond());
    

    輸出結果:

    4.3 增加天數/小時

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    LocalDateTime tomorrow = localDateTime.plusDays(1);
    System.out.println("tomorrow: " + tomorrow);
    
    LocalDateTime nextHour = localDateTime.plusHours(1);
    System.out.println("nextHour: " + nextHour);
    

    輸出結果:

    localDateTime: 2020-06-11T11:13:44.979

    tomorrow: 2020-06-12T11:13:44.979

    nextHour: 2020-06-11T12:13:44.979

    LocalDateTime還提供了添加年、周、分鐘、秒這些方法,這裏就不一一列舉了:

    4.4 減少天數/小時

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    LocalDateTime yesterday = localDateTime.minusDays(1);
    System.out.println("yesterday: " + yesterday);
    
    LocalDateTime lastHour = localDateTime.minusHours(1);
    System.out.println("lastHour: " + lastHour);
    

    輸出結果:

    localDateTime: 2020-06-11T11:20:38.896

    yesterday: 2020-06-10T11:20:38.896

    lastHour: 2020-06-11T10:20:38.896

    類似的,LocalDateTime還提供了減少年、周、分鐘、秒這些方法,這裏就不一一列舉了:

    4.5 獲取時間是本周/本年的第幾天

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println("localDateTime: " + localDateTime);
    
    System.out.println("DayOfWeek: " + localDateTime.getDayOfWeek().getValue());
    System.out.println("DayOfYear: " + localDateTime.getDayOfYear());
    

    輸出結果:

    localDateTime: 2020-06-11T11:32:31.731

    DayOfWeek: 4

    DayOfYear: 163

    5. DateTimeFormatter

    JDK8中推出了java.time.format.DateTimeFormatter來處理日期格式化問題,《阿里巴巴Java開發手冊》中也是建議使用DateTimeFormatter代替SimpleDateFormat

    5.1 格式化LocalDate

    LocalDate localDate = LocalDate.now();
    
    System.out.println("ISO_DATE: " + localDate.format(DateTimeFormatter.ISO_DATE));
    System.out.println("BASIC_ISO_DATE: " + localDate.format(DateTimeFormatter.BASIC_ISO_DATE));
    System.out.println("ISO_WEEK_DATE: " + localDate.format(DateTimeFormatter.ISO_WEEK_DATE));
    System.out.println("ISO_ORDINAL_DATE: " + localDate.format(DateTimeFormatter.ISO_ORDINAL_DATE));
    

    輸出結果:

    如果提供的格式無法滿足你的需求,你還可以像以前一樣自定義格式:

    LocalDate localDate = LocalDate.now();
    
    System.out.println("yyyy/MM/dd: " + localDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));
    

    輸出結果:

    yyyy/MM/dd: 2020/06/11

    5.2 格式化LocalTime

    LocalTime localTime = LocalTime.now();
    System.out.println(localTime);
    System.out.println("ISO_TIME: " + localTime.format(DateTimeFormatter.ISO_TIME));
    System.out.println("HH:mm:ss: " + localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    

    輸出結果:

    14:28:35.230

    ISO_TIME: 14:28:35.23

    HH:mm:ss: 14:28:35

    5.3 格式化LocalDateTime

    LocalDateTime localDateTime = LocalDateTime.now();
    System.out.println(localDateTime);
    System.out.println("ISO_DATE_TIME: " + localDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
    System.out.println("ISO_DATE: " + localDateTime.format(DateTimeFormatter.ISO_DATE));
    

    輸出結果:

    2020-06-11T14:33:18.303

    ISO_DATE_TIME: 2020-06-11T14:33:18.303

    ISO_DATE: 2020-06-11

    6. 類型相互轉換

    6.1 Instant轉Date

    JDK8中,Date新增了from()方法,將Instant轉換為Date,代碼如下所示:

    Instant instant = Instant.now();
    System.out.println(instant);
    
    Date dateFromInstant = Date.from(instant);
    System.out.println(dateFromInstant);
    

    輸出結果:

    2020-06-11T06:39:34.979Z

    Thu Jun 11 14:39:34 CST 2020

    6.2 Date轉Instant

    JDK8中,Date新增了toInstant方法,將Date轉換為Instant,代碼如下所示:

    Date date = new Date();
    Instant dateToInstant = date.toInstant();
    System.out.println(date);
    System.out.println(dateToInstant);
    

    輸出結果:

    Thu Jun 11 14:46:12 CST 2020

    2020-06-11T06:46:12.112Z

    6.3 Date轉LocalDateTime

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    System.out.println(date);
    System.out.println(localDateTimeOfInstant);
    

    輸出結果:

    Thu Jun 11 14:51:07 CST 2020

    2020-06-11T14:51:07.904

    6.4 Date轉LocalDate

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    LocalDate localDate = localDateTimeOfInstant.toLocalDate();
    System.out.println(date);
    System.out.println(localDate);
    

    輸出結果:

    Thu Jun 11 14:59:38 CST 2020

    2020-06-11

    可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalDate

    6.5 Date轉LocalTime

    Date date = new Date();
    Instant instant = date.toInstant();
    LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    LocalTime toLocalTime = localDateTimeOfInstant.toLocalTime();
    System.out.println(date);
    System.out.println(toLocalTime);
    

    輸出結果:

    Thu Jun 11 15:06:14 CST 2020

    15:06:14.531

    可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalTime

    6.6 LocalDateTime轉Date

    LocalDateTime localDateTime = LocalDateTime.now();
    
    Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromInstant = Date.from(toInstant);
    System.out.println(localDateTime);
    System.out.println(dateFromInstant);
    

    輸出結果:

    2020-06-11T15:12:11.600

    Thu Jun 11 15:12:11 CST 2020

    6.7 LocalDate轉Date

    LocalDate today = LocalDate.now();
    
    LocalDateTime localDateTime = localDate.atStartOfDay();
    Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromLocalDate = Date.from(toInstant);
    System.out.println(dateFromLocalDate);
    

    輸出結果:

    Thu Jun 11 00:00:00 CST 2020

    6.8 LocalTime轉Date

    LocalDate localDate = LocalDate.now();
    LocalTime localTime = LocalTime.now();
    
    LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
    Instant instantFromLocalTime = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
    Date dateFromLocalTime = Date.from(instantFromLocalTime);
    
    System.out.println(dateFromLocalTime);
    

    輸出結果:

    Thu Jun 11 15:24:18 CST 2020

    7. 總結

    JDK8推出了全新的日期時間類,如InstantLocaleDateLocalTimeLocalDateTimeDateTimeFormatter,設計比之前更合理,也是線程安全的。

    《阿里巴巴Java開發規範》中也推薦使用Instant代替DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat

    因此,如果條件允許,建議在項目中使用,沒有使用的,可以考慮升級下。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

    ※教你寫出一流的銷售文案?

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

  • 看完這些黑科技,我覺得寶馬的真實身份其實是科技公司!

    看完這些黑科技,我覺得寶馬的真實身份其實是科技公司!

    寶馬通過一台基於全新BMW 5系的原型車展示了未來高度自動駕駛汽車極為個性化、智能化的駕駛感受。在高度自動駕駛狀態下,駕駛者的雙手和雙腳都得到了解放,無需手握方向盤和控制油門、剎車。同時,BMW實時交通信號系統還可以預測前方下一組信號燈情況,讓駕駛者更好地進行選擇。

    (拉斯維加斯/北京)2017年1月5日,一年一度的科技盛會、2017國際消費电子展在拉斯維加斯拉開帷幕。寶馬集團攜一系列前瞻理念和科技成果亮相,展現了其在智能互聯、未來汽車內部設計、控制與显示系統、自動駕駛等創新領域蓬勃的創造力。

    寶馬集團正在成為引領数字出行生活的重要力量。在其願景中,自動駕駛技術將為駕駛者帶來更多選擇和自由;豐富的智能互聯服務圍繞人的需求,將車輛與用戶的数字生活無縫對接,讓出行和生活更高效、便捷、充滿樂趣。重要的是,這些離我們的生活並不遙遠,近年在CES上亮相的手勢控制、遠程3D環視影像等,均迅速應用於量產車型,體現了寶馬在行業內的領先地位。

    這不是科幻大片,是你未來的汽車

    在CES展台,寶馬集團通過BMW i Inside Future未來內室研究項目展示出,未來配備自動駕駛技術的汽車,其座艙將根據用戶需求,在休息室、辦公室和娛樂室之間實現自由切換。

    未來,車內空間的氛圍和控制方式將取決於駕駛模式。在主動駕駛模式下,主動駕駛的功能將處於車內中心位置。在高度自動駕駛模式下,系統將显示更多的舒適、信息娛樂和通訊功能。導航系統可以推薦適合高度或完全自動駕駛的行車路線,並在到達路口時發出提醒。未來,自動駕駛將首先應用於高速或單向行駛道路。

    對人機交互模式的創新是未來車輛的重要課題。寶馬在最近的兩屆CES上都帶來了創新的人機交互系統——手勢控制和AirTouch手勢控制系統。其中手勢控制已經應用於量產的新BMW 7系和全新BMW 5系車型。今年,寶馬又帶來了BMW HoloActive觸控系統,將人機交互體驗提升至新的高度。

    BMW HoloActive觸控系統的原理與平視显示系統類似,通過反射原理在中控台位置投射出一塊“懸浮”屏幕,駕駛者通過指尖“點擊”虛擬屏幕來控制車輛,呈現出科幻大片的既視感。這套系統通過高敏感度的攝像頭捕捉駕駛者指尖的動作。在超聲波裝置的配合下,客戶的手指可以感受到輕微的壓力,模擬了傳統觸控屏的體驗,讓操作更符合人們的習慣。

    未來的車輛內,駕駛者和乘客可以各自享受音樂而不互相干擾。首次展出的音效裝置BMW Sound Curtain通過座椅頭枕發射出不同的聲音信號,為座位上的用戶提供個性化的專屬娛樂信息。一個可摺疊的大尺寸屏幕可從車內頂篷延伸出來,進一步豐富後排乘客的車內互聯生活。

    擁有一台高度互聯的自動駕駛汽車是一種怎樣的體驗?

    寶馬通過一台基於全新BMW 5系的原型車展示了未來高度自動駕駛汽車極為個性化、智能化的駕駛感受。在高度自動駕駛狀態下,駕駛者的雙手和雙腳都得到了解放,無需手握方向盤和控制油門、剎車。同時,BMW實時交通信號系統還可以預測前方下一組信號燈情況,讓駕駛者更好地進行選擇。

    在自動泊車功能展示中,抵達停車場時,車輛自動與泊車管理服務進行連接,显示屏會提示駕駛者可以使用預約的停車位。駕駛者與乘客下車后,車輛隨即啟動自動泊車功能。BMW雲端互聯將在車輛停好後向駕駛者推送提示信息。通過全新BMW 5系中首次配備的環視影像系統,用戶還可以通過BMW雲端互聯應用實時查看車輛情況。

    在自動駕駛帶來的閑暇時間里,駕駛者可以盡情享受豐富的智能互聯功能。例如,在車輛行進途中,前排乘客可以通過BMW增強手勢控制系統、獲取途經場所的信息,如娛樂場所的節目單,還可以直接訂票。

    在開放式移動雲的支持下,BMW 雲端互聯可以整合豐富的應用,讓人們在車內便捷地處理各種事務。比如已經在家用電腦上推出的個人数字助理“微軟小娜(Cortana)”也可以在BMW汽車上使用。在駕駛過程中,用戶可以通過語音控制讓小娜推薦就餐地點並預定位置。

    通過與亞馬遜prime Now速遞服務合作,未來在行車途中預約收取快遞也成為可能。設想一下,用戶正前往一個生日聚會,但忘了購買禮物,通過這一功能,用戶可以從容地在車內在線購物。prime Now與開放式移動雲將根據車輛位置、路線和實時交通信息計算出最佳交付地點;車輛到達交付點之後,prime Now的員工將把貨物送到用戶的手中。BMW 雲端互聯無疑為智能、便捷的数字生活帶來了巨大的想象空間。BMW 雲端互聯已於2016年12月在中國正式上線,未來這些功能也將逐步升級到現有版本中。

    BMW不僅在車庫,還可以在客廳——智能出行和智能生活相結合

    在2017 CES上,寶馬集團還將全新的智能互聯科技從車內延伸到用戶的家中,標志著BMW 雲端互聯與家居環境的結合。未來,通過創新的智能終端BMW Conncted Window,用戶在家也可以享受BMW雲端互聯的豐富功能。

    當用戶開始新的一天時,BMW Connected Window的界面將显示溫暖的問候,與此同時,用戶每天的出行日程會按照時間軸進行显示,出行目的地,建議出發時間、天氣情況等信息一目瞭然。BMW Connected Window的虛擬界面通過手勢來操作,與觸摸屏一樣直觀。更新信息也可以通過BMW 雲端互聯方便地添加到日程中並與其他智能設備保持同步,幫助用戶胸有成竹地開始新一天的生活。

    本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?