参考资料,感谢作者,写的非常形象!
1.栈
栈(stack)是简单的数据结构,但在计算机中使用广泛。它是有序的元素集合。栈最显著的特征是LIFO (Last In, First Out, 后进先出)。当我们往箱子里存放一叠书时,先存放的书在箱子下面,我们必须将后存放的书取出来,才能看到和拿出早先存放的书。

栈中的每个元素称为一个frame。而最上层元素称为top frame。栈只支持三个操作, pop, top, push。
pop取出栈中最上层元素(8),栈的最上层元素变为早先进入的元素(9)。
top查看栈的最上层元素(8)。
push将一个新的元素(5)放在栈的最上层。
栈不支持其他操作。如果想取出元素12, 必须进行3次pop操作。

栈最经典的计算机应用是函数调用。每个进程都会有一个栈,每个frame中记录了调用函数的参数,自动变量和返回地址。当该函数调用一个新的函数时,栈中会 push一个frame(成为压栈)。当函数执行完毕返回时,该frame会pop(称为出栈),从而进入调用该函数的原函数,继续执行。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。栈有一个很重要的特殊性,就是存在栈中的数据可以共享.
在Java中,JVM中的栈记录了线程的方法调用。每个线程拥有一个栈。在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。

Java的参数和局部变量只能是基本类型的变量(比如int),或者对象的引用(reference)。因此,在栈中,只保存有基本类型的变量和对象引用。
引用所指向的对象保存在堆中。(引用可能为Null值,即不指向任何对象)

当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。
2.堆
如上所述,栈(stack)可以自己照顾自己。但堆必须要小心对待。堆是JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收(garbage collection)时,我们主要 回收堆(heap) 的空间。
Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。
在Java中,对象是通过引用使用的(把对象比喻成致命的毒物,引用就像是用于提取毒物的镊子)。如果不再有引用指向对象,那么我们就再也无从调用或者处理该对象。这样的对象将不可到达(unreachable)。垃圾回收用于释放不可到达对象所占据的内存。这是垃圾回收的基本原则。
(不可到达对象是死对象,是垃圾回收所要回收的垃圾)
早期的垃圾回收采用引用计数(reference counting)的机制。每个对象包含一个计数器。当有新的指向该对象的引用时,计数器加1。当引用移除时,计数器减1。当计数器为0时,认为该对象可以进行垃圾回收。
然而,一个可能的问题是,如果有两个对象循环引用(cyclic reference),比如两个对象互相引用,而且此时没有其它(指向A或者指向B)的引用,我们实际上根本无法通过引用到达这两个对象。
因此,我们以栈和static数据为根(root),从根出发,跟随所有的引用,就可以找到所有的可到达对象。也就是说,一个可到达对象,一定被根引用,或者被其他可到达对象引用。
如图,橙色的是可到达,即有引用指向的对象,不会被回收;绿色的是没有引用指向的对象,可能会被回收

3.JVM垃圾回收
JVM的垃圾回收是多种机制的混合。JVM会根据程序运行状况,自行决定采用哪种垃圾回收。
我们先来了解
mark and sweep
。这种机制下,每个对象将有标记信息,用于表示该对象是否可到达。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到所有的可到达对象,并标记mark
。随后,JVM需要扫描整个堆,找到剩余的对象,并清空这些对象所占据的内存。
另一种是
copy and sweep
。这种机制下,堆被分为两个区域。对象总存活于两个区域中的一个。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到可到达对象,将可到达对象复制到空白区域中并紧密排列,修改由于对象移动所造成的引用地址的变化。最后,直接清空对象原先存活的整个区域,使其成为新的空白区域。
可以看到,copy and sweep
需要更加复杂的操作,但也让对象可以紧密排列,避免mark and sweep
中可能出现的空隙。在新建对象时,copy and sweep
可以提供大块的连续空间。因此,如果对象都比较"长寿",那么适用于mark and sweep
。如果对象的"新陈代谢"比较活跃,那么适用于copy and sweep
。
上面两种机制是通过分代回收(generational collection)混合在一起的。每个对象记录有它的世代(generation)信息。所谓的世代,是指该对象所经历的垃圾回收的次数。世代越久远的对象,在内存中存活的时间越久。个人理解就是,越老越不会被回收,越老越值钱;
根据对Java程序的统计观察,世代越久的对象,越不可能被垃圾回收;可理解为古董,越老的古董才越值钱,不会被丢弃,新做的东西就不值钱,坏了就坏了;
现在,具体看一下JVM中的堆:

我们看到,堆分为三代。其中的永久世代(permanent generation)中存活的是Class对象。这些对象不会被垃圾回收。我们在RTTI中已经了解到,每个Class对象代表一个类,包含有类相关的数据与方法,并提供类定义的代码。每个对象在创建时,都要参照相应的Class对象。每个对象都包含有指向其对应Class对象的引用。
年轻世代(young generation)和成熟世代(tenured generation)需要进行垃圾回收。年轻世代中的对象世代较近,而成熟世代中的对象世代较久。

年轻世代(上图中的Young区域)进一步分为三个区域
eden(伊甸): 新生对象存活于该区域。新生对象指从上次GC后新建的对象

from, to: 这两个区域大小相等,相当于copy and sweep中的两个区域。
当新建对象无法放入eden区时,将触发minor collection。JVM采用copy and sweep的策略,将eden区与from区的可到达对象复制到to区。经过一次垃圾回收,eden区和from区清空,to区中则紧密的存放着存活对象。随后,from区成为新的to区, to区成为新的from区。
如果进行minor collection的时候,发现to区放不下,则将部分对象放入成熟世代。另一方面,即使to区没有满,JVM依然会移动世代足够久远的对象到成熟世代。
如果成熟世代放满对象,无法移入新的对象,那么将触发major collection。JVM采用mark and sweep的策略,对成熟世代进行垃圾回收。
网友评论