垃圾回收算法與垃圾回收器

JavaC++等語言最大的技術區別:自動化的垃圾回收機制(GC

為什么要了解GC和內存分配策略

1、面試需要

2GC對應用的性能是有影響的;

3、寫代碼有好處

棧:棧中的生命周期是跟隨線程,所以一般不需要關注

堆:堆中的對象是垃圾回收的重點

方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點

判斷對象的存活

引用計數法

給對象添加一個引用計數器,當對象增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的對象可被回收。(Python在用,但主流虛擬機沒有使用)

優點:快,方便,實現簡單。

缺陷:對象相互引用時(A.instance=B同時B.instance=A),很難判斷對象是否該回收。

可達性分析(Java中使用)

(面試時重要的知識點,牢記)

來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

作為GC Roots的對象包括下面幾種:

當前虛擬機棧中局部變量表中的引用的對象

當前本地方法棧中局部變量表中的引用的對象

方法區中類靜態屬性引用的對象

方法區中的常量引用的對象

請忘記 finalize

finalize可以完成對象的拯救,但是JVM不保證一定能執行,所以請忘記這個“坑”。

各種引用(Reference

傳統定義:Reference中存儲的數據代表的是另一塊內存的起始地址。

強引用

一般的Object obj = new Object() ,就屬于強引用。

(如果有GCroots的強引用)垃圾回收器絕對不會回收它,當內存不足時寧愿拋出 OOM 錯誤,使得程序異常停止

軟引用 SoftReference

垃圾回收器在內存充足的時候不會回收它,而在內存不足時會回收它

軟引用非常適合于創建緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。

一些有用但是并非必需,用軟引用關聯的對象,系統將要發生OOM之前,這些對象就會被回收。參見代碼:

VM參數 -Xms10m ?-Xmx10m -XX:+PrintGC

?

運行結果

?

例如,一個程序用來處理用戶提供的圖片。如果將所有圖片讀入內存,這樣雖然可以很快的打開圖片,但內存空間使用巨大,一些使用較少的圖片浪費內存空間,需要手動從內存中移除。如果每次打開圖片都從磁盤文件中讀取到內存再顯示出來,雖然內存占用較少,但一些經常使用的圖片每次打開都要訪問磁盤,代價巨大。這個時候就可以用軟引用構建緩存。

弱引用 WeakReference

垃圾回收器在掃描到該對象時,無論內存充足與否,都會回收該對象的內存。

一些有用(程度比軟引用更低)但是并非必需,用弱引用關聯的對象,只能生存到下一次垃圾回收之前,GC發生時,不管內存夠不夠,都會被回收。

參看代碼:

?

?

注意:軟引用 SoftReference和弱引用 WeakReference,可以用在內存資源緊張的情況下以及創建不是很重要的數據緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。

實際運用(WeakHashMapThreadLocal

虛引用 PhantomReference

幽靈引用,最弱,被垃圾回收的時候收到一個通知

如果一個對象只具有虛引用,那么它和沒有任何引用一樣,任何時候都可能被回收。

虛引用主要用來跟蹤對象被垃圾回收器回收的活動

GCGarbage Collection

案例Oom

-Xms ?堆區內存初始內存分配的大小

-Xmx ?堆區內存可被分配的最大上限

-XX:+PrintGCDetails

打印GC詳情

-XX:+HeapDumpOnOutOfMemoryError

當堆內存空間溢出時輸出堆的內存快照

新生代大小配置參數的優先級:

中間 -Xmn ?限定大小

?

-XX:SurvivorRatio

2Survivor區和Eden區的比值

8 表示 兩個Survivor Eden = 28 ,每個Survivor1/10

可以修改為2

8 表示 兩個Survivor Eden = 22 ?,各占一半

GC overhead limit exceeded 超過98%的時間用來做GC并且回收了不到2%的堆內存時會拋出此異常

1.垃圾回收會占據資源

2.回收效率過低也會有限制

為什么new出的對象不會被回收了,我們來看看GC是如何判斷對象的存活

?

?

Minor GC

特點: 發生在新生代上,發生的較頻繁,執行速度較快

觸發條件: Eden區空間不足\空間分配擔保

Full GC

特點:主要發生在老年代上(新生代也會回收),較少發生,執行速度較慢

觸發條件:

調用 System.gc()

老年代區域空間不足

空間分配擔保失敗

JDK 1.7 及以前的永久代(方法區)空間不足

CMS GC處理浮動垃圾時,如果新生代空間不足,則采用空間分配擔保機制,如果老年代空間不足,則觸發Full GC

垃圾回收算法

復制算法(Copying

將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半。

注意:內存移動是必須實打實的移動(復制),不能使用指針玩。

?

專門研究表明,新生代中的對象98%“朝生夕死”的,所以一般來說回收占據10%的空間夠用了,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。

HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。

標記-清除算法(Mark-Sweep

過程:

  1. 首先標記所有需要回收的對象
  2. 統一回收被標記的對象

缺點:

1.效率問題,標記和清除效率都不高

2.標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

標記-整理算法(Mark-Compact

首先標記出所有需要回收的對象,在標記完成后,后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

垃圾回收器

分代收集

根據各個年代的特點選取不同的垃圾收集算法

新生代使用復制算法

老年代使用標記-整理或者標記-清除算法

jps -v顯示當前使用的垃圾回收器

?

在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。

而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

請記住下圖的垃圾收集器和之間的連線關系。

?

并行:垃圾收集的多線程的同時進行。

并發:垃圾收集的多線程和應用的多線程同時進行。

?

注:吞吐量=運行用戶代碼時間/(運行用戶代碼時間+ 垃圾收集時間)

垃圾收集時間= 垃圾回收頻率 * 單次垃圾回收時間

各種垃圾回收器

Serial/Serial Old

最古老的,單線程,獨占式,成熟,適合單CPU ?服務器

-XX:+UseSerialGC 新生代和老年代都用串行收集器

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old

?

ParNew

Serial基本沒區別,唯一的區別:多線程,多CPU的,停頓時間比Serial

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

除了性能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合工作。

?

Parallel ScavengeParallerGC/Parallel Old

關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。

所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

?

Concurrent Mark Sweep CMS

收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

-XX:+UseConcMarkSweepGC ,一般新生代使用ParNew,老年代的用CMS

從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“標記—清除”算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,

垃圾回收過程

整個過程分為4個步驟,包括:

l?初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓(STW -Stop the world)。

l?并發標記:GC Root 開始對堆中對象進行可達性分析,找到存活對象,它在整個回收過程中耗時最長,不需要停頓。

l?重新標記:為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓(STW)。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。

l?并發清除:不需要停頓。

?

優點:

由于整個過程中耗時最長的并發標記和并發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。

缺點:

CPU資源敏感:因為并發階段多線程占據CPU資源,如果CPU資源不足,效率會明顯降低。

浮動垃圾由于CMS并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。

由于浮動垃圾的存在,因此需要預留出一部分內存,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。

1.6的版本中老年代空間使用率閾值(92%)

如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啟用 Serial Old 來替代 CMS

會產生空間碎片:標記 - 清除算法會導致產生不連續的空間碎片

?

G1垃圾回收器

?

G1中重要的參數:

-XX:+UseG1GC ??使用G1垃圾回收器

?

內部布局改變

G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再物理隔離。

算法:標記—整理 (humongous) 和復制回收算法(survivor)

?

GC模式
Young GC

選定所有年輕代里的Region。通過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。(復制回收算法)

Mixed GC

選定所有年輕代里的Region,外加根據global concurrent marking統計得出收集收益高的若干老年代Region。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代Region

Mixed GC不是full GC,它只能回收部分老年代的Region。如果mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GCfull GC)來收集整個GC heap。所以我們可以知道,G1是不提供full GC的。

全局并發標記(global concurrent marking

?

初始標記:僅僅只是標記一下GC Roots 能直接關聯到的對象,并且修改TAMSNest Top Mark Start)的值,讓下一階段用戶程序并發運行時,能在正確可以的Region中創建對象,此階段需要停頓線程(STW),但耗時很短。

?

并發標記:GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序并發執行。

?

最終標記:為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標記階段需要把 Remembered Set Logs 的數據合并到 Remembered Set 中。這階段需要停頓線程(STW),但是可并行執行。

?

篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起并發執行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。

?

?

特點

空間整合:不會產生內存碎片

算法:標記—整理 (humongous) 和復制回收算法(survivor)

可預測的停頓:

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。

G1把內存“化整為零”的思路,理解起來似

G1 GC主要的參數

參數

含義

-XX:G1HeapRegionSize=n

設置Region大小,并非最終值

-XX:MaxGCPauseMillis

設置G1收集過程目標時間,默認值200ms,不是硬性條件

-XX:G1NewSizePercent

新生代最小值,默認值5%

-XX:G1MaxNewSizePercent

新生代最大值,默認值60%

-XX:ParallelGCThreads

STW期間,并行GC線程數

-XX:ConcGCThreads=n

并發標記階段,并行執行的線程數

-XX:InitiatingHeapOccupancyPercent

設置觸發標記周期的 Java 堆占用率閾值。默認值是45%。這里的java堆占比指的是non_young_capacity_bytes,包括old+humongous

?

垃圾回收器的重要參數(使用-XX:

參數

描述

UseSerialGC

虛擬機運行在Client模式下的默認值,打開此開關后,使用 Serial+Serial Old 的收集器組合進行內存回收

UseParNewGC

打開此開關后,使用 ParNew + Serial Old 的收集器組合進行內存回收

UseConcMarkSweepGC

打開此開關后,使用 ParNew + CMS + Serial Old 的收集器組合進行內存回收。Serial Old 收集器將作為 CMS 收集器出現 Concurrent Mode Failure 失敗后的后備收集器使用

UseParallelGC

虛擬機運行在 Server 模式下的默認值,打開此開關后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行內存回收

UseParallelOldGC

打開此開關后,使用 Parallel Scavenge + Parallel Old 的收集器組合進行內存回收

SurvivorRatio

新生代中 Eden 區域與 Survivor 區域的容量比值,默認為8,代表 Eden : Survivor = 8 : 1

PretenureSizeThreshold

直接晉升到老年代的對象大小,設置這個參數后,大于這個參數的對象將直接在老年代分配

MaxTenuringThreshold

晉升到老年代的對象年齡,每個對象在堅持過一次 Minor GC 之后,年齡就增加1,當超過這個參數值時就進入老年代

UseAdaptiveSizePolicy

動態調整 Java 堆中各個區域的大小以及進入老年代的年齡

HandlePromotionFailure

是否允許分配擔保失敗,即老年代的剩余空間不足以應付新生代的整個 Eden Survivor 區的所有對象都存活的極端情況

ParallelGCThreads

設置并行GC時進行內存回收的線程數

GCTimeRatio

GC 時間占總時間的比率,默認值為99,即允許 1% GC時間,僅在使用 Parallel Scavenge 收集器生效

MaxGCPauseMillis

設置 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效

CMSInitiatingOccupancyFraction

設置 CMS 收集器在老年代空間被使用多少后觸發垃圾收集,默認值為 68%,僅在使用 CMS 收集器時生效

UseCMSCompactAtFullCollection

設置 CMS 收集器在完成垃圾收集后是否要進行一次內存碎片整理,僅在使用 CMS 收集器時生效

CMSFullGCsBeforeCompaction

設置 CMS 收集器在進行若干次垃圾收集后再啟動一次內存碎片整理,僅在使用 CMS 收集器時生效

?

Stop The World現象

GC收集器和我們GC調優的目標就是盡可能的減少STW的時間和次數。

轉載于:https://www.cnblogs.com/Soy-technology/p/11020638.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/448719.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/448719.shtml
英文地址,請注明出處:http://en.pswp.cn/news/448719.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

提高孩子睡眠質量 學業事半功倍

睡眠如同大腦的食物。在睡眠期間,許多重要的身體機能靜靜地發生著作用。省略睡眠是有害的,如果一個嚴重缺覺的人開著車,他會臉色蒼白、喜怒無常、反應遲鈍,可能是致命的危險。缺少睡眠讓青少年很難與人相處,學業表現不…

實體類(VO,DO,DTO)的劃分

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 從領域建模中的實體劃分、項目中的實際應用情況兩個角度,對這幾個概念進行簡析。 得出的主要結論是:在項目應用…

IIS新建站點服務器,localhost能登錄但是IP訪問登錄不了。

IIS服務器新建站點之后,瀏覽頁面,服務器本地是可以登錄,但是localhost換成IP就無法訪問。其他站點IP卻可以訪問。 1.如果瀏覽直接失敗,說明端口號需要更換。 2.如果出現IP不能訪問,localhost能訪問,需要在高…

eclipse問題_Alt+/不給提示,只補充代碼問題的解決方案

今天用eclipse敲代碼的時候遇到的問題 我還以為是沖突什么的 還重新裝了軟件 最后才發現原來是快捷鍵設置的問題 解決方案: 1:打開菜單window→Preferences,然后在窗口的左側樹選擇General->Keys 2:在下圖中的5框的地方輸入“w…

領域驅動設計:淺析 VO、DTO、DO、PO 概念、區別、用處

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 本篇文章主要討論一下我們經常會用到的一些對象:VO、DTO、DO和PO。 由于不同的項目和開發人員有不同的命名習慣&#xff0c…

動腦的生活教育

心理學家華生曾經說過:“如果給我一打孩子,我可以把他們變成律師、醫師、科學家,或是強盜、土匪。”華生認為,教育孩子就如同馬戲團的馴獸師訓練野獸一樣,是“刺激”與“反應”的聯結,不需要任何的“思考”…

前端知識點回顧之重點篇——CORS

CORS(cross origin resource sharing)跨域資源共享 來源:http://www.ruanyifeng.com/blog/2016/04/cors.html 它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。 簡介 CORS需要瀏覽…

案例:隱秘而低調的內存泄露(OOM)

內存泄露測試的整個過程如下:在手機里啟動被測APP并打開DDMS。在DDMS中選中【com.example.android.hcgallery】之后單擊按鈕【show heap updates】,然后切換到標簽頁【VM Heap】,再單擊按鈕【Cause GC】。不斷操作APP,并觀察Heap。…

員工價值——如何體現自己價值,如何被自己的領導認可

到公司工作快三年了,比我后來的同事陸續得到了升職的機會,我卻原地不動,心里頗不是滋味。終于有一天,冒著被解聘的危險,我找到老板理論。 “老板,我有過遲到、早退或亂章違紀的現象嗎?”我問。 …

java: PO,VO,TO,BO,DAO,POJO 解釋

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 O/R Mapping 是 Object Relational Mapping(對象關系映射)的縮寫。通俗點講,就是將對象與關系數據庫綁…

[譯]JavaScript 究竟是如何工作的?(第一部分)

原文地址:How Does JavaScript Really Work? (Part 1)原文作者:Priyesh Patel如果你是一個 JS 開發者或者是正在學習這門語言的學生,很大概率上你會遇到雙字母詞"V8"。在這篇文章中,我將會為你簡述不同的 JS 引擎并深入…

vue實戰(9):總結二

整理前一段所做的工作內容 0.其它 vue實戰(1):準備與資料整理vue實戰(2):初始化項目、搭建底部導航路由vue實戰(3):底部導航顯示、搭建各模塊靜態頁面、添加登錄頁頁面與…

一名IT從業者的英語口語能力成長路徑

這篇文章是我最近十天口語系列文章的合輯,文章比較長,一萬五千余字。但是系統化地歸納了自己十多年的英語尤其是口語方面的學習經歷與總結思考。我不是個純粹的英語專業學生,我甚至不是任何英語相關專業的學生,但是我和英語卻有著…

解決:SpringBoot 錯誤:Caused by: org.yaml.snakeyaml.scanner.ScannerException

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 錯誤: Caused by: org.yaml.snakeyaml.scanner.ScannerException: while scanning for the next tokenfound character that cannot s…

好程序員前端分享使用JS開發簡單的音樂播放器

好程序員前端分享使用JS開發簡單的音樂播放器,最近,我們在教學生使用JavaScript,今天就帶大家開發一款簡單的音樂播放器。首先,最終效果如圖所示:首先,我們來編寫html界面index.html,代碼如下:&…

學生管理系統stuSystem函數

void stuSystem(){ struct student *head,*stu; int lookup_num; int Delete_num; int Modify_num; char ckeya; int istate0; do { system("cls"); //vc清屏函數&#xff0c;包含在#include<stdlib.h>中 printf(" 歡迎進入學生管理系統&#xff01;\n&q…

OpenCL用于計算機領域的13個經典案例

摘要&#xff1a;當使用加速器和OpenCL時&#xff0c;哪種類型的算法更加快速&#xff1f;來自弗吉尼亞理工大學的Wu Feng教授和他的團隊例舉了一份算法列表&#xff0c;分享了OpenCL常被用于計算機領域的13個經典案例。 哪種算法可以最好的映射GPU及矢量處理器呢&#xff1f;…

版本控制:集中式(SVN) vs 分布式(GIT)

Linus一直痛恨的CVS及SVN都是集中式的版本控制系統&#xff0c;而Git是分布式版本控制系統&#xff0c;集中式和分布式版本控制系統有什么區別呢&#xff1f; 先說集中式版本控制系統&#xff0c;版本庫是集中存放在中央服務器的&#xff0c;而干活的時候&#xff0c;用的都是…

Knative 核心概念介紹:Build、Serving 和 Eventing 三大核心組件

為什么80%的碼農都做不了架構師&#xff1f;>>> 作者| 阿里云智能事業群高級開發工程師 元毅 Knative 主要由 Build、Serving 和 Eventing 三大核心組件構成。Knative 正是依靠這三個核心組件&#xff0c;驅動著 Knative 這艘 Serverless 巨輪前行。下面讓我們來分…

樹莓派基金會來號召用鍵盤生物學家研究企鵝

倫敦動物學會&#xff08;Zoological Society of London&#xff09;于2014年&#xff0c;與伍茲霍爾海洋研究所和牛津大學等組織合作監控企鵝的計劃Penguin Lifelines有了新進展&#xff0c;倫敦動物學會現與其他動物保護組織合作Penguin Watch項目&#xff0c;邀請民眾在網上…