Jvm对象创建过程

        jvm创建对象主要经过检查加载、分配内存、内存空间初始化、设置对象头、初始化对象几个阶段。创建

Jvm对象创建过程_第1张图片

检查加载

        检查类是否已经被加载、解析和初始化过。虚拟机遇到一条new指令时,首先检查是否被类加载器加载。如果没有,则执行相应的类加载过程。类加载就是把class加载到JVM的运行时数据区的过程。如果有多个线程,只会有一个线程完成类加载。JVM保证类加载是线程安全的。

分配内存       

        类加载无误后,开始为对象分配空间。即把一块确定大小的内存从Java堆中划分出来。内存分配常用的有指针碰撞和空闲列表两种方案。

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

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

        具体分配方式由Jvm堆是否规整决定,Jvm堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。如果是Serial、ParNew等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效;如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。

        对象创建在虚拟机中是非常频繁的行为,因而产生大量的线程并发安全问题。Jvm通常采用CAS自旋和TLAB(本地线程缓冲)方案,来处理对象创建中线程安全问题。

        CAS机制:对分配内存空间的动作进行同步处理,虚拟机采用CAS加失败重试的方式保证更新操作的原子性。通过自旋的方式不断尝试分配内存,直到分配成功。

        TALB机制:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),JVM在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。TLAB只是让每个线程有私有的分配指针,但是对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。(Jvm默认使用此方案)。

内存空间初始化

        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

设置

        设置对象头信息,如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

        在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

       对象头包括三部分信息。Mark World、类型指针和数组长度数据。Mark World,用于存储对象自身的运行时数据,包括哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;类型指针,对象指向它的类元数据的指针,虚拟机用来确定对象是哪个类的实例;数组长度,对象是一个java数组时,用于记录数组长度的数据。

       对齐填充起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

Jvm对象创建过程_第2张图片

对象初始化,即调用构造方法,完成对象初始化工作。

对象分配策略

        绝大多数的对象都是在堆上进行分配。经过“逃逸分析”,被认为不会发生逃逸的对象,则可能会在栈上进行分配。“逃逸分析”即虚拟机判断该对象是否会逃逸到外部被访问。逃逸到方法外,称为“方法逃逸”;逃逸到其他线程被访问,称为“线程逃逸”。栈上分配的对象,当方法执行完,自动弹栈,会自动进行释放,无需进行垃圾回收。

        确定在堆上进行分配的对象,优先在Eden区域分配。这里采用“指针碰撞”或“TLAB”方法来处理多线程并发安全问题(上文有详细介绍)。

        针对大对象(如数组、长字符串、bitmap等)会直接分配到Tenured区域。大小超过虚拟机设置阈值的对象认为是大对象,hotsopt默认是10M,通过 "-XX:PretenureSizeThreshold = 10M" 配置进行设置。

        长期存活对象晋级到Tenured区域。新生代区的对象,每经过一次垃圾回收,没有被释放的对象,GC分代年龄会进行+1(在我另一篇博客“Jvm垃圾回收机制”中有详细介绍)。当GC分代年龄达到虚拟机设置阈值(hotspot 默认是15),则将对象晋级到Tenured区。当对象符合“动态对象年龄判定”会提前晋级。动态对象年龄判定,指的是在Survivor( From/To)区域,超过一定年龄(比如5)的对象已经超过空间的一半,这些对象会提起晋级到Tenured区。

        在晋级或直接分配到Tenured区域,分配方法有“悲观策略”和“空间分配担保”两种策略。悲观策略指的是,为了保证Tenured有足够空间,每一次对象进入Tenured区域要进行一次fullGC,因此会每次晋级会产生额外的开销;空间分配担保,指的是不进行fullGC,直接进行分配,在担保失败时再进行fullGC。

Jvm对象创建过程_第3张图片

你可能感兴趣的:(zh和他的Android,java)