发布于2026-05-21 阅读(0)
扫一扫,手机访问
一启动Ja va虚拟机,那块最大的内存区域——Ja va堆,就被创建出来了。它可以说是垃圾收集器工作的“主战场”,被所有线程共享,其核心使命就是存放各种对象实例。
为了更高效地管理内存和进行垃圾回收,堆空间被划分成了老年代和新生代。老年代,顾名思义,存放的是那些“长寿”对象,要么是经过多次垃圾回收(GC)依然坚挺的“老兵”,要么是那些一出生就过于庞大、新生代根本放不下的“巨婴”。而新生代,则是新创建对象的“摇篮”,它内部又细分为三个区域:Eden区、From Survivor区(简称From区)和To Survivor区(简称To区)。具体结构可以参考下图:

一个新对象诞生后,首先会被安置在新生代的Eden区,这里也是新生代最主要的内存区域。当Eden区快满时,会触发一次针对新生代的垃圾回收(Minor GC)。在这次回收中存活下来的对象,会被移动到From区。
接下来的过程就有点“二人转”的味道了。From区里的对象,经过下一次GC后,存活下来的会被转移到To区。而To区里的对象再经历GC时,存活者又会被挪回From区。这里的关键在于,From区和To区的角色并不是固定的,它们会随着GC算法的执行而相互转换。严格来说,每次都是“当前存活的From区”经过GC后,将幸存者送入“当前空闲的To区”。
那么,对象什么时候才能“晋升”到老年代呢?通常,在From区和To区之间来回“折腾”了足够多次(默认是15次)后,如果依然存活,就证明它是一个值得长期持有的对象,于是便会被转移到老年代中。
堆内存的优势非常明显:可以动态地按需分配内存大小,对象的生命周期也无需事先声明,不再使用的数据会有垃圾收集器自动清理。但凡事都有两面性,这种动态分配的灵活性,带来的代价就是存取速度相对较慢。
与共享的堆不同,Ja va栈是线程私有的,它的生命周期与所属线程完全绑定。栈的核心作用,是为Ja va方法的执行提供内存模型。每个方法被执行时,都会在栈中创建一个对应的“栈帧”,用来存储这个方法的局部变量表、操作数栈、动态链接、方法出口等信息。可以说,一个方法从被调用到执行完毕的过程,就对应着它的栈帧在虚拟机栈中从入栈到出栈的完整旅程。
栈的优势在于速度极快,其存取效率仅次于CPU内部的寄存器。但它的缺点也同样突出:存放在栈中的数据,其大小和生存期都必须在编译期就确定下来,缺乏堆那种运行时动态调整的灵活性。
理论说了这么多,不如看一段具体的代码来得直观。我们来看下面这个例子:
public class Test1 {
public void m1(){
int a = 3;
Person person = new Person();
person.setName("name");
m2();
}
public void m2(){
int a = 3;
Person person = new Person();
person.setAge(20);
Person person2 = person;
person2.setName("name2");
}
}
这段代码里,Test1类定义了m1()和m2()两个方法。我们来一步步拆解,看看其中的变量在JVM的堆和栈中是如何分布的。其内存模型可以参考下图:

m1()方法:JVM会为m1()在栈中开辟一个栈帧。首先,存放基本类型变量int a = 3;接着,定义引用类型变量Person person,并执行new Person()——这一步是关键,它会在堆中开辟一块新空间,用于存储这个新创建的Person对象实例,栈中的person变量则保存着指向堆中该对象的引用地址。person.setName("name"):这行代码通过栈中person变量持有的引用,找到堆中对应的Person对象,并修改其name属性。m2()方法:此时,JVM会为m2()在栈顶开辟一个新的栈帧(位于m1栈帧之上)。同样,先存放int a = 3;然后定义Person person并执行new Person(),这会在堆中再次开辟一块新的空间,创建另一个Person对象。Person person2 = person:这是理解引用类型的关键。这行代码并没有在堆中创建新的对象,它仅仅是将栈中person变量保存的引用地址,复制了一份给person2变量。结果是,person和person2这两个栈中的变量,指向了堆中同一个Person对象。person2修改对象:当执行person2.setName("name2")时,通过person2的引用修改了堆中那个唯一对象的数据。此时,通过person变量去访问这个对象,其name属性也同步变成了“name2”。这里引出了一个至关重要的概念:基本数据类型与引用数据类型的根本区别。
基本数据类型(如int, short, long, byte, float, double, boolean, char,共8种,注意String不属于基本类型)的变量,存储的是实际的“字面值”。比如int a = 3;,这个“3”就直接存放在栈帧的局部变量表中。因为它们大小固定、生命周期明确(通常随方法结束而消失),出于追求极致速度的考虑,被安排在栈中。栈还有一个优化:对于字面值相同的变量,可能会共享同一份数据。例如同时定义int a = 3;和int b = 3;,编译器可能会让a和b指向栈中同一个字面值“3”。
引用数据类型(即对象类型)则完全不同。当我们写Person person = new Person();时,new Person()这个动作在堆中创建了对象的实际数据,而栈中的变量person仅仅保存了一个指向堆中那个对象的“地址引用”。因此,当多个引用变量(如person和person2)被赋值为指向同一个堆对象时,无论通过其中哪一个变量去修改对象状态,所有指向该对象的变量所看到的内容都会同步改变。这正是上例中第4、5步所演示的情况。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8