本文内容总结自周志明先生所编著的《深入理解Java虚拟机-JVM高级特性与最佳实践》此书的经典不必多说。本节内容是对象的创建.、分配的内容。
对象的创建
java对象的创建有几种方式呢(这里所说的java对象仅限于普通java对象不包含数据和Class对象)?大致有以下四种方式:
- new关键字。这应该是我们最常见和最常用最简单的创建对象的方式。
- 使用newInstance方法。这里包括Class的newInstance方法和Constructor的newInstance方法(Class的newInstance方法最终调用的也是Constructor的newInstance方法)。
- 使用clone方法。要使用clone方法我们必须实现实现Cloneable接口,用clone方法创建对象并不会调用任何构造函数。即我们所说的浅copy。
- 反序列化。要实现反序列化我们需要让我们的类实现Serializable接口。当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。即我们所说的深Copy。
上面的四种创建对象的方法除了第一种使用new指令之外,其他三种都是使用invokespecial(构造函数的直接调用)。这里我们只说new创建对象的方式,关于invokespecial心急的同学可以看一下
http://wensiqun.iteye.com/blog/1125503。下面我们来看看当虚拟机遇到new指令的时候发生了什么事。
java虚拟机规范中规定了几种类初始化的几种条件,其中就有遇到new指令的时候(在虚拟机的生命周期中,一个类只会在一个类加载器初始化一次)。所以当虚拟机遇到一条new指令的时候首先会检查这个类有没有被初始化过,如果没有则首先进行类的初始化的操作。这个检查是怎么进行的呢?虚拟机会去检查这个指令的参数是否能在常量池中定位到一个相应的符号引用(关于符号引用的内容可以看一下R大的回答
https://www.zhihu.com/question/30300585?sort=created ) ,然后检查这个符号引用代表的类是否已被加载、解析、和初始化过。
分配内存
当上面的检查通过之后,虚拟机就会为新生对象分配内存了。这里需要注意的是:
对象所需内存的大小在类加载完成后便可以完全确定了。既然对象所需的内存大小可以确定了,那为对象分配内存空间就相当于从java堆中找一份相应大小的内存空间了。由于不同的虚拟机所采用的垃圾收集算法不同,或者相同的虚拟机根据不同的配置所采用不同的垃圾收集算法,所以会导致jvm中的内存可能是规整(有一块连续的内存空间)或者不规整(内存一个碎块一个碎块的)(使用Serial、ParNew等带Compact过程的收集器时,java内存是相对规整的,使用CMS这种基于Mark-Sweep算法的收集器时,java内存是相对不规整的。这一部分的内容参考垃圾收集器)。对于内存规整的,对象内存分配方式为“指针碰撞”,对于不规整的内存,对象内存分配方式为空闲列表方法。
指针碰撞
由于java堆中的内存是绝对规整(具体的参看标记压缩的垃圾回收机制)的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针指向空闲空间那边,然后挪动一段与对象大小相等的距离。
空闲列表
由于java堆中的内存是不规整的(具体的参看标记清除的垃圾回收机制)的,正在使用的内存和空闲的内存是交织在一起的,这个时候虚拟机会维护一个列表,在这个列表中会记录那些内存块是可以使用的,所在在分配内存的时候,只需要从列表中找到一块足够大的空间划分给对象就行了。
上面说的是两种为对象分配内存的方式,你以为有这两种内存分配方式就行了吗?图样图森破。只要做过项目的人都知道,对象的创建时多么频繁的一件事,所以这么频繁的创建对象就会产生线程安全的问题。有可能我现在正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来进行内存分配。所以这个时候就需要考虑线程安全的问题了。jvm解决这个问题有两种方式,一种方式是使用CAS算法,另一种是使用
TLAB(Thread Local Allocation Buffer 本地线程分配缓冲)。即,把内存的分配动作按照线程划分在不同的空间之中进行,也就是每个线程在Java堆中预先分配一小块内存。哪个线程需要分配内存,就在哪个线程的TLAB上分配。
在内存分配完成之后,需要做的一件事是为属性赋初始值。然后虚拟机会对对象进行一些必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息就存放在对象头中。对象头就是我们下节所要讨论的内容。