对象在内存中的布局——对象的创建

 

我们在任何一个项目中,无时无刻不关注对象的创建,时时刻刻都在创建对象,都在使用对象,那么,我们就从虚拟机的角度来看对象的创建。

首先,我们知道,创建对象有多种方式,最直观的一种方式就是通过new关键字来创建对象,而且我们之前也提到过了,我们知道,通过new创建一个对象,那么,那个对象就会存储到堆内存中,那么,下面我们就来具体的看一下对象的创建过程

这个图表示的就是一个对象的创建过程,我们知道,在我们的Java代码中,通过new,后面跟一个类的名字,那么,就可以实例化一个对象,第一步就是我们在运行过程中,首先根据new的参数在常量池中定位一个类的符号引用,然后,接着,如果没有找到这个符号引用说明这个类还没有被加载则进行类的加载、解析和初始化, 也就是说,对象的创建首先是需要类的加载,类的加载我们后面会进行详细的讲解,其实在对象的创建之前需要先加载类,这个地方也就是说,如果没有进行加载类的话,我们首先需要进行类的加载工作,当然,类的加载工作也是一个非常复杂的过程,后面讲,类加载完毕之后,虚拟机要为我们新创建出来的对象分配内存,也就是往堆中分配内存区域,分配完内存区域之后,将分配的内存初始化为零值,也就是说,经过一个内存的初始化的过程,我们知道,我们在创建完一个对象之后,比如说,对象的基本数据类型都有它的默认值,抽象数据类型都为空即null,这就是在

这里做到的,最后就是调用对象的<init>方法,这个所谓的<init>方法,其实就是我们的代码块,包括我们的构造方法。

这个是整个对象的创建过程,其实,我们所能够看到的只有new 类名、调用对象的<init>方法

创建对象的初始化方法的时候,我们是有感知的,我们可以通过断点调试等一些手段,能够看到它确实是在创建对象的过程中触发了这么一个事件。

那么关于这四个位置

其实是虚拟机内部所执行的,对于开发者来讲是屏蔽的,但是,它屏蔽了我们,我们为什么还要来学习它呢?就是因为,我们要知道它的原理。

前两个我们后面会讲,我们从虚拟机为对象分配内存开始讲,那么,虚拟机是如何为对象分配内存的呢?如何往堆中去进行分配的呢?那么,在分配的过程中会出现一些问题,比如说,线程安全性问题,它是如何解决的呢?

下面说一下关于给对象分配内存的一些策略。

我们知道,堆是一块很不连续的存储空间,我们在这里假设堆内存是规整的,那么,用过的内存放到一边,空闲的内存放到另一边,中间放一个指针作为分界点指示器,那么,分配内存其实就是指针移动的过程,我们可以画个图表示一下,比如说,这是我们的一块内存

然后我们把这个内存分成两块区域

那么,假设右边是我们使用的内存,左边是空闲的内存

那么,我们在进行对象创建的过程中,要给这个对象分配堆内存,那么,分配多少呢?我们后面会说,其实在对象的创建过程中,在堆中所创建的内存区域也是已经确定了的,但是,如何计算,我们后面再说,我们只要知道,我们创建了一个对象扔到堆内存中,那么,它肯定会占用存储空间,那么,也就是说,已经使用的空间肯定要增加,那么,剩余的空间肯定要减小,那么,也就是说,这个要往左边移动

从刚才的位置移动到了这里,就说明,我们刚才所创建的那个对象就占用这么多的存储空间

这种分配方式称之为指针碰撞,这是第一种给对象分配内存的方式,当然了,我们说Java堆内存并不是规整的,那么,已使用的内存和空闲的内存,它并不是这么有规矩的,而是,可能使用的和未使用的进行相互交错的,那么,就没办法使用我们这种所谓的指针碰撞了,那,这个时候该怎么办呢?虚拟机就必须维护一个列表,记录哪些内存块是可用的,那么,在分配的时候就可以从这张列表中去找出来一块区域给这个对象的实例,并更新在这么一个表中

这一块记录的就是哪些内存可以使用,哪些内存已经使用,比如说,我们就仅仅记录哪些内存没有使用

假设我们记录了第零块内存没有使用,内存都有编号,那么,给它分配了之后,我们把这一块内存再从这张表里面给删除掉

这么一个方式,就是说,使用一张表来进行记录,那么,这种分配的方式叫做空闲列表。那么,到底该选择哪一种内存的分配方式呢?其实,内存的分配方式是由Java的堆是否规整来进行决定的,而,Java堆是否规整是由我们的垃圾回收策略决定的,垃圾回收器,如果说,它带有压缩整理的功能,就是说,在进行垃圾回收的过程中,它会自动的进行压缩整理,那么,把这个内存区域化分成非常有规则的,像已经使用的和未使用的空间的话,那么,我们就可以使用指针碰撞,如果垃圾回收器,它没有这个功能的话,那么,我们就不能使用指针碰撞,就必须使用空闲列表。关于垃圾回收器,我们到后面会详细的讲。垃圾回收器也有非常的多,也有不同的实现,我们这里就不在去介绍垃圾回收器了。

关于给对象分配内存,我们就说这两点,一个是指针碰撞,一个是空闲列表。

接下来我们说线程安全性问题。

对象的创建怎么还会涉及到线程安全性问题呢?其实这个非常好理解,那么,我们可以通过刚才的这个图来看

比如说指针碰撞的方式,每一次创建对象,栈内存中的这个指针都要像左移动,如果在一个高并发的环境下,内存的创建可能,在同一时刻可能会有多个对象在进行创建,那么,指针在进行移动的过程中,有可能会出现线程的安全性问题,那么,关于线程安全性问题该如何解决呢?当然了,如果是空闲列表的方式,比如说第一个线程过来了,从列表中读了一块,发现这块区域没人用,那么给这块内存分配了,那么,还没来得及更新这个列表,第二个线程有过来了,认为这块依然是空闲空间,那么,就把原来的那一块分配的对象给占用了,

所以,线程安全性问题还是有的。

那么,我们出现了线程安全性问题该如何解决呢?这个类型于我们学习多线程的过程中解决线程安全性问题的方案是一样的。

最简单的方案就是实现线程的同步,加锁,当有一个线程过来了,加锁,不让其他线程进来,当这个线程执行完毕之后,下一个线程才能进来,那么,这种方式虽然安全,但是有一个致命的问题,就是执行效率太低,但是这也不失为一种解决方案,那么,如何来提高执行效率呢?我们可以这样,针对每一个线程,在堆内存中给它单独的分配一块区域,我们之前再讲内存划分的时候也提到过这么一回事,比如说这是我们的堆内存

这一块区域很大,我们会每一个线程都来分配一块自己的区域,这块区域并不是特别大,当然这块区域的容量我们可以通过虚拟机参数来指定,有几个线程我们就给它分配几个区域

我们称之为本地线程分配缓冲,简称就是TLAB,当这个线程来进行对象的分配的时候,我们就在这块区域里面进行分配

另外一个线程在这个里面去进行分配

每一个线程操作不同的区域,那么,就不会再导致线程安全性问题了

当然了,如果这块内存占满了怎么办呢?

占满了之后,我们可以再给他分配一块区域,

那么再进行这块区域分配的时候,我们要对它进行采用同步的策略。这个就是解决线程安全问题的第二个方案,就是采用本地线程缓冲,这种方案其实有效的提高了我们每次都加锁这种性能的问题,提高了一些性能。关于线程安全性问题,我们就说到这里。

接下来讲初始化对象。这里所谓的初始化对象,其实就是把将分配的内存初始化为零值,我们知道,任何一个对象,初始化完毕之后,其实都是有一个默认值的,那么,这里其实就是初始化默认值的问题,当然了,不仅仅这么简单,它还要具体的初始化说对象是哪个类的实例,如何才能找到类的源数据信息,对象的哈希码等等,这些信息都是包含在对象头中,对象头我们后面在说,其实在这一步之后

我们的对象其实在Java虚拟机中已经产生了,因为,内存也都分配好了,也都初始化好了,但是,对于我们开发者来讲,其实这个对象还没有创建完成,也就差最后一步,调用初始化方法,我这里说的是执行构造方法,其实不然啊,它就是调用对象的<init>方法,包括代码块等等,

到这里,对象的创建就完毕了,我们可以通过代码来简单的看一下,其实也没有什么好看的,因为内存分配我们是看不到的,我们所能够看到的其实就是一个初始化对象,以及执行构造方法这么一个过程。

我们提供get/set方法,目的就是来获取它的默认值。

然后,我们写一个测试类

我们可以看到,抽象数据类型的默认初始化值就是空即null,基本数据类型int的默认值就是0,boolean型的初始化默认值是false。

初始化完毕的最后一步会执行构造方法

我们发现,这句话已经打印出来了,而且是在System.out.println(usr.toString())这句话之前执行的。

猜你喜欢

转载自blog.csdn.net/G_66_hero/article/details/84199005