天天看點

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結

目錄

1、自動記憶體管理

2、垃圾回收

2.1、回收什麼?

2.2、何時回收?

2.2.1引用計數法

2.2.2 GC Roots:可達性分析算法

2.2.3 垃圾回收條件

2.2.4 回收時機

2.3、如何回收?

2.3.1、标記-複制

2.3.2、标記-清理

2.3.3、标記-整理

3、記憶體的分代思想

3.1、記憶體分代的問題思考

3.2、垃圾收集器

3.2.1、新生代垃圾收集器

3.2.2、老年代垃圾收集器

4、JVM常用配置參數

5、小結

前言     c++和java之間有一堵由記憶體動态配置設定和垃圾收集技術所圍成的牆。牆外面的人想進去牆裡面的人想出來。Java/JVM通過gc營造出了一種“無限記憶體”的假象,正式由于gc的存在,程式員才這麼肆無忌憚。

1、自動記憶體管理

    JVM營造了一種“無限記憶體”的假象,JVM實作隻要“盡力而為”就行,撐不住的時候就抛出OOM,至于使用什麼GC算法,什麼時候觸發GC,這些都是實作細節,規範并不管。JVM實作唯一要保證的是“目前正在使用的對象不能被幹掉了”。     JDK引入的 Epsilon GC(A No-Op Garbage Collector是一種極端的表現,根本不gc,應用盡管用,new到-Xmx用滿了就OOM。一旦java的堆被耗盡,jvm就直接關閉。設計的目的是提供一個完全消極的GC實作,配置設定有限的記憶體配置設定,最大限度降低服務垃圾收集延遲時間來提高吞吐量。     進過技術不斷的發展,記憶體的動态配置設定與記憶體回收技術已經相當成熟,一切看起來都以進入“自動化”的時代,那我們為什麼還要去了解gc和記憶體配置設定呢? 因為自動的好處是我們暫時可以不用管,省心,前提是一切運作正常。但是如果程式運作中發生了記憶體的洩漏和溢出,或者當垃圾回收成為系統達到高并發的瓶頸的時候,我們就需要對這些“自動化”的技術實施必要的監控和調節。 自動記憶體管理的核心 主要解決兩個問題: 記憶體的自動配置設定和回收 (1)給對象配置設定記憶體;

  • 對象主要配置設定在堆中的Eden區上,有時候也配置設定在老年代(大對象),之後會通過gc回收;
  • 如果經過逃逸分析,是小對象且不會被外部引用,無逃逸,則直接在棧上配置設定,随着程式占空間的銷毀而被回收,減輕了垃圾回收器的壓力;
  • 标量替換(聚合量):如果一個對象沒有被外部通路,則不會建立對象,直接建立一些标量,不能分解的變量,這樣就會随着程式方法的調用結束而結束生命。

(2)回收配置設定給對象的記憶體;

  • 根據一定的算法回收無用的java對象。

2、垃圾回收

了解垃圾回收,首先了解三個問題。

  1. 回收什麼?
  2. 何時回收?
  3. 如何回收?

2.1、回收什麼?

    主要回收無用的對象,或者“已死”的對象。 java記憶體運作時區域的各個部分,其中程式計數器/虛拟機棧/本地方法棧三個都是随着線程而生,随線程而滅;棧中的每一個棧幀配置設定多少記憶體基本上是在類結構确定下來的時候就大體已經确定了【除了JIT編譯器進行的一些優化】,是以随着方法的結束和線程的結束,記憶體自然就跟着回收了。     但是堆和方法區不一樣,java所建立的對象基本上都存儲在堆區,雖然“永久代”方法區的垃圾回收效率比較低,但是“永久代”對于廢棄的常量和無用的類,也會進行相應的回收,尤其大量使用反射和動态代理CGlib等場景都需要虛拟機具備類解除安裝功能,保證無用的資源可以被回收,不會發生記憶體洩漏。     要回收就要确定什麼樣的對象是垃圾,确定該對象是否“存活”或者“死去”;“死去”的對像就是不可能再被任何途徑使用的對象,這些對象就可以被回收。例如堆中一個對象沒有任何指針對其引用(循環引用除外),它就是垃圾對象,方法區中廢棄的常量和類等。

2.2、何時回收?

2.2.1引用計數法

    當該對象沒有任何其他對象和程式的引用,即計數為0就可以回收。

  • 優點:實作簡單,效率也很高;
  • 缺點:無法解決循環引用的問題;

2.2.2 GC Roots:可達性分析算法

    Tracing gc的基本思路是:進過一系列名為“gc-root"的對像作為起始點,從這些節點向下走的路徑稱之為引用鍊,當一個對象到gc-root沒有任何引用鍊相連的時候,則證明此對象是不可達的,不可達的對象即可回收。   GC Roots,或者說”tracing GC”的根引用集合,就是 一組必須存活的對對象“live set”的引用集合。   思路:給定一個集合的引用作為根出發,通過引用關系周遊對象圖,能被周遊到(可到達的)對象被判定為存活,其餘對象(也就是沒有周遊到的)就自然被判定為死亡。 因為GC的管理隻針對java堆和方法區,棧區/本地方法區/程式計數器屬于線程私有,不被GC管理,因而選擇這些區域的對象作為gc root, 就可以找到存活的對象,至少保證正在使用的對象不被幹掉,進而避免被gc回收。     在java語言裡,可以作為gc-roots的對象包括:正在引用的對象--可以通過mat工具檢視。

  • 虛拟機棧(棧幀中的本地變量表);所有java線程目前活躍的棧幀裡指向GC堆裡的對象的引用;換句話:目前所有正在被調用的方法的引用類型的參數/局部變量/臨時值;因為局部變量屬于棧幀,棧幀屬于棧,一個虛拟機棧對應一個線程,說明這個變量正在被線程調用,通過它就可以找到那些不是垃圾;
  • 方法區中的靜态屬性引用的對象; 靜态資料結構裡指向gc堆裡的對象引用;
  • 方法區中常量引用的對象;
  • 本地方法棧中JNI,Native方法中引用的對象;
  • Classloader;
  • java程序/線程;

注意:traceing gc 的本質是通過找出所有活對象來把其餘空間認定為“無用”,而不是找出所有死掉的對象并回收它們的占用的記憶體。 gc-root的工作流程:  

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結

2.2.3 垃圾回收條件

思考: 不可達的對象一定會被回收嗎 ? 在搜尋算法中的不可達對象,也并非是非死不可的,可以進行自我拯救。真正宣告一個對象的死亡需要經以下兩個條件:

  • 1. 沒有與GC-root相連的引用鍊;
  • 2. 該對象是否有必要執行finalize方法;【判斷對象是否覆寫過finalize方法,是否已經調用過一次】

   finalize()方法是對象逃過死亡的最後一次機會,意思就是對象在這個階段還可以被複活。但是finalize方法隻會被調用一次,如果有必要執行finalize方法,且該對象不想被回收,則隻需要在F-Queue隊列中重新引用上任何一個對象即可。如果此時對象任然沒有任何的引用,那麼就會被稍後GC正式的回收。  

2.2.4 回收時機

堆區垃圾回收:執行個體對象不可達及finalize方法調用之後仍沒有引用;

  • 引用計數:循環引用問題;循環引用的對象是否挂在根上,挂在根上可達;否則不可達回收;
  • GC-Root算法 + finalize();
  • 強/軟/弱/虛引用的判斷;

方法區垃圾回收 廢棄的常量和 無用的類

  • (1)該類所有的執行個體都已經被回收,即Java堆中不存在該類的任何執行個體;
  • (2)加載該類的ClassLoader已經被回收,因為類加載器是可以作為gc-root的;
  • (3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

方法區的垃圾回收注意點:

  • 1. 永久代的記憶體回收效率比較低下,回收的性能消耗高于回收的記憶體的收益,成本效益低,是以一般可以不考慮使用。
  • 2. 可以使用參數設定,在反射和動态代理-CGLIB - 位元組碼架構asm使用頻繁的場景建議打開永久代的記憶體回收政策;

2.3、如何回收?

    已經能夠确定一個對象是垃圾之後,接下來就是考慮如何回收?如何回收就需要良好的垃圾回收算法,這個也是能讓程式員解脫記憶體管理,專心編碼的關鍵所在。大體主要分為3類垃圾回收算法。

2.3.1、标記-複制

主要思想是:将可用記憶體容量劃分為大小相等的兩塊,每次隻使用其中的一塊,有50%的空間浪費,類似Eden區的from和to兩塊區域。當其中一塊用完之後,就将還存活的對象複制到另一塊後備空間裡,然後在把使用過的記憶體空間一次清理釋放掉。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 實作簡單,效率高; 不會存在記憶體碎片;
缺點 就是需要2倍的記憶體來管理; 複制需要時間開銷,适用于少量對象的場景;
使用場景 新生代垃圾收集器基本都采用标記複制;

   我們可以從jvm的新生代設定的參數看到:-XX:SurvivorRatio=8,表示新生代中Eden區域和Survivor區域的容量比值,代表Eden:Survivor=8:1。一個Survior占新生代的1/10,表達的意思是 Survivor中的from區間和to區間與Edon的比例是1:1:8,from和to的空間也就是采用了标記複制的算法,是以空間大小一樣,且程式運作期間浪費年輕代的1/10。

2.3.2、标記-清理

    标記清除算法分為“标記”和“清除”兩個階段:首先标記出需要回收的對象,标記完成之後統一清除對象。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 清除效率高;
缺點 存在記憶體碎片;
使用場景 老年代垃圾收集器CMS采用“标記-清理”但是可以通過設定靈活改變;

2.3.3、标記-整理

    與“标記-清理”不同的是标記完存活對象,清理完對象後,将所有存活的對象都向一端移動,并更新引用其對象的指針,進行了記憶體的複制整理。因為要移動對象,是以它的效率要比 “标記-清理”效率低,但是不會産生記憶體碎片。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 沒有碎片;
缺點 複制移動資源需要時間的開銷; 指針的改變增加了程式的消耗;
使用場景 老年代的串行收集器和并行收集器都采用“标記-整理”的算法;

3、記憶體的分代思想

   由于java大部分對象都具有 “朝生夕死”的特性,是以對于存活時間長的對象,減少被gc的次數可以避免不必要的開銷。這樣我們就把記憶體分成新生代和老年代,新生代存放剛建立的和存活時間比較短的對象,老年代存放存活時間比較長的對象。這樣每次僅僅清理年輕代,也就是Minor GC,非常頻繁,但是速度較快;老年代的Major GC。僅在必要時時再做清理可以極大的提高GC效率,節省GC時間。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結

分代與回收算法的使用

  • Young區: 複制算法,對象在被配置設定之後,“朝生夕死”,可能生命周期比較短,Young區複制效率比較高;
  • Old區: 标記清除或标記整理,Old區對象存活時間比較長,且對象多,複制來複制去沒必要,不如做個标記清理,效率高;

3.1、記憶體分代的問題思考

問題一: 為什麼會有老年代和青年代?

  • (1)提高垃圾回收效率,減少掃描的空間大小,分治思想;
  • (2)減少停頓時間,分代後掃描的區域小;
  • (3)停頓時間小提高吞吐量;
  • (4)掃描更加油針對性的young區,讓對象早點被回收,減少old-gc的頻率,接受一定程度的young-gc;

前提:對象大部分朝生夕死,對象都是先進入年輕代後進入老年代;此外java大部分用來做web開發: 例如:web應用,order訂單;支付完線程就結束了,是以隻有少部分對象存在,大部分都是垃圾對象; 問題二: 為什麼會有survivor from和to區域?例如NewRation=8:1:1

  • (1)減小或減緩垃圾對象去老年代的機率;讓朝生夕死的對象盡快回收,不讓其進入老年代,如果進入老年代,old區很快被填滿,導緻觸發full-gc頻率增加;
  • (2)提高效率,避免掃描整個堆空間;增大old區的大小會導緻一次full-gc的時間增長;
  • (3)from-to複制整理算法雖然有一半空間的浪費,但是提升了整個垃圾回收和空間的利用效率;

問題三: 何時進入老年代?

  • 虛拟機采用了分代收集的思想來管理記憶體,虛拟機給每個對象定義了一個對象年齡,沒熬過一次MinorGC,年齡增加一歲,預設為15,就會被晉升到老年代,也可以通過參數:-XX:MaxThenuringThreshold來設定它的值。
  • 當suvivor區相同年齡的對象到達其記憶體空間的一半的時候,就直接進入老年代。變相的成為了大對象;
  • 申請的大對象,年輕代的空間不夠,直接進入老年代;

問題四: 老年代的擔保兜底政策:

  • 主要用曆史晉升的對象的平均值大小來判斷老年代剩餘的空間是否滿足接下來可能面臨的新生代對象的晉升;
  • form和to的空間不夠;
  • 擔保機制,直接在老年代配置設定;

tips-大對象:可以通過-XX:PretenureSizeThreshold參數:令大于這個值的對象直接在老年代配置設定,這樣做的目的是避免年輕代發生大批量的拷貝。  

3.2、垃圾收集器

(1). 新生代的收集器包括:

  • Serial  / PraNew / Parallel Scavenge;

(2). 老年代的收集器包括:

  • Serial Old  / Parallel Old / CMS;

(3). 回收整個Java堆(新生代和老年代)

  • G1收集器;

幾種收集器及它們之間的組合關系如下:

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結

3.2.1、新生代垃圾收集器

(1). Serial收集器 - 複制算法 Serial收集器是最基本、發展曆史最悠久的收集器,曾經(在JDK1.3.1之前)是虛拟機新生代收集的唯一選擇,它是一種單線程收集器,不僅僅意味着它隻會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是其在進行垃圾收集的時候需要暫停其他線。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 對與單cpu,實作簡單高效;
缺點 單線程工作不能充分利用多核cpu性能;之間會産生STW,給使用者帶來不良體驗;
使用場景 新生代垃圾收集器,虛拟機運作在Client模式下的預設新生代收集器
使用參數 串行收集器,分為年輕代 Serial 和老年代 Serial Old 收集器。 1. -XX:+UseSerialGC 這個參數就是可以指定使用新生代串行收集器和老年代串行收集器; 2. -XX:+UseParNewGC 新生代使用 ParNew 回收器,老年代使用串行收集器; 3. -XX:+UseParallelGC 新生代私用 ParallelGC 回收器,老年代使用串行收集器; 而 Serial 收集器出現的日志為 DefNew . 注:【“+” 号的意思是ture,開啟,反之,如果是 “-”号,則是關閉】

(2). ParNew 收集器 - 複制算法 ParNew收集器其實就是serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行為與Serial收集器一樣。 單CPU,ParNew不會比Serial收集效果更好,但是随着CPU的數量增加,ParNew則在GC時對系統資源有效利用更好;另外,除了serial收集器外,隻有ParNew收集器可以和CMS收集器愉快的合作;

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 在多核cpu的情況下,gc效率更高;
缺點 單核的效率 <= Serial; 産生STW;
使用場景 新生代垃圾收集器,在Server模式下,ParNew收集器是一個非常重要的收集器,且能與CMS垃圾收集配合使用。
參數使用 行收集器是 Serial 的多線程版本,在 CPU 并行能力強大的計算機上有很大優勢。 其中: 1. -XX:+UseParNewGC 強制設定新生代使用 ParNew 收集器,老年代使用串行收集器。 2. -XX:+UseConcMarkSweepGC:  會預設使用ParNew作為新生代收集器,老年代使用 CMS。 3. -XX:ParallelGCThreads={value} 這個參數是指定并行 GC 線程的數量,一般最好和 CPU 核心數量相當。預設情況下,當 CPU 數量小于8, ParallelGCThreads 的值等于 CPU 數量,當 CPU 數量大于 8 時,則使用公式:3+((5*CPU)/ 8);同時這個參數隻要是并行 GC 都可以使用,不隻是 ParNew。 而ParNew的GC日志則表吸納出ParNew。

(3). Parallel Scavenge并行回收收集器 - 複制算法     Parallel Scavenge收集器也是一個新生代并行多線程收集器。Parallel Scavenge收集器的目标則是: 追求可控制高吞吐量+高效利用CPU 。吞吐量=程式運作時間/(程式運作時間 + 垃圾收集時間),虛拟機總共運作了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%。

垃圾回收算法1、自動記憶體管理2、垃圾回收3、記憶體的分代思想4、JVM常用配置參數5、小結
優點 吞吐量可控,讓使用者代碼獲得更長的運作時間;并行收集高效利用多核cpu;
缺點 (1) 标記和清理産生2次STW; (2) 不能和cms一起愉快的合作,架構不同;
使用場景 該 收集器是 Java 8 的預設新生代收集器,因為它能夠根據系統目前狀态給出吞吐量最高的GC 配置。是以,在一些手工調優複雜的場合或者對實時性要求不高的場合,可以使用該處理器。停頓時間越短就越适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可用高效率地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。
參數使用 1. -XX:MaxGCPauseMillis 設定最大垃圾收集停頓時間,他的值是一個大于0的整數。ParallelGC 工作時,會調整 Java 堆大小或者其他的一些參數,盡可能的把停頓時間控制在 MaxGCPauseMillis 以内。 注意:此值不是越小越好,原因是GC停頓時間縮短是以犧牲吞吐量和調整新生代空間來換取的,這将會到值頻繁 GC ,雖然系統停頓時間小了,但總吞吐量下降了。 2. -XX: GCTimeRatio 設定吞吐量大小,他的值是一個0 到100之間的整數,假設 GCTimeRatio 的值是 n ,那麼系統将花費不超過 1/(1+n) 的時間用于垃圾收集,預設 n 是99,即不超過1% 的時間用于垃圾收集。 3. -XX:UseAdaptiveSizePolicy: 打開自适應政策,與ParNew的差別。在這種模式下,新生代的大小,eden 和 Survivor 的比例,晉升老年代的對象年齡等參數會被自動調整。以達到堆大小,吞吐量,停頓時間的平衡點。 注: 參數1-2其實是沖突的,吞吐量和停頓時間是反比的,需要找到一個平衡點。如果是調參場景比較複雜的情況下,可采用自适應政策。

3.2.2、老年代垃圾收集器

(1). Serial Old收集器 - 标記整理算法     Serial Old是Serial收集器的老年代垃圾回收,它同樣是一個單線程(串行)收集器,不同于年輕代的Serial算法的是使用标記整理算法,工作流程和年輕代一樣。

優點 單線程工作效率高,實作簡單;
缺點 産生STW;多核CPU不能充分利用其性能
使用場景 在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用; 作為 CMS收集器的後備預案,在并發收集發生Concurrent Mode Failure時使用;
參數使用 參考Serial收集器的使用

(2). Parallel Old收集器 - 标記整理算法 Parallel Old是Parallel Scavenge收集器的老年代版本,也是一種關注系統吞吐量的老年代垃圾回收機制,使用多線程和 “标記-整理”算法。之前一直是 Parallel Scavenge + old serial的組合,由于無法真正的利用多核cpu的性能,是以其吞吐量反而不一定有PreNew+CMS組合給力,現在終于才有了新生代和老年代都關注的吞吐量的新組合: Parallel Scavenge + Parallel Old。

優點 高效利用cpu;獲得較大的吞吐量;可以和Parallel Scavenge一起愉快的合作;
使用場景 老年代垃圾回收, 注重吞吐量以及CPU資源敏感的場景 , “吞吐量優先”  收集器的名副其實 組合: Parallel Scavenge +  Parallel Old 組合。
參數使用 -XX:+ UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。該參數可以啟用 ParallelOldGC。 -XX: ParallelGCGThreads :同時可以指定該參數設定并行線程數量。

4、JVM常用配置參數

配置參數 功能
-Xms 初始堆大小。如:-Xms4g
-Xmx 最大堆大小。如:-Xmx4g
-Xmn 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%;
-Xss JDK1.5+ 每個線程堆棧大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的;
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代占整個堆空間的1/3,老年代占2/3;
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 占新生代空間的 8/10,另外兩個 Survivor 各占 1/10;
-XX:PermSize 永久代(方法區)的初始大小;
-XX:MaxPermSize 永久代(方法區)的最大值;
開啟各種GC算法的參數 1)串行
  • -XX:+UseSerialGC
  • -XX:+UseSerialOldGC
(2)并行(吞吐量優先):
  • -XX:+UseParallelGC
  • -XX:+UseParallelOldGC
(3)并發收集器(響應時間優先)
  • -XX:+UseConcMarkSweepGC
  • -XX:+UseG1GC
gc日志列印參數 1. -XX:+PrintGCDateStamps 列印 GC 日志時間戳。 2. -XX:+PrintGCDetails 列印 GC 詳情。 3. -XX:+PrintGCTimeStamps: 列印此次垃圾回收距離jvm開始運作的所耗時間。 4. -Xloggc: 将垃圾回收資訊輸出到指定檔案 5. -verbose:gc 列印 GC 日志 6. -XX:+PrintGCApplicationStopedTime 檢視 gc 造成的應用暫停時間 7. XX:+PrintTenuringDistribution, 對象晉升的日志 8. -XX:+HeapDumpOnOutOfMemoryError 記憶體溢出時輸出 dump 檔案。

5、小結

  • 1、年輕代垃圾回收器-複制算法
    • serial:串性收集器 - 複制算法
    • PraNew:多線程收集器- serial的多線程版本 - 複制算法
    • Parallel Scavenge: 并行收集器 - 複制算法--開始關注吞吐量
      • Parallel Scavenge追求可控制的吞吐量 + 高效利用cpu
        • -XX:MaxGCPauseMillis 設定最大垃圾收集停頓時間
        • XX:GCTimeRatio 設定吞吐量大小,預設 n 是99,即不超過1% 的時間用于垃圾收集。
  • 2、老年代垃圾回收器-标記清理
  • serial-old:串性收集器 - 标記整理算法
    • cms的後備垃圾收集器,當出現concurrent mode failure時使用;
  • Parallel-old: - 标記整理算法,和 Parallel Scavenge組合,
    • 之前都是Parallel Scavenge + old Serial組合;
    • PraNew+ CMS組合(後備:serial-old) ;
    • 現在有了:Parallel-Scavenge + Parallel-Old,真正關于吞吐量的一個垃圾收集組合;
    • -XX:ParallelGCGThreads 同時可以指定該參數設定并行線程數量。
  • CMS(Concurrent Mark Sweep):标記清除算法 —開始關注較少的停頓時間-後面詳解
  • G1:關注停頓時間可控;

    水滴石穿,積少成多。學習筆記,内容簡單,用于複習,梳理鞏固。   參考資料: 《深入了解jvm虛拟機》 參考資料: https://docs.oracle.com/en/java/javase/11/gctuning/available-collectors.html#GUID-F215A508-9E58-40B4-90A5-74E29BF3BD3C