《Java平台体系》——第二章 JVM——JVM实现:内存管理和垃圾回收

http://qing.weibo.com/2494474521/94aea919330006ux.html​学习JVM实现一些初学者可能感到“高不可攀”,其实说实话我也不想去了解JVM实现的细节,但了解JVM实现中一些关键问题解决思路对我们“修炼”境界还是有帮助的。

在学完JVM规范​之后,我们可能有如下的问题需要获得答案:

□ JVM如何实现运行时数据区的管理(即内存管理)的?
□ JVM如何实现堆(Heap)的垃圾回收?
□ JVM如何实现对多线程的支持?在JVM规范中很少见到多线程的影子,都是谈同步。
□ JVM如何将JVM指令翻译成机器指令?

下面我们就针对这些问题展开一些讨论。

内存管理和垃圾回收

Java的一个重要特性就是自动化内存管理,给Java开发者带来的直接好处就是不用为何时释放Java对象而担心,同时也不用担心类型混乱造成的内存非法操作(大不了抛出一个ClassCastException)。

我们已经知道,Java字节码作为中间码引入的一个很重要特性就是任何引用都是标注类型的,即强类型,任何操作指令都是建立在强类型的基础上,这带来的直接好处就是每个操作都明确知道内存的块大小。强类型为JVM的内存管理奠定了坚实的基础。

JVM内存管理主要解决的问题就是如何为新建的对象有效分配内存,如何将不再被使用的对象从内存中移出,如何将内存碎片整理(C/C++是程序员负责内存管理,而在Java中是JVM负责,换句话来说就是人家帮我们解决了内存管理难题。因此学习JVM的内存管理可以帮助我们在C/C++编程中进行有效的内存管理,在后面内容Java和C/C++中我们会更进一步讨论这些问题)。

内存分配通常有两种方法:连续块和非连续块,其各自的优缺点在大学数据结构教程中有所介绍。连续块内存分配的算法相对简单,例如在当前空闲内存中寻找大于等于请求块的连续内存空间,然后分派给请求块。而非连续块内存分配就复杂些,其首先要解决的问题是如何将请求块分解成小块,然后通过指针关联起来,这种非连续块的分配方法既容易造成内存碎片又容易利用内存碎片,但很多问题都是大量内存碎片造成的,还是连续的好。

内存分配的简单过程如下:

□ 首先计算需要分配的内存大小(对象数据结构的压缩程度和对象的切分方法都对实际分配内存的大小有影响),然后计算JVM当前拥有内存的大小,进行对比,如果够,分配,如果不够,向操作系统申请内存,如果达到JVM规定的上界,抛出错误。
□ 其次,根据具体的内存表示结构分配内存。
□ 最后把分配内存的信息写入一个维护表中(主要为垃圾回收提供信息)。

垃圾回收可以看作是自动化内存管理的另外一个直观称呼,其主要负责无用数据块的清理和碎片整理。

垃圾回收的核心是垃圾回收算法,下面我们简单介绍一些常用的垃圾回收算法(准确说是介绍思想,而不是算法细节),要理解细节请阅读该章的参考资料。

在介绍这些算法之前先明确一些概念:在前面我们说过任何程序的执行都是指令驱动的,所以要在堆(Heap)中开辟内存,也是指令驱动,并且我们也说过Java字节码的指令都在方法(Method)中,那对下面代码你可能会疑惑(其实就是Java字节码类文件格式​中的内容细节,但我们在Java字节码类文件格式​中并没有介绍,而是留给感兴趣的朋友阅读参考资料​):

《Java平台体系》——第二章 JVM——JVM实现:内存管理和垃圾回收


其中有不在方法中的代码:
《Java平台体系》——第二章 JVM——JVM实现:内存管理和垃圾回收


其实类似上面的操作都是在编译期被放到一个叫做<init>的方法中,该方法在类加载成功之后被JVM自行调用。用前面提到的JBE工具浏览<init>方法的字节码代码如下:

《Java平台体系》——第二章 JVM——JVM实现:内存管理和垃圾回收


明白了吧!总结一下:在JVM中任何对象的创建都在方法中进行,也就是在方法堆栈(Java Stacks)中,同样任何对象是否活动或者被引用的源头都是在堆栈(堆栈一定是活动的)中。那么我们判断一个对象是否被引用是否应该从堆栈中开始扫描呢?当然是。那我们开始了解下面的算法吧!

引用计数算法(Reference Counting)
为每个对象维护一个引用计数器,每当被引用的时候计数器加1,每当引用结束时计数器减1,垃圾回收线程定时扫描维护表,发现计数器为0的,进行回收。其最明显的问题是如何解决“我引用你,你引用我”的循环引用。

标记-清除算法(Mark-Sweep)
该算法包括两个步骤,第一步,从堆栈中开始扫描引用对象,对遍历到的对象进行标记;第二步,对没有标记的对象进行垃圾回收。

前两种算法是解决任何管理问题的常规思路,即主动注册和被动扫描。主动注册的成本在注册和注销,被动扫面的成本在扫描,因为扫描的时候要求堆栈暂且保持一段时间的静止。

复制算法(Copying)
该算法首先把内存分成两个大小相等的区,只有一个区有效。然后从堆栈开始扫描引用对象,把扫描到的对象复制到另一个区,当前区被认为无效,即可以回收。
复制算法和标记-清除算法在遍历阶段思路基本一致,其优点是直接对内存进行了碎片整理;其代价在于扫描时期暂停程序运行、复制成本和内存的闲置。

标记-压缩算法(Mark-Compact)
类同于复制算法,只是不将内存区分成两块,而是在现有内存中重新整理和分配,这种算法的难度在如何重新分配从而达到内存整理(即压缩)。

增量收集算法(Incremental Collecting)
前面我们说过,扫描要求程序某一时间静止,静止的时间直接影响程序的表现,增量收集算法是从切分任务的角度来使每次的工作负荷减小,使程序保持一定的顺畅连续感。

分代收集算法(Generational Collecting)
增量收集算法切分任务的思想更进一步的提升就是根据对象本身的特点进行切分,然后根据不同特点的对象采用不同的收集策略。分代收集算法就是把对象根据创建的时间进行分类,对不同寿命的对象进行不同策略的回收算法。目前JVM的实现基本都采用分代法。

还有其它的一些算法(最好叫方法吧,不然总感觉不严谨),例如并行、同步等。

上面的算法从解决问题的思路来看基本都有借鉴和可用之处,那么明智的选择就是“站在巨人肩膀上”了,根据JVM的应用场景充分利用各自的优点,避免各自的缺点。这也是我们看到一些JVM实现提供很多参数让我们根据具体情况进行优化配置的理由了,例如Sun的HotSpot给我们提供客户机(Client)和服务器(Server)两种虚拟机模式。既然这样,我们要做得就是评估我们的应用场景了。

那么如何直观评价一个垃圾回收机对应用场景的适应性呢?我们至少应该从如下几个方面去看:

□ 是否安全,不能把不需要回收的搞掉吧?
□ 是否快,不能让多数时间被垃圾回收占用吧?
□ 是否有效整理内存,不能让内存无限制地混乱膨胀吧?
□ 伸缩性如何?不能遇见单核不错,遇见多核就晕了吧?除非就是针对单核的。
□ 当然还有其它。

垃圾回收就介绍到这里吧!希望把你引进门,修行还得靠个人,抓住这个思路你可以进一步做如下深入研究:

□ 前面提到的算法具体是什么?你可以参考该章的参考资料,可能会帮助你,除外,你还可以用前面内容中的算法的中文或英文进行搜索,获得更多资料。
□ 研究某个具体的虚拟机的垃圾回收机制,至少可以研究一下各自的垃圾回收算法组合应用思路,例如Sun的HotSpot。

如果学完该节内容之后感觉空空的,请阅读该章的实战​部分。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值