深入理解JVM(③)判斷對象是否還健在?

前言

因為Java對象主要存放在Java堆里,所以垃圾收集器(Garbage Collection)在對Java堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”着,哪些已經“死去”(不被引用了)。

判斷對象是否健在的算法

1.引用計數算法

引用計數算法,很容易理解,在對象中添加一個引用計數器,每有一個地方引用它時,計數器值就加一;當引用失效是,計數器值就減一;任何時刻計數器為零的對象就是不可以能再被使用的對象
引用計數算法的原理簡單,判定效率也很高。市面上也確實有一些技術使用的此類算法來判定對象是否存活,像ActionScript 3 的FlashPlayer、Python語言等。但是在主流的Java虛擬機裏面都沒有選用引用計算法來管理內存,主要是使用此算法時,必須要配合大量的額外處理才能保證正確的工作,例如要解決對象之間的相互循環引用的問題。

public class OneTest {

    public Object oneTest = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[256 * _1MB];


    /**
     * 這個成員屬性的唯一意義就是占點內存,以便能在GC日誌中看清楚是否有回收過。
     */
    @Test
    public void testGC(){

        OneTest test1 = new OneTest();
        OneTest test2 = new OneTest();

        test1.oneTest = test2;
        test2.oneTest = test1;

        test1 = null;
        test2 = null;

        // 假設在這行發生GC,test1和test2是否能被回收?
        System.gc();

    }

}

分析代碼,test1和test2對象都被設置成了null,在後面發生GC的時候,如果按照引用計數算法,這兩個對象雖然都被設置成了null,但是test1引用了test2,test2又引用了test1,所以這兩個對象的引用計數值都不為0,所以都不會被回收,但是真正的實際運行結果是,這兩個對象都被回收了,這也說明HotSpot虛擬機並不是用引用計數法來進行的內存管理。

2. 可達性分析算法

當前主流的商用程序語言(Java、C#等),都是通過可達性分析(Reachability Analysis)算法來判斷對象是否存活的。這個算法的基本思路就是通過一一系列稱為“GC Roots” 的根對象作為起始節點集,從這些節點開始根據引用關係向下搜索,搜索走過的的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots 間沒有任何引用鏈相連,或者從GC Roots 到這個對象不可達時,則證明此對象是不可能再被使用的。
如下圖,object10、object11、object12這三個對象,雖然互相有關聯,但是它們到GC Roots是不可達的,因此它們會被判定為可回收的對象。

在Java程序中,固定可作為GC Roots 的對象包括以下幾種:

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個現場被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(NullPointException、OutOfMemoryError)等,還有系統類加載器。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。
    除了這些固定的GC Roots集合以外,根據垃圾收集器以及當前回收的呢村區域不同,還會有其他對象“臨時性”的加入,如果只針對Java堆中某一塊兒區域發起垃圾收集時(例如只針對年輕代的垃圾收集),必須考慮到當前區域內的對象是否有被其他區域的對象所引用,這個時候就需要把這些關聯區域的對象一併加入GC Roots集合中,來保證可達性分析的正確性。

重申引用

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判斷對象是否存活都和“引用”離不開關係。在JDK1.2之前,Java里對引用的概念是:如果reference類型的數據中存儲的數值代表的是另外一塊兒內存的地址,就稱該reference數據是代表某塊內存、某個對象的引用。
在JDK1.2版之後,Java對引用的概念進行了擴充,將引用分為強引用(Strongly Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  • 強引用是最傳統的“引用”的定義,指引用複製,即類似
Object obj = new Object()

這種引用關係。無論在任何情況下,只要強引用關係還存在,垃圾收集器就不會回收掉被引用的對象。

  • 軟引用是用來描述一些還有用,但非必須的對象。在系統發生內存溢出前,會先對軟引用對象進行第二次回收,如果回收后還沒有足夠的內存,才會拋出內存溢出的異常。
  • 弱引用也是用來描述那些非必須的對象,但是它的強度比軟引用更弱一些,弱引用的對象,只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用也稱為“幽靈引用”或“幻影引用”,它是最弱的一種引用關係。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。

判斷對象是生是死的過程

即使在可達性分析算法中,判斷為不可達的對象,也不是“非死不可”的,要真正宣告一個對象死亡,至少要經歷兩次標記過程:

  • 如果第一次對象在進行可達性分析后發現與GC Roots 不可達,將進行第一次標記。
  • 隨後對此對象進行一次是否有必要執行finalize()方法進行篩選,假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,都視為“沒有必要執行”。
    如果對象被判定有必要執行finalize()方法,會將對象放置在一個名為F-Queue的隊列中,並在由一條由虛擬機自動建立的、低調度的線程區執行它們的finalize()方法。但並不承諾一定會等待它們運行結束。

需要注意的是:任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨第二次回收,它的finalize()方法不會被再次執行。
還有一點就是Java官方已經明確聲明不推薦手動調用finalize()方法了,因為它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,並且finanlize()能做的所有工作,使用try-finally或其他方式都可以做的更好、更及時。

回收方法區

方法區垃圾收集的“性價比”通常比較低,並且方法區回收也有過於苛刻的判定條件。
方法區的垃圾收集主要回收兩部分內容:廢棄的常量不再使用的類型,回收廢棄常量時,如果當前系統沒有一個常量的值是當前常量值,且虛擬機中也沒有其他地方引用這個常量。如果這個時候發生垃圾回收,常量就會被系統清理出常量池。
判定一個類型是否屬於“不再使用的類”的條件就比較苛刻了,要同時滿足如下三個條件:

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
  • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的沖加載等,否則通常很難達成的。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

同時滿足了上述的三個條件后,也只是被允許進行回收了,關於是否要對類型進行回收還要對虛擬機進行一系列的參數設置,這裏就不贅述了,感興趣的可以自己去查詢。

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

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

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

漲見識了,在終端執行 Python 代碼的 6 種方式!

原作:BRETT CANNON

譯者:豌豆花下貓@Python貓

英文:https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the-terminal

為了我們推出的 VS Code 的 Python 插件 [1],我寫了一個簡單的腳本來生成變更日誌 [2](類似於Towncrier [3],但簡單些,支持 Markdown,符合我們的需求)。在發布過程中,有一個步驟是運行python news ,它會將 Python 指向我們代碼中的”news”目錄。

前幾天,一位合作者問這是如何工作的,似乎我們團隊中的每個人都知道如何使用-m ?(請參閱我的有關帶 -m 使用 pip 的文章 [4],了解原因)(譯註:關於此話題,我也寫過一篇更為詳細的文章 )

這使我意識到其他人可能不知道有五花八門的方法可以將 Python 指向要執行的代碼,因此有了這篇文章。

1、通過標準輸入和管道

因為如何用管道傳東西給一個進程是屬於 shell 的內容,我不打算深入解釋。毋庸置疑,你可以將代碼傳遞到 Python 中。

# 管道傳內容給 python
echo "print('hi')" | python

如果將文件重定向到 Python,這顯然也可以。

# 重定向一個文件給 python
python < spam.py

歸功於 Python 的 UNIX 傳統,這些都不太令人感到意外。

2、通過-c 指定的字符串

如果你只需要快速地檢查某些內容,則可以在命令行中將代碼作為字符串傳遞。

# 使用 python 的 -c 參數
python -c "print('hi')"

當需要檢查僅一行或兩行代碼時,我個人會使用它,而不是啟動 REPL(譯註:Read Eval Print Loop,即交互式解釋器,例如在 windows 控制台中輸入python, 就會進入交互式解釋器。-c 參數用法可以省去進入解釋器界面的過程) 。

3、文件的路徑

最眾所周知的傳代碼給 python 的方法很可能是通過文件路徑。

# 指定 python 的文件路徑
python spam.py

要實現這一點的關鍵是將包含該文件的目錄放到sys.path 里。這樣你的所有導入都可以繼續使用。但這也是為什麼你不能/不應該傳入包含在一個包里的模塊路徑。因為sys.path 可能不包含該包的目錄,因此所有的導入將相對於與你預期的包不同的目錄。

4、對包使用 -m

執行 Python 包的正確方法是使用 -m 並指定要運行的包名。

python -m spam

它在底層使用了runpy [5]。要在你的項目中做到這點,只需要在包里指定一個__main__.py 文件,它將被當成__main__ 執行。而且子模塊可以像任何其它模塊一樣導入,因此你可以對其進行各種測試。

我知道有些人喜歡在一個包里寫一個main 子模塊,然後將其__main__.py 寫成:

from . import main

if __name__ == "__main__":
    main.main()

就我個人而言,我不感冒於單獨的main 模塊,而是直接將所有相關的代碼放入__main__.py ,因為我感覺這些模塊名是多餘的。

(譯註:即作者不關心作為入口文件的”main”或者“__main__”模塊,因為執行時只需用它們的包名即可。我認為這也暗示了入口模塊不該再被其它模塊 import。我上篇文章 [6]比作者的觀點激進,認為連那句 if 語句都不該寫。)

5、目錄

定義__main__.py也可以擴展到目錄。如果你看一下促成此博客文章的示例,python news 可執行,就是因為 news 目錄有一個 __main__.py 文件。該目錄就像一個文件路徑被 Python 執行了。

現在你可能會問:“為什麼不直接指定文件路徑呢?”好吧,坦白說,關於文件路徑,有件事得說清楚。在發布過程中,我可以簡單地寫上說明,讓運行python news/announce.py ,但是並沒有確切的理由說明這種機制何時存在。

再加上我以後可以更改文件名,而且沒人會注意到。再加上我知道代碼會帶有輔助文件,因此將其放在目錄中而不是單獨作為單個文件是有意義的。

當然,我也可以將它變為一個使用 -m 的包,但是沒必要,因為 announce 腳本很簡單,我知道它要保持成為一個單獨的自足的文件(少於 200 行,並且測試模塊也大約是相同的長度)。

況且,__main__.py 文件非常簡單。

import runpy
# Change 'announce' to whatever module you want to run.
runpy.run_module('announce', run_name='__main__', alter_sys=True)

現在顯然必須要處理依賴關係,但是如果你的腳本僅使用標準庫或將依賴模塊放在__main__.py 旁邊(譯註:即同級目錄),那麼就足夠了!

(譯註:我覺得作者在此有點“炫技”了,因為這種寫法的前提是得知道 runpy 的用法,但是就像前一條所寫的用 -m 參數運行一個包,在底層也是用了 runpy。不過炫技的好處也非常明顯,即__main__.py 里不用導入 announce 模塊,還是以它為主模塊執行,也就不會破壞原來的依賴導入關係)

6、執行一個壓縮文件

如果你確實有多個文件和/或依賴模塊,並且希望將所有代碼作為一個單元發布,你可以用一個__main__.py ,放置在一個壓縮文件中,並把壓縮文件所在目錄放在 sys.path 里,Python 會替你運行__main__.py 文件。

# 將一個壓縮包傳給 Python
python app.pyz

人們現在習慣上用 .pyz 文件擴展名來命名此類壓縮文件,但這純粹是傳統,不會影響任何東西;你當然也可以用 .zip 文件擴展名。

為了簡化創建此類可執行的壓縮文件,標準庫提供了zipapp [7]模塊。它會為你生成__main__.py並添加一條組織行(shebang line),因此你甚至不需要指定 python,如果你不想在 UNIX 上指定它的話。如果你想移動一堆純 Python 代碼,這是一種不錯的方法。

不幸的是,僅當壓縮文件包含的所有代碼都是純 Python 時,才能這樣運行壓縮文件。執行壓縮文件對擴展模塊無效(這就是為什麼 setuptools 有一個 zip_safe [8]標誌的原因)。(譯註:擴展模塊 extension module,即 C/C++ 之類的非 Python 文件)

要加載擴展模塊,Python 必須調用 dlopen() [9]函數,它要傳入一個文件路徑,但當該文件路徑就包含在壓縮文件內時,這顯然不起作用。

我知道至少有一個人與 glibc 團隊交談過,關於支持將內存緩衝區傳入壓縮文件,以便 Python 可以將擴展模塊讀入內存,並將其傳給壓縮文件,但是如果內存為此服務,glibc 團隊並不同意。

但是,並非所有希望都喪失了!你可以使用諸如shiv [10]之類的項目,它會捆綁(bundle)你的代碼,然後提供一個__main__.py 來處理壓縮文件的提取、緩存,然後為你執行代碼。儘管不如純 Python 解決方案理想,但它確實可行,並且在這種情況下算得上是優雅的。

(譯註:翻譯水平有限,難免偏差。我加註了部分內容,希望有助於閱讀。請搜索關注“Python貓”,閱讀更多優質的原創或譯作。)

參考鏈接

[0] https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the-terminal/

[1] https://marketplace.visualstudio.com/items?itemName=ms-python.python

[2] https://github.com/microsoft/vscode-python/tree/master/news

[3] https://pypi.org/project/towncrier

[4] https://snarky.ca/why-you-should-use-python-m-pip

[5] https://docs.python.org/3/library/runpy.html#module-runpy

[6] https://mp.weixin.qq.com/s/1ehySR5NH2v1U8WIlXflEQ

[7] https://docs.python.org/3/library/zipapp.html#module-zipapp

[8] https://setuptools.readthedocs.io/en/latest/setuptools.html#setting-the-zip-safe-flag

[9] https://linux.die.net/man/3/dlopen

[10] https://pypi.org/project/shiv/

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

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

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

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

教科書級講解,秒懂最詳細Java的註解

所有知識體系文章,GitHub已收錄,歡迎Star!再次感謝,願你早日進入大廠!

GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual

Java註解

一、Java註解概述

註解(Annotation),也叫元數據。一種代碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等的前面,用來對這些元素進行說明,註釋。

二、註解的作用分類

  • 編寫文檔: 通過代碼里標識的元數據生成文檔【生成文檔doc文檔】
  • 代碼分析: 通過代碼里標識的元數據對代碼進行分析【使用反射】
  • 編譯檢查: 通過代碼里標識的元數據讓編譯器能夠實現基本的編譯檢查【Override等】

編寫文檔

首先,我們要知道Java中是有三種註釋的,分別為單行註釋、多行註釋和文檔註釋。而文檔註釋中,也有@開頭的元註解,這就是基於文檔註釋的註解。我們可以使用javadoc命令來生成doc文檔,此時我們文檔的內元註解也會生成對應的文檔內容。這就是編寫文檔的作用。

代碼分析

我們頻繁使用之一,也是包括使用反射來通過代碼里標識的元數據對代碼進行分析的,此內容我們在後續展開講解。

編譯檢查

至於在編譯期間在代碼中標識的註解,可以用來做特定的編譯檢查,它可以在編譯期間就檢查出“你是否按規定辦事”,如果不按照註解規定辦事的話,就會在編譯期間飄紅報錯,並予以提示信息。可以就可以為我們代碼提供了一種規範制約,避免我們後續在代碼中處理太多的代碼以及功能的規範。比如,@Override註解是在我們覆蓋父類(父接口)方法時出現的,這證明我們覆蓋方法是繼承於父類(父接口)的方法,如果該方法稍加改變就會報錯;@FunctionInterface註解是在編譯期檢查是否是函數式接口的,如果不遵循它的規範,同樣也會報錯。

三、jdk的內置註解

3.1 內置註解分類

  • @Override: 標記在成員方法上,用於標識當前方法是重寫父類(父接口)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規則,如果不符合,編譯報錯。
  • @Deprecated: 用於標記當前類、成員變量、成員方法或者構造方法過時如果開發者調用了被標記為過時的方法,編譯器在編譯期進行警告。
  • @SuppressWarnings: 壓制警告註解,可放置在類和方法上,該註解的作用是阻止編譯器發出某些警告信息。

3.2 @Override註解

標記在成員方法上,用於標識當前方法是重寫父類(父接口)方法,編譯器在對該方法進行編譯時會檢查是否符合重寫規則,如果不符合,編譯報錯。

這裏解釋一下@Override註解,在我們的Object基類中有一個方法是toString方法,我們通常在實體類中去重寫此方法來達到打印對象信息的效果,這時候也會發現重寫的toString方法上方就有一個@Override註解。如下所示:

image-20200604203535421

於是,我們試圖去改變重寫后的toString方法名稱,將方法名改為toStrings。你會發現在編譯期就報錯了!如下所示:

image-20200604203645332

那麼這說明什麼呢?這就說明該方法不是我們重寫其父類(Object)的方法。這就是@Override註解的作用。

3.3 @Deprecated註解

用於標記當前類、成員變量、成員方法或者構造方法過時如果開發者調用了被標記為過時的方法,編譯器在編譯期進行警告。

我們解釋@Deprecated註解就需要模擬一種場景了。假設我們公司的產品,目前是V1.0版本,它為用戶提供了show1方法的功能。這時候我們為產品的show1方法的功能又進行了擴展,打算髮布V2.0版本。但是,我們V1.0版本的產品需要拋棄嗎?也就是說我們V1.0的產品功能還繼續讓用戶使用嗎?答案肯定是不能拋棄的,因為有一部分用戶是一直用V1.0版本的。如果拋棄了該版本會損失很多的用戶量,所以我們不能拋棄該版本。這時候,我們對功能進行了擴展后,發布了V2.0版本,我們給予用戶的通知就可以了,也就是告知用戶我們在V2.0版本中為功能進行了擴展。可以讓用戶自行選擇版本。

但是,除了發布告知用戶版本情況之外,我們還需要在原來版本的功能上給予提示,在上面的模擬場景中我們需要在show1方法上方加@Deprecated註解給予提示。通過這種方式也告知用戶“這是舊版本時候的功能了,我們不建議再繼續使用舊版本的功能”,這句話的意思也就正是給用戶做了提示。用戶也會這麼想“奧,這版本的這個功能不好用了,肯定有新版本,又更好用的功能。我要去官網查一下下載新版本”,還會有用戶這麼想“我明白了,又更新出更好的功能了,但是這個版本的功能我已經夠用了,不需要重新下載新版本了”。

那麼我們怎麼查看我上述所說的在功能上給予的提示呢?這時候我需要去創建一個方法,然後去調用show1方法,並查看調用時它是如何提示的。

圖已經貼出來了,你是否發現的新舊版本功能的異同點呢?很明顯,在方法中的提示是在調用的方法名上加了一道橫線把該方法劃掉了。這就體現了show1方法過時了,已經不建議使用了,我們為你提供了更好的。

回想起來,在我們的api中也會有方法是過時的,比如我們的Date日期類中的方法有很多都已經過時了。如下圖:

image-20200604210154348 image-20200604210416762

如你所見,是不是有很多方法都過時了呢?那它的方法上是加了@Deprecated註解嗎?來跟着我的腳步,我帶你們看一下。

我們已經知道的Date類中的這些方法已經是過時的了,如果我們使用該方法並執行該程序的話。執行的過程中就會提示該方法已過時的內容,但是只是提示,並不影響你使用該方法。如下:

image-20200604221938895

OK!這也就是@Deprecated註解的作用了。

3.4 @SuppressWarnings註解

壓制警告註解,可放置在類和方法上,該註解的作用是阻止編譯器發出某些警告信息,該註解為單值註解,只有 一個value參數,該參數為字符串數組類型,參數值常用的有如下幾個。

  • unchecked:未檢查的轉化,如集合沒有指定類型還添加元素
  • unused:未使用的變量
  • resource:有泛型未指定類型
  • path:在類路徑,原文件路徑中有不存在的路徑
  • deprecation:使用了某些不贊成使用的類和方法
  • fallthrough:switch語句執行到底沒有break關鍵字
  • rawtypes:沒有寫泛型,比如: List list = new ArrayList();
  • all:全部類型的警告

壓制警告註解,顧名思義就是壓制警告的出現。我們都知道,在Java代碼的編寫過程中,是有很多黃色警告出現的。但是我不知道你的導師是否教過你,程序員只需要處理紅色的error,不需要理會黃色的warning。如果你的導師說過此問題,那是有原因的。因為在你學習階段,我們認清處理紅色的error即可,這樣可以減輕你學習階段在腦部的記憶內容。如果你剛剛加入學習Java的隊列中,需要大腦記憶的東西就有太多了,也就是我們目前不需要額外記憶其他的東西,只記憶重點即可。至於黃色warning嘛,在你的學習過程中慢慢就會有所了解的,而不是死記硬背的。

那為了解釋@SuppressWarnings註解,我們還使用上一個例子,因為在那個例子中就有黃色的warning出現。

而每一個黃色的warning都會有警告信息的。比如,這一個圖中的警告信息,就告知你show2()方法沒有被使用,簡單來說,你創建的show2方法,但是你在代碼中並沒有調用過此方法。以後你便會遇到各種各樣黃色的warning。然後, 我們就可以使用不同的註解參數來壓制不同的註解。但是在該註解的參數中,提供了一個all參數可以壓制全部類型的警告。而這個註解是需要加到類的上方,並賦予all參數,即可壓制所有警告。如下:

image-20200604213943722

我們加入註解並賦予all參數后,你會發現use方法和show2方法的警告沒有了,實際上導Date包的警告還在,因為我們Date包導入到了該類中,但是我們並沒有創建Date對象,也就是並沒有寫入Date在代碼中,你也會發現那一行是灰色的,也就證明了我們沒有去使用導入這個包的任何信息的說法,出現這種情況我們就需要把這個沒有用的導包內容刪除掉,使用Ctrl + X刪除導入沒有用到的包即可。還有一種辦法就是在包的上方修飾壓制警告註解,但是我認為在一個沒有用的包上加壓制註解是毫無意義的,所以,我們直接刪除就好。

然後,我們還見到上圖,註解那一行出現了警告信息提示。這一行的意思是冗餘的警告壓制。這就是說我們壓制以下的警告並沒有什麼意義而造成的冗餘,但是如果我們使用了該類並做了點什麼的話,壓制註解的冗餘警告就會消失,畢竟我們使用了該類,此時就不會早場冗餘了。

上述解釋@SuppressWarnings註解也差不多就這些了。OK,繼續向下看吧。持續為大家講解。

3.5 @Repeatable註解

@Repeatable 表明標記的註解可以多次應用於相同的聲明或類型,此註解由Java8版本引入。我們知道註解是不能重複定義的,其實該註解就是一個語法糖,它可以重複多此使用,更適用於我們的特殊場景。

首先,我們先創建一個可以重複使用的註解。

package com.mylifes1110.anno;

import java.lang.annotation.Repeatable;

@Repeatable(Hour.class)
public @interface Hours {
    double[] hours() default 0;
}

你會發現註解要求傳入的值是一個類對象,此類對象就需要傳入另外一個註解,這裏也就是另外一個註解容器的類對象。我們去創建一下。

package com.mylifes1110.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//容器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hour {
    Hours[] value();
}

其實,這兩個註解的套用,就是將一個普通的註解封裝了一個可重複使用的註解,來達到註解的復用性。最後,我們創建一下測試類,隨後帶你去看一下源碼。

package com.mylifes1110.java;

import com.mylifes1110.anno.Hours;

@Hours(hours = 4)
@Hours(hours = 4.5)
@Hours(hours = 2)
public class Worker {
    public static void main(String[] args) {
        //通過Hours註解類型來獲取Worker中的值數組對象
        Hours[] hours = Worker.class.getAnnotationsByType(Hours.class);
        //遍曆數組
        for (Hours h : hours) {
            System.out.println(h);
        }
    }
}

測試類,是一個工人測試類,該工人使用註解記錄早中晚的工作時間。測試結果如下:

image-20200606183652359

然後我們進入到源碼一探究竟。

image-20200606183737877

我們發現進入到源碼后,就只看見一個返回值為類對象的抽象方法。這也就驗證了該註解只是一個可實現重複性註解的語法糖而已。

四、註解分類

4.1 註解分類

註解可以根據註解參數分為三大類:

  • 標記註解: 沒有參數的註解,僅用自身的存在與否為程序提供信息,如@Override註解,該註解沒有參數,用於表示當前方法為重寫方法。
  • 單值註解: 只有一個參數的註解,如果該參數的名字為value,那麼可以省略參數名,如 @SuppressWarnings(value = “all”),可以簡寫為@SuppressWarnings(“all”)。
  • 完整註解: 有多個參數的註解。

4.2 標記註解

說到@Override註解是一個標記註解,那我們進入到該註解的源碼查看一下。從上往下看該註解源碼,發現它繼承了導入了java.lang.annotation.*,也就是有使用到該包的內容。然後下面就又是兩個看不懂的註解,其實發現註解的定義格式是public修飾的@Interface,最終看到該註解中方法體並沒有任何參數,也就是只起到標記作用。

4.3 單值註解

在上面我們用到的@SuppressWarnings註解就是一個單值註解。那我們進入到它的源碼看一下是怎麼個情況。其實,和標記註解比較,它就多一個value參數而已,而這就是單值註解的必要條件,即只有一個參數。並且這一個參數為value時,我們可以省略value。

4.4 完整註解

上述兩個類型註解講解完,至於完整註解嘛,這下就能更明白了。其中的方法體就是有多個參數而已。

五、自定義註解

5.1 自定義註解格式

格式: public @Interface 註解名 {屬性列表/無屬性}

注意: 如果註解體中無任何屬性,其本質就是標記註解。但是與其標註註解還少了上邊修飾的元註解。

如下,這就是一個註解。但是它與jdk自定義註解有點區別,jdk自定義註解的上方還有註解來修飾該註解,而那註解就叫做元註解。元註解我會在後面詳細的說到。

image-20200606104149069

這裏我們的確不知道@Interface是什麼,那我們就把自定義的這個註解反編譯一下,看一下反編譯信息。反編譯操作如下:

image-20200606104818131

反編譯后的反編譯內容如下:

public interface com.mylifes1110.anno.MyAnno extends java.lang.annotation.Annotation {
}

首先,看過反編譯內容后,我們可以直觀的得知他是一個接口,因為它的public修飾符後面的關鍵字是interface。

其次,我們發現MyAnno這個接口是繼承了java.lang.annotation包下的Annotation接口。

所以,我們可以得知註解的本質就是一個接口,該接口默認繼承了Annotation接口。

既然,是繼承的Annotation接口,那我們就去進入到這個接口中,看它定義了什麼。以下是我抽取出來的接口內容。我們發現它看似很常見,其實它們不是很常用,作為了解即可。

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

最後,我們的註解中也是可以寫有屬性的,它的屬性不同於普通的屬性,它的屬性是抽象方法。既然註解也是一個接口,那麼我們可以說接口體中可以定義什麼,它同樣也可以定義,而它的修飾符與接口一樣,也是默認被public abstract修飾。

而註解體中的屬性也是有要求的。其屬性要求如下:

  • 屬性的返回值類型必須是以下幾種:
  • 基本數據類型
  • String類型
  • 枚舉類型
  • 註解
  • 以上類型的數組
  • 注意: 在這裏不能有void的無返回值類型和以上類型以外的類型
  • 定義的屬性,在使用時需要給註解中的屬性賦值
  • 如果定義屬性時,使用default關鍵字給屬性默認初始化值,則使用註解時可以不為屬性賦值,它取的是默認值。如果為它再次傳入值,那麼就發生了對原值的覆蓋。
  • 如果只有一個屬性需要賦值,並且屬性的名稱為value,則賦值時value可以省略,可以直接定義值
  • 數組賦值時,值使用{}存儲值。如果數組中只有一個值,則可以省略{}

5.2 自定義註解屬性的返回值

屬性返回值既然有以上幾種,那麼我就在這裏寫出這幾種演示一下是如何寫的。

首先,定義一個枚舉類和另外一個註解備用。

package com.mylifes1110.enums;

public enum Lamp {
    RED, GREEN, YELLOW
}
package com.mylifes1110.anno;

public @interface MyAnno2 {
}

其次,我們來定義上述幾種類型,如下:

package com.mylifes1110.anno;

import com.mylifes1110.enums.Lamp;

public @interface MyAnno {
    //基本數據類型
    int num();

    //String類型
    String value();

    //枚舉類型
    Lamp lamp();

    //註解類型
    MyAnno2 myAnno2();

    //以上類型的數組
    String[] values();
    Lamp[] lamps();
    MyAnno2[] myAnno2s();
    int[] nums();
}

5.3 自定義註解的屬性賦值

這裏我們演示一下,首先,我們使用該註解來進行演示。

package com.mylifes1110.anno;

public @interface MyAnno {
    //基本數據類型
    int num();

    //String類型
    String value();
}

隨後創建一個測試類,在類的上方寫上註解,你會發現,註解的參數中會讓你寫這兩個參數(int、String)。

image-20200606113037920

此時,傳參是這樣來做的。格式為:名稱 = 返回值類型參數。如下:

上述所說,如果使用default關鍵字給屬性默認初始化值,就不需要為其參數賦值,如果賦值的話,就把默認初始化的值覆蓋掉了。

當然還有一個規則,如果只有一個屬性需要賦值,並且屬性的名稱為value,則賦值時value可以省略,可以直接定義值。那麼,我們的num已經有了默認值,就可以不為它傳值。我們發現,註解中定義的屬性就剩下了一個value屬性值,那麼我們就可以來演示這個規則了。

image-20200606113849685

這裏,我並沒有寫屬性名稱value,而是直接為value賦值。如果我將num的default關鍵字修飾去掉呢,那意思也就是說在使用該註解時必須為num賦值,這樣可以省略value嗎?那我們看一下。

image-20200606114216801

結果,就是我們所想的,它報錯了,必須讓我們給num賦值。其實想想這個規則也是很容易懂的,定義一個為value的值,就可以省略其value名稱。如果定義多個值,它們可以省略名稱就無法區分定義的是那個值了,關鍵是還有數組,數組內定義的是多個值呢,對吧。

5.4 自定義註解的多種返回值類型賦值

這裏我們演示一下,上述的多種返回值類型是如何賦值的。這裏我們定義這幾個參數來看一下,是如何為屬性賦值的。

num是一個int基本數據類型,即num = 1

value是一個String類型,即value = "str"

lamp是一個枚舉類型,即lamp = Lamp.RED

myAnno2是一個註解類型,即myAnno2 = @MyAnno2

values是一個String類型數組,即values = {"s1", "s2", "s3"}

values是一個String類型數組,其數組中只有一個值,即values = "s4"

注意: 值與值之間是,隔開的;數組是用{}來存儲值的,如果數組中只有一個值可以省略{};枚舉類型是枚舉名.枚舉值

六、元註解

6.1 元註解分類

元註解就是用來描述註解的註解。一般使用元註解來限制自定義註解的使用範圍、生命周期等等。

而在jdk的中java.lang.annotation包中定義了四個元註解,如下:

元註解 描述
@Target 指定被修飾的註解的作用範圍
@Retention 指定了被修飾的註解的生命周期
@Documented 指定了被修飾的註解是可以Javadoc等工具文檔化
@Inherited 指定了被修飾的註解修飾程序元素的時候是可以被子類繼承的

6.2 @Target

@Target 指定被修飾的註解的作用範圍。其作用範圍可以在源碼中找到參數值。

屬性 描述
CONSTRUCTOR 用於描述構造器
FIELD(常用) 用於描述屬性
LOCAL_VARIABLE 用於描述局部變量
METHOD(常用) 用於描述方法
PACKAGE 用於描述包
PARAMETER 用於描述參數
TYPE(常用) 用於描述類、接口(包括註解類型) 或enum聲明
ANNOTATION_TYPE 用於描述註解類型
TYPE_USE 用於描述使用類型

由此可見,該註解體內只有一個value屬性值,但是它的類型是一個ElementType數組。那我們進入到這個數組中繼續查看。

進入到該數組中,你會發現他是一個枚舉類,其中定義了上述表格中的各個屬性。

了解了@Target的作用和屬性值后,我們來使用一下該註解。首先,我們要先用該註解來修飾一個自定義註解,定義該註解的指定作用在類上。如下:

而你觀察如下測試類,我們把註解作用在類上時是沒有錯誤的。而當我們的註解作用在其他地方就會報錯。這也就說明了,我們@Target的屬性起了作用。

注意: 如果我們定義多個作用範圍時,也是可以省略該參數名稱了,因為該類型是一個數組,雖然能省略名稱但是,我們還需要用{}來存儲。

6.3 @Retention

@Retention 指定了被修飾的註解的生命周期

屬性 描述
RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
RetentionPolicy.CLASS 註解只被保留到編譯進行時的class文件,但 JVM 加載class文件時候被遺棄,也就是在這個階段不會讀取到該class文件。
RetentionPolicy.RUNTIME(常用) 註解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。

注意: 我們常用的定義即是RetentionPolicy.RUNTIME,因為我們使用反射來實現的時候是需要從JVM中獲取class類對象並操作類對象的。

首先,我們要了解反射的三個生命周期階段,這部分內容我在Java反射機制中也是做了非常詳細的說明,有興趣的小夥伴可以去看看我寫的Java反射機制,相信你在其中也會有所收穫。

這裏我再次強調一下這三個生命周期是源碼階段 – > class類對象階段 – > Runtime運行時階段

那我們進入到源碼,看看@Retention註解中是否有這些參數。

我們看到該註解中的屬性只有一個value,而它的類型是一個RetentionPolicy類型,我們進入到該類型中看看有什麼參數,是否與表格中的參數相同呢?

image-20200606145449931

至於該註解怎麼使用,其實是相同的,用法如下:

這就證明了我們的註解可以保留到Runtime運行階段,而我們在反射中大多數是定義到Runtime運行時階段的,因為我們需要從JVM中獲取class類對象並操作類對象。

6.4 @Documented

@Documented 指定了被修飾的註解是可以Javadoc等工具文檔化

@Documented註解是比較好理解的,它是一個標記註解。被該標記註解標記的註解,生成doc文檔時,註解是可以被加載到文檔中显示的。

image-20200606152526551

還拿api中過時的Date中的方法來說,在api中显示Date中的getYear方法是這樣的。

正如你看到的,註解在api中显示了出來,證明該註解是@Documented註解修飾並文檔化的。那我們就看看這個註解是否被@Documented修飾吧。

然後,我們發現該註解的確是被文檔化了。所以在api中才會显示該註解的。如果不信,你可以自己使用javadoc命令來生成一下doc文檔,看看被該註解修飾的註解是否存在。

至於Javadoc文檔生成,我在javadoc文檔生成一文中有過詳細記載,大家可以進行參考,生成doc文檔查看。

6.5 @Inherited

@Inherited 指定了被修飾的註解修飾程序元素的時候是可以被子類繼承的

首先進入到源碼中,我們也可以清楚的知道,該註解也是一個標記註解。而且它也是被文檔化的註解。

其次,我們去在自定義註解中,標註上@Inherited註解。

演示@Inherited註解,我需要創建兩個類,同時兩個類中有一層的繼承關係。如下:

我們在Person類中標記了@MyAnno註解,由於該註解被@Inherited註解修飾,我們就可以得出繼承於Person類的Student類也同樣被@MyAnno註解標記了,如果你要獲取該註解的值的話,肯定獲取的也是父類上註解值的那個”1″。

七、使用反射機制解析註解

自定義註解

package com.mylifes1110.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @InterfaceName Sign
 * @Description 描述需要執行的類名和方法名
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
    String methodName();

    String className();
}

Cat

package com.mylifes1110.java;

/**
 * @ClassName Cat
 * @Description 描述一隻貓的類
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

public class Cat {
    /*
     * @Description 描述一隻貓吃魚的方法 
     * @Author Ziph
     * @Date 2020/6/6
     * @Param []
     * @return void
     */

    public void eat() {
        System.out.println("貓吃魚");
    }
}

準備好,上述代碼后,我們就可以開始編寫使用反射技術來解析註解的測試類。如下:

首先,我們先通過反射來獲取註解中的methodName和className參數。

package com.mylifes1110.java;

import com.mylifes1110.anno.Sign;

/**
 * @ClassName SignTest
 * @Description 要求創建cat對象並執行其類中eat方法
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Sign(className = "com.mylifes1110.java.Cat", methodName = "eat")
public class SignTest {
    public static void main(String[] args) {
        //獲取該類的類對象
        Class<SignTest> signTestClass = SignTest.class;
        //獲取類對象中的註解對象
        //原理實際上是在內存中生成了一個註解接口的子類實現對象
        Sign sign = signTestClass.getAnnotation(Sign.class);
        //調用註解對象中定義的抽象方法(註解中的屬性)來獲取返回值
        String className = sign.className();
        String methodName = sign.methodName();
        System.out.println(className);
        System.out.println(methodName);
    }
}

此時的打印結果證明我們已經成功獲取到了該註解的兩個參數。

image-20200606162810165

注意: 獲取類對象中的註解對象時,其原理實際上是在內存中生成了一個註解接口的子類實現對象並返回的字符串內容。如下:

public class SignImpl implements Sign {
    public String methodName() {
        return "eat";
    }

    public String className() {
        return "com.mylifes1110.java.Cat";
    }
}

繼續編寫我們後面的代碼,代碼完整版如下:

完整版代碼

package com.mylifes1110.java;

import com.mylifes1110.anno.Sign;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @ClassName SignTest
 * @Description 要求創建cat對象並執行其類中eat方法
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Sign(className = "com.mylifes1110.java.Cat", methodName = "eat")
public class SignTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //獲取該類的類對象
        Class<SignTest> signTestClass = SignTest.class;
        //獲取類對象中的註解對象
        //原理實際上是在內存中生成了一個註解接口的子類實現對象
        Sign sign = signTestClass.getAnnotation(Sign.class);
        //調用註解對象中定義的抽象方法(註解中的屬性)來獲取返回值
        String className = sign.className();
        String methodName = sign.methodName();
        //獲取className名稱的類對象
        Class<?> clazz = Class.forName(className);
        //創建對象
        Object o = clazz.newInstance();
        //獲取methodName名稱的方法對象
        Method method = clazz.getMethod(methodName);
        //執行該方法
        method.invoke(o);
    }
}

執行結果

執行后成功的調用了eat方法,並打印了貓吃魚的結果,如下:

八、自定義註解改變JDBC工具類

首先,我們在使用JDBC的時候是需要通過properties文件來獲取配置JDBC的配置信息的,這次我們通過自定義註解來獲取配置信息。其實使用註解並沒有用配置文件好,但是我們需要了解這是怎麼做的,獲取方法也是魚使用反射機制解析註解,所謂“萬變不離其宗”,它就是這樣的。

自定義註解
package com.mylifes1110.java.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @InterfaceName DBInfo
 * @Description 給予註解聲明周期為運行時並限定註解只能用在類上
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBInfo {
    String driver() default "com.mysql.jdbc.Driver";

    String url() default "jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf8";

    String username() default "root"
;

    String password() default "123456";
}
數據庫連接工具類

為了代碼的健全我也在裏面加了properties文件獲取連接的方式。

package com.mylifes1110.java.utils;

import com.mylifes1110.java.anno.DBInfo;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

/**
 * @ClassName DBUtils
 * @Description 數據庫連接工具類
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@DBInfo()
public class DBUtils {
    private static final Properties PROPERTIES = new Properties();
    private static String driver;
    private static String url;
    private static String username;
    private static String password;

    static {
        Class<DBUtils> dbUtilsClass = DBUtils.class;
        boolean annotationPresent = dbUtilsClass.isAnnotationPresent(DBInfo.class);
        if (annotationPresent) {
            /**
             * DBUilts類上有DBInfo註解,並獲取該註解
             */

            DBInfo dbInfo = dbUtilsClass.getAnnotation(DBInfo.class);
//            System.out.println(dbInfo);
            driver = dbInfo.driver();
            url = dbInfo.url();
            username = dbInfo.username();
            password = dbInfo.password();
        } else {
            InputStream inputStream = DBUtils.class.getResourceAsStream("db.properties");
            try {
                PROPERTIES.load(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() {
        try {
            return DriverManager.getConnection(url, username, password);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return null;
    }

    public static void closeAll(Connection connection, Statement statement, ResultSet resultSet) {
        try {
            if (resultSet != null) {
                resultSet.close();
                resultSet = null;
            }

            if (statement != null) {
                statement.close();
                statement = null;
            }
            if (connection != null) {
                connection.close();
                connection = null;
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}
測試類
package com.mylifes1110.java.test;

import com.mylifes1110.java.utils.DBUtils;

import java.sql.Connection;

/**
 * @ClassName GetConnectionDemo
 * @Description 測試連接是否可以獲取到
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

public class GetConnectionDemo {
    public static void main(String[] args) {
        Connection connection = DBUtils.getConnection();
        System.out.println(connection);
    }
}
測試結果

為了證明獲取的連接是由註解的配置信息獲取到的連接,我將properties文件中的所有配置信息刪除后測試的。

九、自定義@MyTest註解實現單元測試

我不清楚小夥伴們是否了解,Junit單元測試。@Test是單元測試的測試方法上方修飾的註解。此註解的核心原理也是由反射來實現的。如果有小夥伴不知道什麼是單元測試或者對自定義@MyTest註解實現單元測試感興趣的話,可以點進來看看哦!

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

ASP.NET Core Blazor WebAssembly實現一個簡單的TODO List

基於blazor實現的一個簡單的TODO List

最近看到一些大佬都開始關注blazor,我也想學習一下。做了一個小的demo,todolist,僅是一個小示例,參考此vue項目的實現http://www.jq22.com/code1339

先看實現的效果圖

不BB,直接可以去看

源碼與預覽地址

  • 示例地址 http://baimocore.cn:8081/
  • 源碼地址 BlazorAppTodoList

源碼介紹

我們這裏刪除了默認的一些源碼。只保留最簡單的結構,在Pages/Index.razor中。

@code代碼結構中寫如下內容

  1. 創建一個類,裡面包含 id,label,isdone三個屬性值。
public class TodoItem
{
    public TodoItem () { }
    public TodoItem (int id, string label, bool isDone)
    {
        Id = id;
        Label = label;
        IsDone = isDone;
    }
    public int Id { get; set; }
    public string Label { get; set; }
    public bool IsDone { get; set; }
}
  1. 我們可以通過override重寫初始化,並給Todos設置一些數據。
private IList<TodoItem> Todos;
private int id = 0;
protected override void OnInitialized ()
{
    Todos = new List<TodoItem> ()
    {
        new TodoItem (++id, "Learn Blazor", false),
        new TodoItem (++id, "Code a todo list", false),
        new TodoItem (++id, "Learn something else", false)
    };
}

展示還有多少未完成的任務

<h1>
        Todo List(@Todos.Count(todo => !todo.IsDone))
        <span>Get things done, one item at a time.</span>
</h1>

當任務沒有時,我們展示默認效果,提示用戶無任務

<p class="emptylist" style="display: @(Todos.Count()>0?"none":"");">Your todo list is empty.</p>

新增一個任務

<form name="newform">
    <label for="newitem">Add to the todo list</label>
    <input type="text" name="newitem" id="newitem" @bind-value="Label">
    <button type="button" @onclick="AddItem">Add item</button>
</form>

這裏我們用了一個Label變量,一個onclick事件。

private string Label;

private void AddItem()
{
    if (!string.IsNullOrWhiteSpace(Label))
    {
        Todos.Add (new TodoItem { Id = ++id, Label = Label });
        Label = string.Empty;
    }
    this.SortByStatus();
}

this.SortByStatus
因為我們這裏還實現一個功能,就是當勾選(當任務完成時,我們將他移到最下面)

<div class="togglebutton-wrapper@(IsActive==true?" togglebutton-checked":"")">
    <label for="todosort">
        <span class="togglebutton-Label">Move done items at the end?</span>
        <span class="tooglebutton-box"></span>
    </label>
    <input type="checkbox" name="todosort" id="todosort" value="@IsActive" @onchange="ActiveChanged">
</div>

一個IsActive的變量,用於指示當前checkbox的樣式,是否開啟已完成的任務移動到最下面。當勾選時,改變IsActive的值。並調用排序的功能。

private bool IsActive = false;
private void ActiveChanged()
{
    this.IsActive = !this.IsActive;
    this.SortByStatus();
}
private void SortByStatus()
{
    if (this.IsActive)
    {
        Todos = Todos.OrderBy(r => r.IsDone).ThenByDescending(r => r.Id).ToList();
    }
    else
    {
        Todos = Todos.OrderByDescending(r => r.Id).ToList();
    }
}

對於列表的展示我們使用如下ul li @for實現

<ul>
    @foreach (var item in Todos)
    {
        <li stagger="5000" class="@(item.IsDone?"done":"")">
            <span class="label">@item.Label</span>
            <div class="actions">
                <button class="btn-picto" type="button"
                        @onclick="@((e)=> {MarkAsDoneOrUndone(item);})"
                        title="@(item.IsDone ? "Undone" :"Done")"
                        aria-label="@(item.IsDone ? "Undone" :"Done")">
                    <i aria-hidden="true" class="material-icons">@(item.IsDone ? "check_box" : "check_box_outline_blank")</i>
                </button>
                <button class="btn-picto" type="button"
                        @onclick="@((e)=> { DeleteItemFromList(item); })"
                        aria-Label="Delete" title="Delete">
                    <i aria-hidden="true" class="material-icons">delete</i>
                </button>
            </div>
        </li>
    }
</ul>

循環Todos,然後,根據item.IsDone,改變li的樣式,從而實現一个中劃線的功能,二個按鈕的功能,一個是勾選任務表示此任務已完成,另一個是刪除此任務。同理,我們仍然通過IsDone來標識完成任務的圖標,標題等。

  • 任務設置已完成/設置為未完成: @onclick調用方法MarkAsDoneOrUndone,並將當前的一行記錄item傳給方法,需要使用一個匿名函數調用@code中的方法,將isDone取相反的值,並重新排序。
private void MarkAsDoneOrUndone(TodoItem item)
{
    item.IsDone = !item.IsDone;
    this.SortByStatus();
}
  • 刪除一個任務,同理,使用匿名函數,DeleteItemFromList直接通過IList的方法Remove刪除一個對象,並排序。
private void DeleteItemFromList(TodoItem item)
{
    Todos.Remove(item);
    this.SortByStatus();
}

當然,我們可以 在ul,外包裹一層,根據Count判斷有沒有任務,從而显示這個列表。

<div style="display: @(Todos.Count()>0?"":"none");"><ul>xxx</ul></div>

其他的樣式與圖標,請看最上面的源碼wwwroot/css目錄獲取。

deploy(部署)

  • 有興趣的可以看官網 https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/blazor/?view=aspnetcore-3.1&tabs=visual-studio

在項目根目錄執行如下命令

dotnet publish -c Release

我們就能得到一個發布包,他的位置在 (BlazorAppTodoList\bin\Release\netstandard2.1\publish) ,我們把他複製到服務器上,這裏我放到/var/www/todolilst目錄中。

它相當於一個靜態文件,你可以將他部署到任何一個web服務器上。

這裏我們把他放到nginx中,並在目錄/etc/nginx/conf.d/ 新建一個文件 todolist.conf,然後放入如下內容。

 server {
        listen 8081;

        location / {
            root /var/www/todolist/wwwroot;
            try_files $uri $uri/ /index.html =404;
        }
}

記得在etc/nginx/nginx.conf中配置gzip壓縮。

gzip  on;
gzip_min_length 5k; #gzip壓縮最小文件大小,超出進行壓縮(自行調節)
gzip_buffers 4 16k; #buffer 不用修改
gzip_comp_level 8; #壓縮級別:1-10,数字越大壓縮的越好,時間也越長
gzip_types text/plain application/x-javascript application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/octet-stream; #  壓縮文件類型 
gzip_vary on; # 和http頭有關係,加個vary頭,給代理服務器用的,有的瀏覽器支持壓縮,有的不支持,所以避免浪費不支持的也壓縮,所以根據客戶端的HTTP頭來判斷,是否需要壓縮

我遇到dll,wasm,後綴的文件壓縮無效。因為gzip_types ,沒有配置他們的Content-Type。我們在瀏覽器中找到響應頭Content-Type: application/octet-stream,然後在上文中的nginx配置文件中,gzip_types加上application/octet-stream,

最後執行

nginx -t
nginx -s reload

打開網站看效果

http://baimocore.cn:8081

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

Python3 源碼閱讀 – 內存管理機制

Python 內存管理分層架構

/* An object allocator for Python.

   Here is an introduction to the layers of the Python memory architecture,
   showing where the object allocator is actually used (layer +2), It is
   called for every object allocation and deallocation (PyObject_New/Del),
   unless the object-specific allocators implement a proprietary allocation
   scheme (ex.: ints use a simple free list). This is also the place where
   the cyclic garbage collector operates selectively on container objects.


    Object-specific allocators
    _____   ______   ______       ________
   [ int ] [ dict ] [ list ] ... [ string ]       Python core         |
+3 | <----- Object-specific memory -----> | <-- Non-object memory --> |
    _______________________________       |                           |
   [   Python's object allocator   ]      |                           |
+2 | ####### Object memory ####### | <------ Internal buffers ------> |
    ______________________________________________________________    |
   [          Python's raw memory allocator (PyMem_ API)          ]   |
+1 | <----- Python memory (under PyMem manager's control) ------> |   |
    __________________________________________________________________
   [    Underlying general-purpose allocator (ex: C library malloc)   ]
 0 | <------ Virtual memory allocated for the python process -------> |

   =========================================================================
    _______________________________________________________________________
   [                OS-specific Virtual Memory Manager (VMM)               ]
-1 | <--- Kernel dynamic storage allocation & management (page-based) ---> |
    __________________________________   __________________________________
   [                                  ] [                                  ]
-2 | <-- Physical memory: ROM/RAM --> | | <-- Secondary storage (swap) --> |

*/

reference:Objects/obmalloc.c

layer 3: Object-specific memory(int/dict/list/string....)
		python 實現並維護
		用戶對Python對象的直接操作,主要是各類特定對象的緩衝池機制,緩衝池,比如小整數對象池等等
layer 2: Python's object allocator
		實現了創建/銷毀python對象的接口(PyObject_New/Del),涉及對象參數/引用計數等

layer 1: Python's raw memory allocator (PyMem_ API)
		包裝了第0層的內存管理接口,提供同一個raw memory管理接口
		封裝的原因:不同操作系統C行為不一致,保證可移植性,相同語義相同行為
		
layer 0: Underlying general-purpose allocator (ex: C library malloc)
		操作系統提供的內存管理接口,由操作系統實現並管理,Python不能干涉這一層的行為,大內存 分配調用malloc函數分配內存

Python 內存分配策略之-block,pool

Python中有分為大內存和小內存,512K為分界線

  • 大內存使用系統malloc進行分配

  • 小內存使用python內存池進行分配

1. 如果要分配的內存空間大於 SMALL_REQUEST_THRESHOLD bytes(512 bytes), 將直接使用layer 1的內存分配接口進行分配
2. 否則, 使用不同的block來滿足分配需求
申請一塊大小28字節的內存, 實際從內存中劃到32字節的一個block (從size class index為3的pool裏面劃出)

block

內存塊block 是python內存的最小單位

* For small requests we have the following table:
 *
 * Request in bytes     Size of allocated block      Size class idx
 * ----------------------------------------------------------------
 *        1-8                     8                       0
 *        9-16                   16                       1
 *       17-24                   24                       2
 *       25-32                   32                       3
 *       33-40                   40                       4
 *       41-48                   48                       5
 *       49-56                   56                       6
 *       57-64                   64                       7
 *       65-72                   72                       8
 *        ...                   ...                     ...
 *      497-504                 504                      62
 *      505-512                 512                      63
 *
 *      0, SMALL_REQUEST_THRESHOLD + 1 and up: routed to the underlying
 *      allocator.
 */

pool

pool內存池,管理block, 一個pool管理着一堆固定大小的內存塊,在Python中, 一個pool的大小通常為一個系統內存頁. 4kB

#define SYSTEM_PAGE_SIZE        (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)

#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK

pool的4kB內存 = pool_header + block集合(N多大小一樣的block)

typedef uint8_t block;

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

pool_header 作用

與其他pool鏈接, 組成雙向鏈表
2. 維護pool中可用的block, 單鏈表
3. 保存 szidx , 這個和該pool中block的大小有關係, (block size=8, szidx=0), (block size=16, szidx=1)...用於內存分配時匹配到擁有對應大小block的pool

pool 初始化

void *
PyObject_Malloc(size_t nbytes)
{
  ...

          init_pool:
            // 1. 連接到 used_pools 雙向鏈表, 作為表頭
            // 注意, 這裏 usedpools[0] 保存着 block size = 8 的所有used_pools的表頭
            /* Frontlink to used pools. */
            next = usedpools[size + size]; /* == prev */
            pool->nextpool = next;
            pool->prevpool = next;
            next->nextpool = pool;
            next->prevpool = pool;
            pool->ref.count = 1;

            // 如果已經初始化過了...這裏看初始化, 跳過
            if (pool->szidx == size) {
                /* Luckily, this pool last contained blocks
                 * of the same size class, so its header
                 * and free list are already initialized.
                 */
                bp = pool->freeblock;
                pool->freeblock = *(block **)bp;
                UNLOCK();
                return (void *)bp;
            }


            /*
             * Initialize the pool header, set up the free list to
             * contain just the second block, and return the first
             * block.
             */
            // 開始初始化pool_header
            // 這裏 size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;  其實是Size class idx, 即szidx
            pool->szidx = size;

            // 計算獲得每個block的size
            size = INDEX2SIZE(size);

            // 注意 #define POOL_OVERHEAD           ROUNDUP(sizeof(struct pool_header))
            // bp => 初始化為pool + pool_header size,  跳過pool_header的內存
            bp = (block *)pool + POOL_OVERHEAD;

            // 計算偏移量, 這裏的偏移量是絕對值
            // #define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
            // POOL_SIZE = 4kb, POOL_OVERHEAD = pool_header size
            // 下一個偏移位置: pool_header size + 2 * size
            pool->nextoffset = POOL_OVERHEAD + (size << 1);
            // 4kb - size
            pool->maxnextoffset = POOL_SIZE - size;

            // freeblock指向 bp + size = pool_header size + size
            pool->freeblock = bp + size;

            // 賦值NULL
            *(block **)(pool->freeblock) = NULL;
            UNLOCK();
            return (void *)bp;
        }

pool 進行block分配 – 總體代碼

  if (pool != pool->nextpool) {   //
            /*
             * There is a used pool for this size class.
             * Pick up the head block of its free list.
             */
            ++pool->ref.count;
            bp = pool->freeblock; // 指針指向空閑block起始位置
            assert(bp != NULL);

            // 代碼-1
            // 調整 pool->freeblock (假設A節點)指向鏈表下一個, 即bp首字節指向的下一個節點(假設B節點) , 如果此時!= NULL
            // 表示 A節點可用, 直接返回
            if ((pool->freeblock = *(block **)bp) != NULL) {
                UNLOCK();
                return (void *)bp;
            }

            // 代碼-2
            /*
             * Reached the end of the free list, try to extend it.
             */
            // 有足夠的空間, 分配一個, pool->freeblock 指向後移
            if (pool->nextoffset <= pool->maxnextoffset) {
                /* There is room for another block. */
                // 變更位置信息
                pool->freeblock = (block*)pool +
                                  pool->nextoffset;
                pool->nextoffset += INDEX2SIZE(size);


                *(block **)(pool->freeblock) = NULL; // 注意, 指向NULL
                UNLOCK();

                // 返回bp
                return (void *)bp;
            }

            // 代碼-3
            /* Pool is full, unlink from used pools. */  // 滿了, 需要從下一個pool獲取
            next = pool->nextpool;
            pool = pool->prevpool;
            next->prevpool = pool;
            pool->nextpool = next;
            UNLOCK();
            return (void *)bp;
        }

pool進行block分配 -1

內存塊尚未分配完, 且此時不存在回收的block, 全新進來的時候, 分配第一塊block

(pool->freeblock = *(block **)bp) == NULL

當進入代碼邏輯2時,表示有空閑的block, 代碼2的執行流程圖如下

pool進行block分配 – 2 回收了某幾個block

回收涉及的代碼:

void
PyObject_Free(void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;

    pool = POOL_ADDR(p);
    if (Py_ADDRESS_IN_RANGE(p, pool)) {
        /* We allocated this address. */
        LOCK();
        /* Link p to the start of the pool's freeblock list.  Since
         * the pool had at least the p block outstanding, the pool
         * wasn't empty (so it's already in a usedpools[] list, or
         * was full and is in no list -- it's not in the freeblocks
         * list in any case).
         */
        assert(pool->ref.count > 0);            /* else it was empty */
        // p被釋放, p的第一個字節值被設置為當前freeblock的值
        *(block **)p = lastfree = pool->freeblock;
        // freeblock被更新為指向p的首地址
        pool->freeblock = (block *)p;

        // 相當於往list中頭插入了一個節點

     ...
    }
}

每釋放一個block,該blcok就會變成pool->freeblock的頭結點, 假設已經連續分配了5塊, 第1塊和第4塊被釋放,此時的內存圖示如下:

此時再一個block分配調用進來, 執行分配, 進入的邏輯是代碼-1

bp = pool->freeblock; // 指針指向空閑block起始位置
// 代碼-1
// 調整 pool->freeblock (假設A節點)指向鏈表下一個, 即bp首字節指向的下一個節點(假設B節點) , 如果此時!= NULL
// 表示 A節點可用, 直接返回
if ((pool->freeblock = *(block **)bp) != NULL) {
    UNLOCK();
    return (void *)bp;
}

pool進行block分配 – 3 pool用完了

pool中內存空間都用完了, 進入代碼-3

/* Pool is full, unlink from used pools. */  // 滿了, 需要從下一個pool獲取
next = pool->nextpool;
pool = pool->prevpool;
next->prevpool = pool;
pool->nextpool = next;
UNLOCK();
return (void *)bp;

Python 內存分配策略之-arena

arena: 多個pool聚合的結果, 可放置64個pool

#define ARENA_SIZE              (256 << 10)     /* 256KB */

arena結構

一個完整的arena = arena_object + pool集合

/* Record keeping for arenas. */
struct arena_object {
    /* The address of the arena, as returned by malloc.  Note that 0
     * will never be returned by a successful malloc, and is used
     * here to mark an arena_object that doesn't correspond to an
     * allocated arena.
     */
    uintptr_t address;

    /* Pool-aligned pointer to the next pool to be carved off. */
    block* pool_address;

    /* The number of available pools in the arena:  free pools + never-
     * allocated pools.
     */
    uint nfreepools;

    /* The total number of pools in the arena, whether or not available. */
    uint ntotalpools;

    /* Singly-linked list of available pools. */
    struct pool_header* freepools;

    /* Whenever this arena_object is not associated with an allocated
     * arena, the nextarena member is used to link all unassociated
     * arena_objects in the singly-linked `unused_arena_objects` list.
     * The prevarena member is unused in this case.
     *
     * When this arena_object is associated with an allocated arena
     * with at least one available pool, both members are used in the
     * doubly-linked `usable_arenas` list, which is maintained in
     * increasing order of `nfreepools` values.
     *
     * Else this arena_object is associated with an allocated arena
     * all of whose pools are in use.  `nextarena` and `prevarena`
     * are both meaningless in this case.
     */
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};
arena_object的作用
1. 與其他arena連接, 組成雙向鏈表
2. 維護arena中可用的pool, 單鏈表
  • pool_header和管理的blocks內存是一塊連續的內存 => pool_header被申請時,其管理的的block集合的內存一併被申請 uint maxnextoffset; /* largest valid nextoffset */
  • arena_object 和其管理的內存是分離的 => arena_object被申請時,其管理的pool集合的內存沒有被申請,而是在某一時刻建立關係的

arena的兩種狀態

/* The head of the singly-linked, NULL-terminated list of available
 * arena_objects.
 */
// 單鏈表
static struct arena_object* unused_arena_objects = NULL;

/* The head of the doubly-linked, NULL-terminated at each end, list of
 * arena_objects associated with arenas that have pools available.
 */
// 雙向鏈表
static struct arena_object* usable_arenas = NULL;

arena 初始化

* Allocate a new arena.  If we run out of memory, return NULL.  Else
 * allocate a new arena, and return the address of an arena_object
 * describing the new arena.  It's expected that the caller will set
 * `usable_arenas` to the return value.
 */
static struct arena_object*
new_arena(void)
{
    struct arena_object* arenaobj;
    uint excess;        /* number of bytes above pool alignment */
    void *address;
    static int debug_stats = -1;

    if (debug_stats == -1) {
        const char *opt = Py_GETENV("PYTHONMALLOCSTATS");
        debug_stats = (opt != NULL && *opt != '\0');
    }
    if (debug_stats)
        _PyObject_DebugMallocStats(stderr);

    // 判斷是否需要擴充"未使用"的arena_object列表
    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;

        /* Double the number of arena objects on each allocation.
         * Note that it's possible for `numarenas` to overflow.
         */
        // 確定需要申請的個數, 首次初始化, 16, 之後每次翻倍
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        if (numarenas <= maxarenas)
            return NULL;                /* overflow */
#if SIZEOF_SIZE_T <= SIZEOF_INT
        if (numarenas > SIZE_MAX / sizeof(*arenas))
            return NULL;                /* overflow */
#endif
        nbytes = numarenas * sizeof(*arenas);
        // 申請內存
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;

        /* We might need to fix pointers that were copied.  However,
         * new_arena only gets called when all the pages in the
         * previous arenas are full.  Thus, there are *no* pointers
         * into the old array. Thus, we don't have to worry about
         * invalid pointers.  Just to be sure, some asserts:
         */
        assert(usable_arenas == NULL);
        assert(unused_arena_objects == NULL);

        /* Put the new arenas on the unused_arena_objects list. */
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /* mark as unassociated */
            // 新申請的一律為0, 標識着這個arena處於"未使用"
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

         // 將其放入unused_arena_objects鏈表中
        // unused_arena_objects 為新分配內存空間的開頭
        /* Update globals. */
        unused_arena_objects = &arenas[maxarenas];
        maxarenas = numarenas;
    }

    /* Take the next available arena object off the head of the list. */
    assert(unused_arena_objects != NULL);
    // 從unused_arena_objects中, 獲取一個未使用的object
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;  // 更新鏈表
    assert(arenaobj->address == 0);
    // 申請內存, 256KB, 內存地址賦值給arena的address. 這塊內存可用
    address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
    if (address == NULL) {
        /* The allocation failed: return NULL after putting the
         * arenaobj back.
         */
        arenaobj->nextarena = unused_arena_objects;
        unused_arena_objects = arenaobj;
        return NULL;
    }
    arenaobj->address = (uintptr_t)address;

    ++narenas_currently_allocated;
    ++ntimes_arena_allocated;
    if (narenas_currently_allocated > narenas_highwater)
        narenas_highwater = narenas_currently_allocated;
    arenaobj->freepools = NULL;
    /* pool_address <- first pool-aligned address in the arena
       nfreepools <- number of whole pools that fit after alignment */
    arenaobj->pool_address = (block*)arenaobj->address;
    arenaobj->nfreepools = MAX_POOLS_IN_ARENA;
    // 將pool的起始地址調整為系統頁的邊界
    // 申請到 256KB, 放棄了一些內存, 而將可使用的內存邊界pool_address調整到了與系統頁對齊
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}

從arenas取一個arena進行初始化

arena分配

new一個全新的arena

static void*
pymalloc_alloc(void *ctx, size_t nbytes)
 {
            // 剛開始沒有可用的arena
            if (usable_arenas == NULL) {
              // new一個, 作為雙向鏈表的表頭
              usable_arenas = new_arena();
              if (usable_arenas == NULL) {
                  UNLOCK();
                  goto redirect;
              }

              usable_arenas->nextarena =
                  usable_arenas->prevarena = NULL;

           }

          .......

          // 從arena中獲取一個pool
          pool = (poolp)usable_arenas->pool_address;
          assert((block*)pool <= (block*)usable_arenas->address +
                                 ARENA_SIZE - POOL_SIZE);
          pool->arenaindex = usable_arenas - arenas;
          assert(&arenas[pool->arenaindex] == usable_arenas);
          pool->szidx = DUMMY_SIZE_IDX;

          // 更新 pool_address 向下一個節點
          usable_arenas->pool_address += POOL_SIZE;
          // 可用節點數量-1
          --usable_arenas->nfreepools;

}

從全新的arena中獲取一個pool

假設arena是舊的, 怎麼分配的pool, 跟pool分配block原理一樣,使用單鏈表記錄freepools

pool = usable_arenas->freepools;
if (pool != NULL) {

當arena中一整塊pool被釋放的時候

/* Free a memory block allocated by pymalloc_alloc().
   Return 1 if it was freed.
   Return 0 if the block was not allocated by pymalloc_alloc(). */
static int
pymalloc_free(void *ctx, void *p) {
    struct arena_object* ao;
    uint nf;  /* ao->nfreepools */

    /* Link the pool to freepools.  This is a singly-linked
               * list, and pool->prevpool isn't used there.
              */
    ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    nf = ++ao->nfreepools;
}

在pool整塊被釋放的時候, 會將pool加入到arena->freepools作為單鏈表的表頭, 然後, 在從非全新arena中分配pool時, 優先從arena->freepools裏面取, 如果取不到, 再從arena內存塊裏面獲取

注: 上圖中nfreepools = n – 2

當arena1用完了,獲取arena1指向的下一個節點arena2

static void*
pymalloc_alloc(void *ctx, size_t nbytes)
{


          // 當發現用完了最後一個pool!!!!!!!!!!!
          // nfreepools = 0
          if (usable_arenas->nfreepools == 0) {
              assert(usable_arenas->nextarena == NULL ||
                     usable_arenas->nextarena->prevarena ==
                     usable_arenas);
              /* Unlink the arena:  it is completely allocated. */

              // 找到下一個節點!
              usable_arenas = usable_arenas->nextarena;
              // 右下一個
              if (usable_arenas != NULL) {
                  usable_arenas->prevarena = NULL; // 更新下一個節點的prevarens
                  assert(usable_arenas->address != 0);
              }
              // 沒有下一個, 此時 usable_arenas = NULL, 下次進行內存分配的時候, 就會從arenas數組中取一個

          }

  }

注意: 這裡有個邏輯, 就是每分配一個pool, 就檢查是不是用到了最後一個, 如果是, 需要變更usable_arenas到下一個可用的節點, 如果沒有可用的, 那麼下次進行內存分配的時候, 會判定從arenas數組中取一個

arena回收

內存分配和回收最小單位是block, 當一個block被回收的時候, 可能觸發pool被回收, pool被回收, 將會觸發arena的回收機制

    1. arena中所有pool都是閑置的(empty), 將arena內存釋放, 返回給操作系統
    1. 如果arena中之前所有的pool都是佔用的(used), 現在釋放了一個pool(empty), 需要將 arena加入到usable_arenas, 會加入鏈表表頭
    1. 如果arena中empty的pool個數n, 則從useable_arenas開始尋找可以插入的位置. 將arena插入. (useable_arenas是一個有序鏈表, 按empty pool的個數, 保證empty pool數量越多, 被使用的幾率越小, 最終被整體釋放的機會越大)

內存分配的步驟

關注點:如何尋找到一塊可用的nbytes的blcok內存?

pool = usedpools[size + size]

if pool:

​ pool 沒滿,取一個blcok返回

​ pool 滿了,從下一個pool取一個blcok返回

else:

​ 獲取arena, 从里面初始化一個pool, 拿到第一個blcok返回

進行內存分配和銷毀, 所有操作都是在pool上進行的

問題: pool中所有block的size一樣, 但是在arena中, 每個pool的size都可能不一樣, 那麼最終這些pool是怎麼維護的? 怎麼根據大小找到需要的block所在的pool? => usedpools

pool在內存池中的三種狀態

  1. used狀態:pool中至少有一個block已經被使用,並且至少有一個block未被使用,這種狀態的pool受控於Python內部維護的usedpool數組
  2. full狀態:pool中所有的block都已經被使用,這種狀態的pool在arena中, 但不在arena的freepools鏈表中,處於full的pool各自獨立, 不會被鏈表維護起來
  3. empty狀態:pool中所有的blcok都未被使用,處於這個狀態的pool的集合通過其pool_header中的nextpool構成一個鏈表,鏈表的表頭示arena_object中的freepools

Python內部維護的usedpools數組是一個非常巧妙的實現,維護着所有的處於used狀態的pool,當申請內存時,python就會通過usedpools尋找到一個可用的pool(處於used狀態),從中分配一個block。因此我們想,一定有一個usedpools相關聯的機制,完成從申請的內存的大小到size class index之間的轉換,否則python就無法找到最合適的pool了。這種機制和usedpools的結構有着密切的關係,我們看一下它的結構

usedpools

usedpools數組: 維護着所有處於used狀態的pool, 當申請內存的時候, 會通過usedpools尋找到一塊可用的(處於used狀態的)pool, 從中分配一個block。

//obmalloc.c
typedef uint8_t block;
#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

//在我當前的機器就是512/8=64個,對應的size class index就是從0到63
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
#if NB_SMALL_SIZE_CLASSES > 16
    , PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
#if NB_SMALL_SIZE_CLASSES > 24
    , PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
#if NB_SMALL_SIZE_CLASSES > 32
    , PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
#if NB_SMALL_SIZE_CLASSES > 40
    , PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
#if NB_SMALL_SIZE_CLASSES > 48
    , PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
#if NB_SMALL_SIZE_CLASSES > 56
    , PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
#if NB_SMALL_SIZE_CLASSES > 64
#error "NB_SMALL_SIZE_CLASSES should be less than 64"
#endif /* NB_SMALL_SIZE_CLASSES > 64 */
#endif /* NB_SMALL_SIZE_CLASSES > 56 */
#endif /* NB_SMALL_SIZE_CLASSES > 48 */
#endif /* NB_SMALL_SIZE_CLASSES > 40 */
#endif /* NB_SMALL_SIZE_CLASSES > 32 */
#endif /* NB_SMALL_SIZE_CLASSES > 24 */
#endif /* NB_SMALL_SIZE_CLASSES > 16 */
#endif /* NB_SMALL_SIZE_CLASSES >  8 */
};

如果正在申請28字節, python首先會獲取(size class index) size = (uint )(nbytes - 1) >> ALIGNMENT_SHIFT 顯然這裏size=3, 那麼在usedpools中,尋找第3+3=6個元素,發現usedpools[6]的值是指向usedpools[4]的地址

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 當然pool裏面的block數量    */
    block *freeblock;                   /* 一個鏈表,指向下一個可用的block   */
    struct pool_header *nextpool;       /* 指向下一個pool  */
    struct pool_header *prevpool;       /* 指向上一個pool       ""        */
    uint arenaindex;                    /* 在area裏面的索引 */
    uint szidx;                         /* block的大小(固定值?後面說)     */
    uint nextoffset;                    /* 下一個可用block的內存偏移量         */
    uint maxnextoffset;                 /* 最後一個block距離開始位置的距離     */
};

顯然是從usedpools[6](即usedpools+4)開始向後偏移8個字節(一個ref的大小加上一個freeblock的大小)后的內存,正好是usedpools[6]的地址(即usedpools+6),這是python內部的trick

當我們要申請一個size class為32字節的pool,想要將其放入這個usedpools中時,要怎麼做呢?從上面的描述我們知道,只需要進行usedpools[i+i] -> nextpool = pool即可,其中i為size class index,對應於32字節,這個i為3.當下次需要訪問size class 為32字節(size class index為3)的pool時,只需要簡單地訪問usedpools[3+3]就可以得到了。python正是使用這個usedpools快速地從眾多的pool中快速地尋找到一個最適合當前內存需求的pool,從中分配一塊block。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    poolp next;
    uint size;
    ...
    LOCK();
    //獲得size class index
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    //直接通過usedpools[size+size],這裏的size不就是我們上面說的i嗎?
    pool = usedpools[size + size];
    //如果usedpools中有可用的pool
    if (pool != pool->nextpool) {
        ... //有可用pool
    }
    ... //無可用pool,嘗試獲取empty狀態的pool
}  

內存池全局結構

參考:

pyhton源碼閱讀-內存管理機制

python源碼解析第17章-python內存管理與垃圾回收

後期查缺補漏需要看的文章

Memory management by Zpoint
Memory management in Python

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

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

疫情缺工夠慘了 百年大旱讓波蘭農民雪上加霜

摘錄自2020年04月26日中央通訊社波蘭報導

武漢肺炎疫情導致來自烏克蘭的季節工人數大減,波蘭農民已經苦不堪言;現在,百年未見的大旱災也跑來湊熱鬧,農民境況更如雪上加霜。

農業部長阿德諾斯基(Jan Krzysztof Ardanowski)警告,旱災恐傷及糧食產量,雖然沒有短缺風險,價格卻可能水漲船高。總統杜達(Andrzej Duda)也憂心忡忡,呼籲民眾節約用水。波蘭旱災並非新鮮事,但現在情況卻是日趨惡化。波蘭最高審計局(Supreme Audit Office)去年就已警告,每年每名波蘭人只能用1600立方公尺的水。

旱災的嚴重程度,就連首都華沙也明顯可見:波蘭最大且最長的河川維斯杜拉河(Vistula)現在水位已降到60公分,通常隱藏在水下2公尺的沙洲都已冒出頭來。「地面很乾,要向下挖個15公分的洞,才找得到水分。」

除了人手不夠,布科夫斯卡-拉薩斯卡還有一個大問題:假新聞。「我們聽人家說,要用溫水洗草莓才能洗掉病毒。草莓禁不起熱水,我們得把它們醃製成蜜餞。」但這種說法根本是謠言,波蘭衛生部告訴法新社,歐洲食品安全局(European Food Safety Authority)上月就說,「無證據顯示食物是傳染來源或散播途徑」。

永續發展
土地利用
國際新聞
波蘭
烏克蘭
旱災
農業缺工
武漢肺炎
疫情下的食衣住行
糧食

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

動物園因疫情撐不下去……就怕全餓死 浣熊恐率先安樂死

摘錄自2020年04月25日自由時報報導

英國一間小型動物園因為無法負擔龐大的支出,負責人說,最壞的打算就是將動物「安樂死」,而浣熊將會首當其衝。

綜合外媒報導,51歲的安迪.科威爾(Andy Cowell)在英國肯特郡聖瑪利胡(St Mary Hoo, Kent)經營一間小型動物園,受疫情影響,園區遊客銳減,他花盡畢生積蓄,還是無法負擔一日2000英鎊(約新台幣7萬4000元)的龐大開銷,最壞的打算是將動物安樂死,避免牠們活活餓死。由於浣熊被英國政府列為入侵物種,因此會最先成為安樂死對象。

生活環境
國際新聞
英國
武漢肺炎
動物園
疫情下的食衣住行

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

「疫」外減碳!全球碳排放今年估降6% 二戰以來最大降幅

摘錄自2020年4月22日自由時報報導

世界氣象組織(World Meteorological Organization, WMO)負責人今(22)日表示,預計今年武漢肺炎(COVID-19)疫情將讓二氧化碳排放量減少6%,是自第二次世界大戰以來最大降幅。

然而,聯合國機構表示,下降幅度仍不足以阻止氣候變化,並敦促各國政府將氣候行動納入復甦計畫。WMO警告,過去經濟復甦帶來的排放量增長甚至比危機爆發前更高。WMO週三(22日)還發布一份全球氣候報告,內容指出2015-2019年是有記錄以來最溫暖的5年。

生活環境
全球變遷
溫室氣體
氣候變遷
國際新聞
二氧化碳排放
疫情看氣候與能源
武漢肺炎

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

文在寅防疫受肯定 南韓國會大選民主黨大勝 2050碳中和實現有望

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

科學家在北義空污粒子上檢出新冠病毒 傳播距離、病毒是否存活仍待釐清

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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