认识栈和堆的概念, 有助于了解变量的有效范围(scope),对象的建立,内存管理,线程(thread)和异常处理。
对象生存在可垃圾回收的堆(heap)上面,方法调用(方法的参数,局部变量的引用)的生存空间(stack)。
当JVM启动时,从底层操作系统取得一块内存, 以此区段来执行Java程序。
实例变量和局部变量
实例变量声明在类方法之外:实例变量保存在所属的对象中,位于堆上。如果实例变量是个对对象的引用,则引用和对象都在堆上。
public class Duck {
int size;
Mouth mouth;
}
当新建一个Duck()
时, Java必须在堆上帮它找位置, 保证空间足够存放该对象所有实例变量.
- 对于primitive数据类型的实例变量, Java会根据类型的大小为它们留下堆空间,
int 32, long 64 ...
. - 如果实例变量是个对象, 也就是
Duck
对象带有引用变量mouth
, Java会在堆上留给Duck
的专属空间只包含该引用变量, 不包含引用的那个对象.- 被引用的对象, 只有实际实例化后, 才会在堆上占有一席之地
mouth = new Mouth()
, 该空间并不属于Duck
.
- 被引用的对象, 只有实际实例化后, 才会在堆上占有一席之地
局部变量声明在方法或方法的参数上:它们是暂时的,且生命周期只限于被放在栈上的这段时间(也就是方法调用至执行完毕位止)。所有局部变量存在栈上相对应的堆栈块中,故又称为栈变量。引用对象的局部变量(只是对对象的引用)和primitive数据类型变量也都放在栈上。
public void foo(int x) {
int i = x + 3;
}
不管是实例变量还是局部变量, 它们指向的对象本身在堆上。
栈
程序对方法的调用,以带有方法的状态的堆栈块的形式,不断堆在栈上面,当前被执行的方法处于栈顶。执行完毕的方法会出栈。递归调用太深层,会导致栈空间耗尽。
如果方法中有局部变量引用对象,那么该局部变量也是存在于栈中. 但被引用的对象还是运行在堆上面.
堆
变量的生命周期
- 局部变量只会存活在声明该变量的方法中, 其余方法无法接触到
- 实例变量的寿命与所属对象相同. 如果所属对象还活着, 则实例变量也会活着.
public class Life {
int size; // size 在类中一直可以用
public void setSize(int s) { size = s; }
// 但 s 仅限于setSize中, 且在方法结束后消失
}
所以这里存在生命周期life和范围scope的概念差别
- Life:只要变量的堆栈块还存在于堆栈上, 局部变量就还活着
- Scope:局部变量的活动范围仅限于声明它的方法之内。此方法如果调用别的方法时,该变量还活着,但不在目前活跃的方法范围内。得等到其他方法执行完毕返回时,才回到该局部变量所属方法的范围。
引用变量只能在处于它的范围内才能被引用。即使引用变量暂时不在活跃范围内,只要引用变量还活着,被引用的对象也就活着。如果一个对象唯一的引用变量死了(随着堆栈块一起解散),对象就会被认定为可被垃圾回收(Garbage Collection)的。没有被引用的对象,是没有存在意义的,因为没有人知道它的地址,无法调用它,它的存在只会浪费空间。
程序内存不足时,GC就会去把可GC的对象回收,释放内存,具体怎么回收,回收多少,有很多不同算法。如果回收完全,内存还是不够用,那就会变成真正的内存不足。
三种方法释放对象的引用:
- 引用永久性地离开它的范围
void go() { Life z = new Life(); }
,z
会在方法结束时消失. - 引用被赋值到其他对象:
Life z = new Life(); z = new Life();
, 第一个对象失去了引用 - 直接null引用:
z = null
,null
代表空字节组合, 但实际上是什么只有JVM知道