认识栈和堆的概念, 有助于了解变量的有效范围(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;
}

不管是实例变量还是局部变量, 它们指向的对象本身在堆上。

程序对方法的调用,以带有方法的状态的堆栈块的形式,不断堆在栈上面,当前被执行的方法处于栈顶。执行完毕的方法会出栈。递归调用太深层,会导致栈空间耗尽。

如果方法中有局部变量引用对象,那么该局部变量也是存在于栈中. 但被引用的对象还是运行在堆上面.

变量的生命周期

  1. 局部变量只会存活在声明该变量的方法中, 其余方法无法接触到
  2. 实例变量的寿命与所属对象相同. 如果所属对象还活着, 则实例变量也会活着.
public class Life {
    int size; // size 在类中一直可以用
    public void setSize(int s) { size = s; }
    // 但 s 仅限于setSize中, 且在方法结束后消失
}

所以这里存在生命周期life和范围scope的概念差别

  • Life:只要变量的堆栈块还存在于堆栈上, 局部变量就还活着
  • Scope:局部变量的活动范围仅限于声明它的方法之内。此方法如果调用别的方法时,该变量还活着,但不在目前活跃的方法范围内。得等到其他方法执行完毕返回时,才回到该局部变量所属方法的范围。

引用变量只能在处于它的范围内才能被引用。即使引用变量暂时不在活跃范围内,只要引用变量还活着,被引用的对象也就活着。如果一个对象唯一的引用变量死了(随着堆栈块一起解散),对象就会被认定为可被垃圾回收(Garbage Collection)的。没有被引用的对象,是没有存在意义的,因为没有人知道它的地址,无法调用它,它的存在只会浪费空间。

程序内存不足时,GC就会去把可GC的对象回收,释放内存,具体怎么回收,回收多少,有很多不同算法。如果回收完全,内存还是不够用,那就会变成真正的内存不足。

三种方法释放对象的引用:

  1. 引用永久性地离开它的范围void go() { Life z = new Life(); }, z会在方法结束时消失.
  2. 引用被赋值到其他对象: Life z = new Life(); z = new Life();, 第一个对象失去了引用
  3. 直接null引用: z = null, null代表空字节组合, 但实际上是什么只有JVM知道