创建对象所需要内存的大小在类加载完成后便完全确定(JVM可以通过普通Java对象的类元数据信息确定对象大小),为对象分配内存就是把堆内的一部分空间划给该对象使用。
一、对象在堆空间的分配方式:指针碰撞 和 空闲列表。
指针碰撞 :
假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)
空闲列表:
如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)
选择哪种方式由内存是否规整决定,而内存是否规整由设定的GC的方式决定。
二、分配推空间是的线程安全问题
对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。
解决办法:
1,分配内存空间的动作进行同步——实际上虚拟机是采用CAS与失败重试的方式保证更新操作的原子性
2,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,(TLAB ,Thread Local Allocation Buffer),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时再使用CAS进行同步操作。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。默认的TLAB使用的空间是Eden Space的1%。TLAB是典型的以空间换效率的做法。
三、逃逸分析
逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是这个对象分配到堆上还是栈上。
计算机原理上的逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
Java在Java SE 6u23以及以后的版本中支持并默认开启了逃逸分析的选项。Java的 HotSpot JIT编译器,能够在方法重载或者动态加载代码的时候对代码进行逃逸分析,同时Java对象在堆上分配和内置线程的特点使得逃逸分析成Java的重要功能。
逃逸分析主要由编译器决定,目的是减轻GC压力,让GC更有效率。毕竟在方法执行完后出栈的操作会清空所占用的栈内存。
四、对象分配内存的过程:
- 根据常量池中确定的对象的大小size。
- 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入选项2。
- 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3。
- 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4。
- 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5。
- 执行一次Young GC(minor collection)。
- 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
参考资料: