通过HTML演示JVM的垃圾回收-新生代与老年代 - 详解
当内存溢出频发,或垃圾回收成为并发瓶颈时,理解 JVM 的内存布局和 GC 行为,是性能调优的关键。代码演示在目录的HTML演示中
目录
从垃圾回收的角度:
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)
所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(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)。
以下对象会进入老年代:
- 长期存活对象:在新生代中经历多次 Minor GC 仍存活,且年龄达到阈值。
- 大对象:为避免复制开销,直接分配到老年代(部分 GC 支持)。
- Survivor 空间不足时的担保晋升:Minor GC 时若 Survivor 无法容纳所有存活对象,部分对象会提前晋升。
- 动态年龄判定: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>