在真实的世界里,每个人都是一个对象,从出生到长大再到死亡是一个完整的生命周期。而在计算机的世界里,对象也会有它的生命周期,包括对象的创建、对象的内存布局、对象的访问和对象的销毁。C++ 中对象是这样,Java 中对象也是这样。只是在 C++ 里对象的生命周期完全由程序员掌控,包括创建、使用和回收。而在 Java 中程序员只需要负责创建和使用对象即可,回收全权交给 Java 虚拟机。创建对象易,销毁对象难。作为 Java 程序员,虽然虚拟机给我们做了对象回收,但不代表我们不用去理解一个完整的对象生命周期。只有理解了对象从生到死的过程,才能对对象爱恨情深。
在程序员看来创建对象大部分情况下就是一个 new 关键字,而在虚拟机中远远不止这么简单。它至少包括如下几个阶段:
流程图如下:
都知道 Java 生成对象需要分配内存,虚拟机又是怎样从内存池中分配一块内存给新创建的对象呢?虚拟机的实现中主要有以下两种方法:
如果 Java 堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
如果 Java 堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
以上两种方法只考虑了对象内存的分配,其实还有内存的回收,也是虚拟机需要做的。内存的分配与回收在C/C++ 中讨论的比较多,对于 Java 对象创建来说这里不是重点,只需要知道对象内存分配有这两种方式就行,它们各有利弊。多说一句,对象内存分配和回收的设计,我只服 nginx,nginx 将内存资源管理这块玩的淋漓尽致,有兴趣的可以看看相关源码。
Java 对象并不只是包含我们在 class 中所定义那部分实例数据,还需要包含虚拟机所需要的一些额外信息以及空填充,即对象头(Header)、实体数据(Instance Data)和对齐填充(Padding)三个部分,如下图所示:
静态变量和函数是属于类的,一个类只有一份,它在类加载的时候存进方法区。 函数作为类的元数据也存在方法区(同时会受到即时编译的影响)。 局部变量在方法执行时在栈中进行动态分配的,主要位置是局部变量表。
C++ 中有一本广为流传的书《深入理解 C++ 对象模型》专门讲 C++ 对象模型的,整整写成了一本书。可见相对于 C++ 对象,Java 要容易的多。C++ 中要计算一个对象的大小,直接使用 sizeof 即可。而在 Java 中却不能那么直接,如果想更具体的了解 Java 对象在内存中的大小,请看 一个java对象占多大内存 这篇文章,自己实践下。其实,只要知道了对象在内存中的组成(头、实例数据和填充)、实例数据中各个字段类型的大小,就能轻易算出对象的整体大小。
就像创建自然世界的人主要目的就是活着,创建计算机世界的对象主要是为了使用它。Java 对象保存在堆上的,而它的引用主要保存在栈上,引用中存放的是对象在堆中的地址。这句话也对也不对,不对在后半句,引用中存放的是啥要看虚拟机访问对象方式的具体实现,主流的有以下两种:
这种方案的好处是引用保存的是稳定的句柄地址,在对象在内存中被移动的时候,只需要修改句柄中的实例数据指针即可,无需修改 reference。
这种方案的好处是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
这下别人再问你 reference 中保存的是什么的时候,别急着回答说是对象的地址哦,也有可能是对象地址的地址。
这是对象生命周期中最复杂的部分,也是 Java 的精髓所在。在 C++ 中一个 new 出来的对象如果不再需要使用的时候,需要手动 DELETE 才行。而 Java 中靠虚拟机来完成回收,虚拟机 GC(Gargbage Collection)对象内存要解决如下两个问题:
当一个对象还在被使用的时候显然是不能被回收的,只有死亡了的(不再使用的)对象才能进行内存回收,那如何判断对象是否不再使用呢?
找到了需要被回收的对象,如何进行回收也是个技术活。一方面它影响到虚拟机的回收性能,另一方面也会影响到对象内存的分配。所以回收算法很重要,主要有以下几种算法:
由于这些算法涉及的内容比较多,这里只抛出概念,后面还会整理一篇文章专门讲虚拟机的垃圾回收,这也是 Java 虚拟机中的重中之重,不想三言两语的带过,也不想在本篇文章大篇幅的介绍。 说起来读者可能不信,在上家公司的一款上线产品(C++实现)中,我们的项目中几乎不允许使用 new(面向过程编程),就是怕一不小心导致内存泄露。真的是如履薄冰,不过我觉得这样做有点因噎废食了,完全没有体现 C++ 面向对象的优势。
C++ 下多线程网络库 muduo 的作者陈硕在它的《Linux多线程服务端编程》中提到的“创建对象很简单,销毁太难”,看了 Java 的对象生命周期管理才终于明白此言不虚。感谢伟大的 Java 发明者,把对象生命周期管理中最容易的部分给了使用者,把最难的部分留给了自己。