# HotSpot虚拟机对象
# 对象的创建
# 类加载
虚拟机遇到一条new指令时,会去首先检查这个指令的参数能否在常量池定位到这个类的符号引用,并检查这个符号引用代表的类是否被加载过、解析过和初始化过。如果没有,就要执行类的加载过程。
# 类加载器
- 启动类加载器(Bootstrap ClassLoader):这个类加载器由C++实现,是虚拟机自身的一部分;启动类加载器无法被Java程序直接引用的。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,开发者可以直接使用该扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。它一般被称为系统类加载器,负责加载用户类路径上所指定的类库。开发者可以直接使用这个类加载器,一般就是程序中默认的类加载器。
# 双亲委派模型
类加载器之间的层次关系,被称为是类加载器的双亲委派模型。其中,除了顶层加载器之外,其余的类加载器都会有自己的父类加载器。它们之间的父子关系不是继承,而是以组合的关系来复用父类加载器的代码。
双亲委派模型的工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求都委派给父类加载器去完成,每一个层次的类加载器都是如此。所以所有的加载请求最终都会被传送到顶层的启动类加载器中。只有当父类加载器无法完成这个加载请求时(即它的搜索范围类没有找到这个类),子加载器才会尝试自己去完成这个加载请求。
双亲委派模型的好处就是Java随着它的类加载器一起具备了一种带有优先级的层次关系。比如Object类在各种类加载环境中都是同一个类,即java.lang.Object
。
而如果没有双亲委派模型的话,如果开发者自己编写了一个同名的Object类,那么系统中会有多个Object类,那么需要加载哪个类就很混乱。
# 破坏双亲委派模型
之所以有时候要破坏双亲委派模型,是因为在某些情况下,父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器可能会无法加载到需要的文件,这时候就需要启动类加载器来委托子类来加载实现,从而破坏了双亲委派模型(上述的只是一个例子)。
到目前为止,双亲委派模型出现过三次较大规模的「被破坏」情况:
- 第一次其实出现在双亲委派模型之前,即JDK1.2发布之前。当时,用户必须去重写
loadClass
方法去继承java.lang.ClassLoader
。 - 第二次是由于这个模型自身的缺陷导致的。双亲委派模型很好地解决了各个类加载器的基础类的统一问题,但是当基础类又要调回到用户的代码中,该怎么办?为了解决这个问题,Java设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过
java.lang.Thread
类的setContextClassLoader()
方法进行设置。如果创建线程时还没有设置,它将会从父线程中继承一个,如果整个应用程序的全局范围内都没设置过的话,那么这个类加载器默认就是应用程序类加载器。 - 第三次是因为用户对于程序动态性的追求导致的。因为用户最求一些例如:代码热替换(HotSwap)、模块热部署(Hot Deployment)等,不需要重启服务器就能立即使用的情况。
# 分配内存
在类加载检查过后,就要为新生对象分配内存。分配方式有两种:
- 指针碰撞
- 空闲列表
具体选择哪种方式由Java堆是否规整来决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理的功能来决定。
# 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。
# 设置对象头
虚拟机要对对象进行必要的设置,例如对象是哪个类的实例、对象的哈希码、对象GC分代等信息,都存储在信息头中。
# 执行init
方法
new
指令之后会接着执行init
方法,初始化之后对象才算完全生产出来。
# 对象的内存布局
对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
# 对象头
对象头包括两部分信息:
- 第一部分:用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁标志)等
- 第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
# 实例数据
实例数据部分是对象真正存储的有效信息,也是程序中所定义的各种类型的字段内容。
# 对齐填充
对齐填充部分不是必然存在的,也没有特别的含义,仅仅起到占位作用。
# 对象的访问定位
对于对象的访问方式,主流的有使用句柄和直接指针两种。
# 使用句柄
使用句柄的话,Java堆中会划分出一块内存来作为句柄池,引用存储的就是对象的句柄地址,句柄中包括了对象实例数据与类型数据鸽子的具体地址信息;
# 直接指针
如果使用直接指针,那么引用存储的就是对象的地址
# 对比
两种对象的访问方式各有优劣。使用句柄的好处是引用存储的是稳定的句柄地址,那么对象移动时只会改变句柄中的实例数据指针。
使用直接指针访问的最大好处是速度快,节省了一次指针定位的时间花销。