通过HTML演示JVM的垃圾回收-新生代与老年代 - 详解

当内存溢出频发,或垃圾回收成为并发瓶颈时,理解 JVM 的内存布局和 GC 行为,是性能调优的关键。代码演示在目录的HTML演示中

目录

新生代(Young Generation)

Eden 区

Survivor 区

为什么需要 Survivor 区?

老年代(Old Generation)

GC 类型

HTML展示

新老年代转换演示

JVM垃圾回收算法演示


从垃圾回收的角度:

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)

所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

JDK 8 版本之后 PermGen(永久代/方法区) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

新生代(Young Generation)

新生代分为三部分:

  • Eden 区:新对象几乎都首先分配在这里。
  • Survivor 区:包含两个等大小的区域 S0 和 S1,通常比例为 Eden : S0 : S1 = 8 : 1 : 1(可通过 -XX:SurvivorRatio 调整)。

Eden 区

  • 新对象优先分配在 Eden。
  • 大对象(如大数组)可能直接进入老年代,避免频繁复制。
  • Eden 空间不足时,触发 Minor GC(也称 Young GC),回收不可达对象。

Survivor 区

  • Minor GC 时,Eden 中存活的对象被复制到一个 Survivor 区(如 S0)。
  • 下一次 Minor GC 时,存活对象从 Eden + S0 复制到 S1,S0 被清空。
  • 两个 Survivor 区交替使用,始终有一个为空,这种机制称为 复制算法
  • 每次 Minor GC 后,存活对象的“年龄”加 1;达到阈值(默认 15,可通过 -XX:MaxTenuringThreshold 设置)后晋升到老年代。
为什么需要 Survivor 区?

若没有 Survivor 区,存活对象会直接进入老年代,导致:

  • 老年代快速填满,频繁触发 Full GC;
  • 无法区分短期与长期存活对象。

Survivor 区起到缓冲和筛选作用,只让真正长期存活的对象进入老年代,从而减少 Full GC 频率。


老年代(Old Generation)

老年代存放生命周期较长的对象,通常占堆内存的 2/3(默认新生代 : 老年代 = 1 : 2)。

以下对象会进入老年代:

  1. 长期存活对象:在新生代中经历多次 Minor GC 仍存活,且年龄达到阈值。
  2. 大对象:为避免复制开销,直接分配到老年代(部分 GC 支持)。
  3. Survivor 空间不足时的担保晋升:Minor GC 时若 Survivor 无法容纳所有存活对象,部分对象会提前晋升。
  4. 动态年龄判定:HotSpot 会判断,若某年龄以上对象总和超过 Survivor 一半,则直接晋升。

GC 类型

JVM 的垃圾回收按范围分为两类:

  • 部分收集(Partial GC)

    • Minor GC / Young GC:仅回收新生代。
    • Major GC / Old GC:仅回收老年代(注意:某些语境中 “Major GC” 也指 Full GC)。
    • Mixed GC:G1 GC 特有,回收新生代 + 部分老年代。
  • 整堆收集(Full GC):回收整个 Java 堆和方法区,通常代价高、STW 时间长,应尽量避免。

HTML展示

新老年代转换演示

可直接复制代码丢到HTML中查看




  
  
  JVM 垃圾回收机制演示
  


JVM 垃圾回收机制演示:新生代与老年代

Eden S0 S1 老年代 死亡对象

Eden

Survivor S0

Survivor S1

老年代

准备开始... 点击“下一步”查看 JVM 内存变化

新生代(Young Generation)结构

新生代通常被划分为三个部分:

  • Eden 区:新对象首先分配在此。
  • Survivor 区:分为 S0 和 S1,比例通常为 Eden:S0:S1 = 8:1:1。

各区域作用

  • Eden:对象创建地;满时触发 Minor GC。
  • Survivor:Minor GC 时,存活对象在 S0/S1 之间“复制”;每经历一次 GC,年龄 +1。
  • 老年代:存放长期存活对象(年龄 ≥ 阈值,默认 15)或大对象。

❓ 为什么需要 Survivor 区?

  • 避免短期存活对象直接进入老年代,防止老年代快速填满。
  • 通过“多次筛选”,只让真正长期存活的对象晋升,减少 Full GC 频率。

⚙️ 晋升老年代的条件

  • 对象年龄达到阈值(本演示设为 3)
  • Survivor 空间不足(分配担保)
  • 动态年龄判定:某年龄以上对象总和 > Survivor 一半
  • 大对象(如大数组)可能直接进入老年代
<script> // 初始化状态 let eden = []; let s0 = []; let s1 = []; let old = []; let step = 0; const AGE_THRESHOLD = 3; function render() { const regions = { eden, s0, s1, old }; for (const [name, list] of Object.entries(regions)) { const el = document.getElementById(name); if (el) { el.innerHTML = list.map(obj => `
${obj.name}
` ).join(''); } } } function log(msg) { const logEl = document.getElementById('log'); if (logEl) logEl.innerText = msg; } function nextStep() { step++; try { switch (step) { case 1: eden = [{name:'A',age:0,alive:true}, {name:'B',age:0,alive:true}, {name:'C',age:0,alive:true}]; log("✅ 创建 A、B、C → 放入 Eden(新对象首先进入 Eden)"); break; case 2: if (eden.length >= 2) { eden[0].alive = false; eden[1].alive = false; } log("⏳ A、B 无引用(死亡),C 仍被引用(存活)"); break; case 3: s0 = eden.filter(o => o.alive).map(o => ({...o, age: o.age + 1})); eden = []; log("♻️ Minor GC:回收 A、B;C(age=1) → S0;清空 Eden"); break; case 4: eden = [{name:'D',age:0,alive:true}, {name:'E',age:0,alive:true}, {name:'F',age:0,alive:true}]; log("✅ 创建 D、E、F → 放入 Eden"); break; case 5: if (eden.length >= 1) eden[0].alive = false; log("⏳ D 死亡;E、F 存活;C(age=1) 在 S0 仍存活"); break; case 6: const toS1 = [...s0, ...eden.filter(o => o.alive)].map(o => ({...o, age: o.age + 1})); s1 = toS1; s0 = []; eden = []; log("♻️ Minor GC:C(age=2)、E、F → S1;清空 Eden 和 S0"); break; case 7: eden = [{name:'G',age:0,alive:true}, {name:'H',age:0,alive:true}]; log("✅ 创建 G、H → 放入 Eden"); break; case 8: if (eden.length >= 1) eden[0].alive = false; log("⏳ G 死亡;H 存活;C(age=2)、E、F 在 S1 仍存活"); break; case 9: const candidates = [...s1, ...eden.filter(o => o.alive)].map(o => ({...o, age: o.age + 1})); const promote = candidates.filter(o => o.age >= AGE_THRESHOLD); const stay = candidates.filter(o => o.age < AGE_THRESHOLD); old.push(...promote); s0 = stay; s1 = []; eden = []; const promotedNames = promote.map(o => o.name).join(', '); const msg = promote.length > 0 ? `♻️ Minor GC:H、E、F → S0;${promotedNames}(age=${AGE_THRESHOLD}) 晋升老年代!` : "♻️ Minor GC:所有存活对象 → S0"; log(msg); break; case 10: log(" 演示结束!C 已进入老年代"); document.getElementById('nextBtn').disabled = true; return; default: if (step > 10) return; } render(); } catch (e) { console.error("演示步骤出错:", e); log("⚠️ 演示异常,请刷新页面重试。"); } } // 绑定按钮事件(避免内联 onclick) document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('nextBtn'); if (btn) { btn.addEventListener('click', nextStep); } render(); }); </script>

JVM垃圾回收算法演示

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

代码




    
    
    JVM垃圾回收算法详细动画演示
    


JVM垃圾回收算法动画演示

标记-清除算法 (Mark-and-Sweep)

准备开始...

特点: 分为标记和清除两个阶段。标记所有存活对象,然后清除未标记的对象。

局限性: 效率问题(标记和清除过程较慢);空间问题(产生内存碎片)。

复制算法 (Copying)

准备开始...

特点: 将内存分为两块,每次只使用一块。GC时将存活对象复制到另一块,清理已用空间。

局限性: 可用内存减半;不适合老年代(存活对象多时复制开销大)。

标记-整理算法 (Mark-and-Compact)

准备开始...

特点: 在标记后,将所有存活对象向一端移动,然后清理边界外的内存。

局限性: 效率较低(多了整理步骤),适用于老年代这种GC频率低的场景。

分代收集算法 (Generational Collection)

准备开始...

特点: 根据对象存活周期将堆分为新生代和老年代,分别采用合适的算法。

原因: 新生代对象“朝生夕死”,适合复制算法;老年代对象存活率高,适合标记-清除/整理。

<script> // --- Common Utilities --- function createElement(tag, className, styles = {}) { const el = document.createElement(tag); if (className) el.className = className; Object.assign(el.style, styles); return el; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // --- Mark-and-Sweep Algorithm (Detailed) --- async function startMarkSweepDetailed() { const area = document.getElementById('mark-sweep-animation'); const info = document.getElementById('mark-sweep-phase'); const button = event.target; button.disabled = true; area.innerHTML = ''; const objects = [ {id: 'A', marked: false, alive: true}, {id: 'B', marked: false, alive: false}, {id: 'C', marked: false, alive: true}, {id: 'D', marked: false, alive: false}, {id: 'E', marked: false, alive: true}, {id: 'F', marked: false, alive: false} ]; const objectElements = []; const totalWidth = area.clientWidth - 20; const objectWidth = totalWidth / objects.length - 10; objects.forEach((obj, i) => { const el = createElement('div', `memory-block object-${obj.alive ? 'alive' : 'dead'}`, { width: `${objectWidth}px`, left: `${i * (objectWidth + 10) + 10}px` }); el.innerText = obj.id; area.appendChild(el); objectElements.push({data: obj, element: el}); }); info.innerText = "步骤 1: 对象创建完成,B, D, F 无引用变为垃圾。"; await sleep(1500); info.innerText = "步骤 2: GC启动,进入标记阶段。"; await sleep(1000); info.innerText = "步骤 3: 从GC Roots开始遍历,标记存活对象..."; await sleep(1000); for (let item of objectElements) { if (item.data.alive) { item.data.marked = true; item.element.className = 'memory-block object-marked'; await sleep(600); } } info.innerText = "步骤 4: 标记阶段完成。"; await sleep(1000); info.innerText = "步骤 5: 进入清除阶段,回收未标记对象..."; await sleep(1000); for (let item of objectElements) { if (!item.data.marked) { item.element.style.opacity = '0.3'; item.element.style.transform = 'scale(0.8)'; await sleep(600); } } info.innerText = "步骤 6: 清除完成,产生内存碎片。标记-清除算法演示结束。"; button.disabled = false; } // --- Copying Algorithm (Detailed) --- async function startCopyingDetailed() { const area = document.getElementById('copying-animation'); const info = document.getElementById('copying-phase'); const button = event.target; button.disabled = true; area.innerHTML = ''; const fromSpace = createElement('div', 'memory-region region-from', { left: '10px', width: 'calc(50% - 20px)' }); fromSpace.innerText = 'From Space'; const toSpace = createElement('div', 'memory-region region-to', { right: '10px', width: 'calc(50% - 20px)' }); toSpace.innerText = 'To Space'; area.appendChild(fromSpace); area.appendChild(toSpace); const objects = [ {id: 'X', alive: true}, {id: 'Y', alive: false}, {id: 'Z', alive: true}, {id: 'W', alive: true} ]; const objectElements = []; const spaceWidth = (area.clientWidth / 2) - 30; const objectWidth = spaceWidth / objects.length - 10; objects.forEach((obj, i) => { const el = createElement('div', `memory-block object-${obj.alive ? 'alive' : 'dead'}`, { width: `${objectWidth}px`, left: `${i * (objectWidth + 10) + 20}px`, top: '85px' }); el.innerText = obj.id; fromSpace.appendChild(el); objectElements.push({data: obj, element: el}); }); info.innerText = "步骤 1: Eden区 (From Space) 对象分配完成,Y死亡。"; await sleep(1500); info.innerText = "步骤 2: Minor GC 触发,准备进行复制回收。"; await sleep(1000); info.innerText = "步骤 3: 遍历From Space,识别存活对象..."; await sleep(1000); info.innerText = "步骤 4: 将存活对象(X, Z, W)依次复制到To Space..."; await sleep(1000); let toIndex = 0; for (let item of objectElements) { if (item.data.alive) { const newEl = item.element.cloneNode(true); newEl.className = 'memory-block object-copied'; newEl.style.left = `${toIndex * (objectWidth + 10) + 20}px`; newEl.style.top = '85px'; toSpace.appendChild(newEl); toIndex++; await sleep(800); } } info.innerText = "步骤 5: 复制完成,清空整个From Space..."; await sleep(1000); fromSpace.innerHTML = 'From Space
(已清空)'; fromSpace.style.backgroundColor = '#fdebd0'; info.innerText = "步骤 6: From和To角色互换。复制算法演示结束。"; button.disabled = false; } // --- Mark-and-Compact Algorithm (Detailed) --- async function startCompactingDetailed() { const area = document.getElementById('compact-animation'); const info = document.getElementById('compact-phase'); const button = event.target; button.disabled = true; area.innerHTML = ''; const objects = [ {id: 'M', marked: false, alive: true}, {id: 'N', marked: false, alive: false}, {id: 'O', marked: false, alive: true}, {id: 'P', marked: false, alive: false}, {id: 'Q', marked: false, alive: true}, {id: 'R', marked: false, alive: false} ]; const objectElements = []; const totalWidth = area.clientWidth - 20; const objectWidth = totalWidth / objects.length - 10; objects.forEach((obj, i) => { const el = createElement('div', `memory-block object-${obj.alive ? 'alive' : 'dead'}`, { width: `${objectWidth}px`, left: `${i * (objectWidth + 10) + 10}px` }); el.innerText = obj.id; area.appendChild(el); objectElements.push({data: obj, element: el}); }); info.innerText = "步骤 1: 老年代内存状态,N, P, R 为垃圾对象。"; await sleep(1500); info.innerText = "步骤 2: Full GC 启动,进入标记阶段。"; await sleep(1000); info.innerText = "步骤 3: 标记所有存活对象..."; await sleep(1000); for (let item of objectElements) { if (item.data.alive) { item.data.marked = true; item.element.className = 'memory-block object-marked'; await sleep(600); } } info.innerText = "步骤 4: 标记完成。"; await sleep(1000); info.innerText = "步骤 5: 进入整理阶段,将存活对象向内存一端移动..."; await sleep(1000); let compactIndex = 0; for (let item of objectElements) { if (item.data.marked) { item.element.className = 'memory-block object-moved'; const targetLeft = compactIndex * (objectWidth + 10) + 10; const currentLeft = parseInt(item.element.style.left); const delta = targetLeft - currentLeft; item.element.style.transform = `translateX(${delta}px)`; compactIndex++; await sleep(700); } } info.innerText = "步骤 6: 整理完成,清理边界外的内存..."; await sleep(1000); for (let item of objectElements) { if (!item.data.marked) { item.element.style.opacity = '0.3'; item.element.style.transform += ' scale(0.8)'; await sleep(600); } } info.innerText = "步骤 7: 内存整理完毕,无碎片。演示结束。"; button.disabled = false; } // --- Generational Collection Algorithm (Detailed) --- async function startGenerationalDetailed() { const area = document.getElementById('generational-animation'); const info = document.getElementById('generational-phase'); const button = event.target; button.disabled = true; area.innerHTML = ''; const youngGen = createElement('div', 'memory-region region-young', { top: '10px', left: '10px', width: 'calc(100% - 20px)' }); youngGen.innerText = '新生代 (Young Generation)'; const oldGen = createElement('div', 'memory-region region-old', { bottom: '10px', left: '10px', width: 'calc(100% - 20px)' }); oldGen.innerText = '老年代 (Old Generation)'; area.appendChild(youngGen); area.appendChild(oldGen); info.innerText = "步骤 1: JVM内存分为新生代和老年代。"; await sleep(1500); info.innerText = "步骤 2: 新生代对象'朝生夕死',采用复制算法。"; await sleep(1000); youngGen.style.boxShadow = 'inset 0 0 10px #3498db'; await sleep(1000); youngGen.style.boxShadow = 'none'; await sleep(500); info.innerText = "步骤 3: 老年代对象存活率高,采用标记-清除/整理。"; await sleep(1000); oldGen.style.boxShadow = 'inset 0 0 10px #8e44ad'; await sleep(1000); oldGen.style.boxShadow = 'none'; await sleep(500); info.innerText = "步骤 4: 对象在新生代经历多次GC后晋升到老年代。"; await sleep(1000); const obj = createElement('div', 'memory-block object-alive', { width: '30px', height: '20px', top: '25px', left: '45%', fontSize: '12px' }); obj.innerText = 'Obj'; youngGen.appendChild(obj); await sleep(1000); obj.style.transition = 'all 1s ease'; obj.style.top = '155px'; obj.style.backgroundColor = '#8e44ad'; await sleep(1200); info.innerText = "步骤 5: 不同代使用最适合的算法,这就是分代收集。"; await sleep(1500); info.innerText = "分代收集算法演示完成。"; button.disabled = false; } </script>

posted on 2025-10-21 17:28  slgkaifa  阅读(4)  评论(0)    收藏  举报

导航