对象的生命周期,从创建到回收

1. 说明

java是一门完全的面向对象编程语言。对于开发者而言,面向对象的思想无疑是非常重要的,但是对于对象本身我们也有必要知道,对象从怎么来的?又怎么死的。

通常我们创建一个对象最常见的方式如下

Object object=new Object();

这样对象就被创建了,我们可以操作object去实现我们需要的功能,但是问题在于,怎么创建的?执行了所有代码后,对象又去哪儿了呢?

2. 如何创建

学过java的都知道,对象创建后,会调用构造方法和代码块完成初始化(这里不探究父类子类的执行顺序),这个过程是可控的,是程序员所能够完全操作的过程。但是往深一点分析,对象是如何被创建的呢。

  • 对象存放在什么地方

    首先应该思考的问题是java虚拟机在什么地方存放对象。

    java虚拟机将内存按照以下内容进行管理

    • 方法区

    • 虚拟机栈

    • 本地方法栈

    • java堆

    • 程序计数器

    如果不考虑逃逸分析和栈上分配,java虚拟机将对象分配在堆中。

  • 开辟多大的内存空间

    既然是要存放对象,自然需要在堆中开辟一块内存,那么问题来了,应该为对象开辟多大的内存空间呢

    为了使得问题变得简单,通常我们希望对象分配多大内存在分配之前是已知的,那么我们的内存大小是否已知呢?答案是是的,看看类中有什么,属性和方法,属性由基本数据类型和引用数据类型,这些数据类型所占的内存都是已知的,所以在分配之前我们知道应该分配多大内存。

  • 对象由什么组成

    对象内存由三个部分组成:对象头,对象体,对齐填充

    • 对象头

      第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。

      Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

      对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

      如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

    • 实例数据

      实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

    • 对齐填充

      自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

  • 怎么分配的

    • 指针碰撞

      假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

    • 空闲列表

      如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

    • 线程分配缓冲

      上面的两种方式看上去没有什么问题,但是考虑一下多线程环境下分配对象,很可能将空闲的内存和已经使用的内存搞混,如何解决这个问题呢。java虚拟机为每一个线程分配了一个线程分配缓冲(TLAB)不同线程分配内存的时候现在线程分配缓冲中分配,这样对于线程分配缓冲而言,每次只有一个线程在他上面分配内存,这样就不会将使用过和未使用的内存搞混了。

  • 为什么对象属性有初始值

    已经有了一块内存用来存对象了,但是这块内存可能之前被别人使用过,内存中的数据还是以前的数据,那么为什么我们访问对象的一个成员变量,即使我们没有初始化也会有个初始值呢?

    当内存分配完成后,jvm会将对象分配的内存空间初始化为零值(不包括对象头)。

    这样我们就有了一个真正意义上的对象,之前我们分析过,对象可以按照程序员的要求进行初始化,初始化的方法有很多,构造方法,代码块,直接初始化等等,java虚拟机会按照一定的顺序将这些过程封装成一个<init>方法,调用后就得到了一个对象了。

  • 小总结

    • 检查类是否加载进入方法区,如果没有则执行类加载过程
    • 分配一块固定大小的内存块
    • 将内存块初始化零值
    • 执行<init>方法

3. 如何回收

java的一个特点是,对象不需要程序员手动回收,而是通过java虚拟机实现垃圾自动回收。

既然要回收我们一定要搞清楚这几个问题?

  1. 什么对象需要回收
  2. 对象如何回收

对象如何回收依靠具体的垃圾回收器实现,显然在这里知道什么对象需要回收更重要。

  • 分析

    什么对象需要回收呢?联想一下什么样的东西是垃圾?所谓垃圾指的是这个东西已经没有人使用了,那么它就是垃圾,对象也是一样,当一个对象没有地方引用了,这个对象就是垃圾,就应该被回收,如何直到这个对象没用了呢?一般有两种方法,但是这里先不说,而是以我们自己的思路去分析。

    public class Test{
        public static Object func1(){
            Object o1=new Object();
            return o1;
    	}
        public static void func2(){
           Object o2=func1(); 
        }
        public static void main(String[] args){
            func2();
        }
    }
    

    我们通常是通过引用互相赋值来实现对象引用关系的变换,以上面的代码为例。在func2方法中调用func1方法,会先创建一个对象,该对象通过o1进行访问。如果没有func2方法中Object o2=func1(),o1所指向的对象就无法被访问了,而现在由于将o1赋给了02,虽然o1已经不再指向对象,但是依然可以通过o2去访问,也就是说该对象还能用,不能被回收,当func2方法执行完,由于main方法中无法保存之前那个对象的引用,导致该对象在也无法被访问,因此该对象可以被回收。

    所以我们可以记录每个对象有多少个引用存在,当引用数目为0的时候,则这个对象可以被回收。

    但是存在这么一个问题

    Class Test{
        Test t;
        public static Object func1(){
         	Test t1,t2;
            t1.t=t2;
            t2.t=t1;
    	}
        public static void main(String[] args){
         	func1();
        }
    }
    

    func执行完,很显然t1和t2应该被回收,但是t2保存了t1的引用,t1保存了t2的引用,那么他们的引用数都不是0。

    虽然上面这种算法是很简单实现的,但是因为存在互相引用的权限,所以我们只能再次进行思考。

    我们会在上面地方使用引用指向一个对象呢?类的静态变量,类的常量,栈中的执行方法时的局部变量。

    既然我们只会在这几种地方使用引用执行一个对象,那么不管对象之间是否存在相互引用,只要他们没有直接或者间接的被上面几种引用指向,那么这个对象就是可以回收的,这样看来,那些引用就像根一样,所有可用的对象必然都有一个这样的根。

    其实上面两种思路对应着两种不同的垃圾回收算法

    • 引用计数法
    • 可达性分析
  • 引用计数法

    为每一个对象维护一个引用计数器,每当该对象被引用则加一,取消引用则减一,当引用计数器为0时,说明该对象无法被引用,则可以进行回收。这种方法的优点是实现简单,效率高,但是缺点是无法解决循环引用的问题。

  • 可达性分析

    可达性分析算法,从被称为GC_Root的引用进行查找所走过的路径称为引用链,不在任何一条引用链的对象就是不可达的,可以被回收,能够作为GC_Root的对象包括虚拟机栈上(局部变量表)的引用,方法区红静态变量,方法区中的常量。

posted @ 2019-07-24 20:22  _zeng  阅读(928)  评论(0编辑  收藏  举报