1. 垃圾回收
垃圾回收是Java程序设计中内存管理的核心概念,JVM的内存管理机制被称为垃圾回收机制。
一个对象创建后被放置在JVM的堆内存中,当永远不再引用这个对象时,它将被JVM在堆内存中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。即当对象在JVM运行空间中无法通过根集合到达(找到)时,这个对象被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。JVM通过根集合索引对象。
在做Java应用开发时经常会用到由JVM管理的两种类型的内存:堆内存和栈内存。简单来讲,堆内存主要用来存储程序在运行时创建或实例化的对象与变量。例如通过new关键字创建的对象。而栈内存则是用来存储程序代码中声明为静态或非静态的方法。
(1) 堆内存
堆内存在JVM启动的时候被创建,堆内存中所存储的对象可以被JVM自动回收,不能通过其他外部手段回收,也就是说开发人员无法通过添加相关代码的手段来回收堆内存中的对象。堆内存通常情况下被分为两个区域:新对象区域与老对象区域。
新对象区域:又可细分为三个小区域:伊甸园区域、From区域与To区域。伊甸园区域用来保存新创建的对象,它就像一个堆栈,新的对象被创建,就像指向该栈的指针在增长一样,当伊甸园区域中的对象满了之后,JVM系统将要做到可达性测试,主要任务是检测有哪些对象由根集合出发是不可达的,这些对象就可以被JVM回收,并且将所有的活动对象从伊甸园区域拷贝到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。上面对象迁移的整个过程,都是由JVM控制完成的。
老对象区域:在老对象区域中的对象仍然会有一个较长的生命周期,大多数的JVM系统垃圾对象,都是源于"短命"对象,经过一段时间后,被转入老对象区域的对象,就变成了垃圾对象。此时,它们都被打上相应的标记,JVM系统将会自动回收这些垃圾对象,建议不要频繁地强制系统作垃圾回收,这是因为JVM会利用有限的系统资源,优先完成垃圾回收工作,导致应用无法快速地响应来自用户端的请求,这样会影响系统的整体性能。
(2) 栈内存
堆内存主要用来存储程序在运行时创建或实例化的对象与变量。例如通过new关键字创建的对象。而栈内存则是用来存储程序代码中声明为静态或非静态的方法。
一、引用计数
引用记数是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用记数器,当有引用链接至对象时,引用记数加1。
虽然管理引用记数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用记数为0时,就释放其占用的空间(但是,引用记数模式经常会在记数值变为0时立即释放对象)。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大。引用记数常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中。
二、“追溯引用”
在一些更快的模式中,垃圾回收器并非基于引用记数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。
由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。您所访问过的对象必须都是“活”的。注意,这就解决了“交互自引用的对象组”的问题——这种现象根本不会被发现,因此也就被自动回收了。
尽管如此,复制式回收器仍然会将所有内存自一处复制到另一处,这很浪费。为了避免这种情形,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(即“自适应”)。这种模式称为标记-清扫(mark-and-sweep),Sun公司早期版本的Java虚拟机使用了这种技术。对一般用途而言,“标记-清扫”方式速度相当慢,但是当您知道只会产生少量垃圾甚至不会产生垃圾时,它的速度就很快了。
“标记-清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。
“停止-复制”的意思是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会被暂停。在Sun公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但事实上垃圾回收器在Sun公司早期版本的Java虚拟机中并非以这种方式实现的。当可用内存数量较低时,Sun版本的垃圾回收器会暂停运行程序,同样,“标记-清扫”工作也必须在程序暂停的情况下才能进行。
在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种做法名为停止-复制(stop-and-copy)。显然这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。(挑拣的工作)当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。
当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想像成有个表格,将旧地址映射至新地址)。
对于这种所谓的“复制式回收器”而言,效率会降低,这有两个原因。
首先,得有两个堆,然后得在这两个分离的堆之间来回捣腾,从而维护比实际需要多一倍的空间。某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
第二个问题在于复制。程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。
“块”
如前文所述,在这里所讨论的Java虚拟机中,内存分配以较大的“块”为单位。如果对象较大,它会占用单独的块。严格来说,“停止-复制”要求在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的代数(generation count)来记录它是否还存活。
通常,如果块在某处被引用,其代数会增加;垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。
Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;同样,Java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。这就是“自适应”技术,您可以给它个罗嗦的称呼:“自适应的、分代的、停止-复制、标记-清扫”式垃圾回收器。
Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。
此时,有两种方案可供选择。一种是就让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。
另一种做法称为惰性评估(lazy cvaluation)意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被JIT所编译。新版JDK中的JavaHotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。
小结:
1、存储空间的释放会影响存储空间的分配。
2、Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。
3、引用计数可能会出现“对象应该被回收,但引用计数却不为零”的情况。
4、停止-复制:暂停程序,从当前堆复制到另一个堆。
5、“停止-复制”和“标记-清扫”都不属于后台回收模式。
6、“自适应”技术用于切换工作模式,如从“停止-复制”切换到“标记-清扫”。
可参考:java 垃圾回收总结
http://www.cnblogs.com/aigongsi/archive/2012/04/06/2434771.html ,http://www.cnblogs.com/aigongsi/archive/2012/04/13/2446166.html
2. JVM中对象的生命周期
在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:
创建阶段;
应用阶段;
不可视阶段;
不可到达阶段;
可收集阶段;
终结阶段;
释放阶段
上面这7个阶段,构成了JVM中对象的完整的生命周期。
(1) 创建阶段
在对象的创建阶段,系统主要通过下面的步骤,完成对象的创建过程:
<1> 为对象分配存储空间;
<2> 开始构造对象;
<3> 从超类到子类对static成员进行初始化;
<4> 超类成员变量按顺序初始化,递归调用超类的构造方法;
<5> 子类成员变量按顺序初始化,子类构造方法调用。
在创建对象时应注意几个关键应用规则:
<1> 避免在循环体中创建对象,即使该对象占用内存空间不大。
<2> 尽量及时使对象符合垃圾回收标准。比如 myObject = null。
<3> 不要采用过深的继承层次。
<4> 访问本地变量优于访问类中的变量。
(2) 应用阶段
在对象的引用阶段,对象具备如下特征:
<1> 系统至少维护着对象的一个强引用(Strong Reference);
<2> 所有对该对象的引用全部是强引用(除非我们显示地适用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference)).
强引用(Strong Reference):是指JVM内存管理器从根引用集合出发遍历堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,这个对象的引用就被称为强引用。
软引用(Soft Reference):软引用的主要特点是有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此内存足够时它们通常不被回收。另外这些引用对象还能保证在Java抛出OutOfMemory异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache功能,保证最大限度地使用内存你而不引起OutOfMemory。
下面是软引用的实现代码:
import java.lang.
ref
.SoftReference;
...
A a =
new
A();
...
// 使用a
...
// 使用完了a, 将它设置为soft引用类型,并且释放强引用
SoftReference sr =
new
SoftReference(a);
a =
null
;
...
// 下次使用时
if
(sr !=
null
) {
a = sr.
get
();
}
else
{
// GC由于低内存,已释放a,因此需要重新装载
a =
new
A();
sr =
new
SoftReference(a);
}
|
软引用技术的引进使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃。因此在处理一些占用内存较大且生命周期较长,但使用并不繁地对象时应尽量应用该技术。提高系统稳定性。
弱引用(Weak Reference):弱应用对象与软引用对象的最大不同就在于:GC在进行垃圾回收时,需要通过算法检查是否回收Soft应用对象,而对于Weak引用,GC总是进行回收。Weak引用对象更容易、更快地被GC回收。Weak引用对象常常用于Map结构中。
import java.lang.
ref
.WeakReference;
4. ...
5.
6. A a =
new
A();
7. ...
8.
9.
// 使用a
10. ...
11.
12.
// 使用完了a, 将它设置为Weak引用类型,并且释放强引用
13. WeakReference wr =
new
WeakReference(a);
14. a =
null
;
15. ...
16.
17.
// 下次使用时
18.
if
(wr !=
null
) {
19. a = wr.
get
();
20. }
else
{
21. a =
new
A();
22. wr =
new
WeakReference(a);
23. }
|
虚引用(Phantom Reference): 虚引用的用途较少,主要用于辅助finalize函数的使用。
虚引用(Phantom Reference)对象指一些执行完了finalize函数,并为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖了Refernce的clear()方法,增强资源回收机制的灵活性。
在实际程序设计中一般很少使用弱引用和虚引用,是用软引用的情况较多,因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
(3) 不可视阶段
当一个对象处于不可视阶段,说明我们在其他区域的代码中已经不可以在引用它,其强引用已经消失,例如,本地变量超出了其可视
的范围。
1
.
try
{
2
. Object localObj =
new
Object();
3
. localObj.doSomething();
4
. }
catch
(Exception e) {
5
. e.printStackTrace();
6
. }
7
.
8
.
if
(
true
) {
9
.
// 此区域中localObj 对象已经不可视了, 编译器会报错。
10
. localObj.doSomething();
11
. }
|
(4) 不可到达阶段
处于不可达阶段的对象在虚拟机的对象引用根集合中再也找不到直接或间接地强引用,这些对象一般是所有线程栈中的临时变量。所有已经装载的静态变量或者是对本地代码接口的引用。
(5) 可收集阶段、终结阶段与释放阶段
当一个对象处于可收集阶段、终结阶段与释放阶段时,该对象有如下三种情况:
<1> 回收器发现该对象已经不可达。
<2> finalize方法已经被执行。
<3> 对象空间已被重用。