编程学习笔记之java相关::内存回收原理

来源:互联网 发布:自学编程入门教程 编辑:程序博客网 时间:2024/06/07 02:16

     不同于C语言或C++等其它语言,java有着自己的一套垃圾内存回收机制。在java中,除了基本类型外,其它如类对象等,都是在堆上分配内存的,但这并不影响java的性能,事实上,java在堆上分配内存的效率,甚至可以和C++在栈上分配内存的效率相媲美,其中原因,就在于java虚拟机的内存回收机制;在其它语言如c++中,都是由程序员来负责垃圾内存的回收,当我们释放某个对象所占据的内存空间时,只是简单的把这段内存重新归入到可使用堆空间的链表中,这也就意为着它们很可能是不连续、分散开的排列阵容,但在java中不同,java在堆上为对象分配空间类似于流水线上的传送带:我们把一个物品(对象)放在传送带上的某段履带(堆内存),然后这个传送带会自动前进(堆指针指向新的内存地址),这样就为我们腾出一个新+空的履带空间,并且这个新空间和上面那个有物品的履带空间是紧挨着的。但这样的话,也会有一些效率上的开销,比如导致频繁的内存页面调度。所以我们就需要一套有效的垃圾回收机制,使得java可以一边回收垃圾内存,一边让对象紧密衔接。下面,我们就来介绍一下几个主要的垃圾回收机制

      1):引用计数(reference counting):这是一种简单粗暴的机制,堆中的每个对象内存中都会附加一个“引用计数”值,每当一个引用连接到该对象内存时,引用计数值会+1,而一旦引用离开作用域就会被-1,如果该对象的引用计数值被置为0,那么该对象就可以被当成垃圾给回收掉。但这种方法存在一个致命的缺陷:如果两个或更多对象存在循环引用,就可能会出现某对象应该被回收,但引用计数却不是0的情况,而且对于垃圾回收器来说,鉴别程序中是否存在交互引用的对象所需要的工作量很大,所以这种垃圾回收机制很少出现在java虚拟机之中。

      2)标记——清扫(mark_sweep):这种模式所依据是思想是,任何存活的对象,都能够最终追溯到该对象在堆栈或静态存储区之中的引用,这个引用链条可能会贯穿N个对象层次。所以我们从堆栈或静态存储区开始,就能够遍历所有的对象引用,对于发现的所有引用,都能够顺藤摸瓜找到它所引用的对象,每找到一个存活的对象,都会把该对象标记起来。如此反复,直到“引用树”被遍历完毕为止,这个时候才可以进行垃圾内存回收的工作。。

      3):停止——复制(stop_copy):顾名思义,就是先暂停程序的运行,然后把所有存活对象从一个堆中倒腾到另一个空闲的堆中,在这个过程中,对象是被一个紧挨一个复制到新堆中去的。动作完成后,原先堆中没有被“倒腾”的都被当成垃圾。当把某个对象从一个地方倒腾到另一个地方时,所有指向该对象的引用都要被修改,位于堆或静态存储区的引用可以直接被修正,但可能还有其它指向该对象的引用,而它们只能在遍历的时候才能被发现。所以我们需要一个表格,它将旧地址映射至新地址,这样我们就可以在遍历的同时进行修改了。(它所依赖的思想和“标记——清扫”一样,也是先遍历堆栈和静态存储区,顺藤摸瓜找出所有的存货对象。)

      鉴于后面两种都是现在主流的垃圾回收机制,那么程序应该如何选择呢?这就要看程序是否处于“稳定”状态了,稳定状态下,程序只会产生少量的垃圾,这个时候,我们就需要采用“标记——清扫”的垃圾回收模式了。相反,如果程序会产生大量的碎片垃圾,那么就需要“停止——复制”了。

     一般的java虚拟机,内存分配单位是块。“停止——复制”在释放旧对象之前,会先把所有的存活对象从旧堆复制到新堆,这是一种大量的内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都有一个相应的代数(generation count)来标识这个块是否还存活。如果该块在某处被引用,那么其对应的代数会+1;垃圾回收器对上次回收动作之后新分配的块进行整理。这对处理大量的临时对象很有帮助,垃圾回收器会定时进行清除动作——大型对象不会被复制,只是它的引用代数会增加,但那些含有小型对象的块则会被复制整理。

     java虚拟机会进行监视,如果所有的对象都比较稳定,那么就会切换到“标记——清扫”模式;同样,如果堆中出现了很多碎片,那么就会切换到“停止——复制”模式中去。这种智能化设计,也叫“自适应”的垃圾回收器。

      java还有一种与加载器相关的,被称为“即时编译”技术。该技术通过java虚拟机把一个程序的全部或部分翻译成本地机器码。当你为某个类创建一个对象(加载某个类)时,编译器会首先找到该类的“ .class ”文件,然后将该类的字节码装入内存。这时,会有两种可选择方案:1)让即时编译器编译所有的代码,但这种方式下,加载动作会散落在整个程序生命周期,程序运行就会花费更多的时间,而且编译器展开后的本地机器码要比原先的字节码长很多,而这将导致一些页面调度,同样会降低程序执行的效率。  2)惰性编译;在这种方式下,即时编译器只在必要的时候才会编译代码。新版JDK中的Java HotSpot就采用这种方法,代码每执行一次都会得到一些优化,如此原因,程序的速度会越来越快。

3 0
原创粉丝点击