堆的内存划分
整体划分为年轻代和老年代细分如下:
1. eden 空间 ------------------ 年轻代
2. from survivor 空间 -------- 年轻代
3. to survivor 空间 ------------ 年轻代
4. old generation - 老年代
创建的对象存放在哪里
当我们创建一个对象的时候,比如 new Student(); 这个可能存放在以下3个地方
-
eden 空间
这块区域是对象创建最活跃的区域,当我们一般新创建的对象都会在这 -
TLAB(Thread Local Allocation Buffer)
线程分配缓冲区,我们知道线程与线程之间是相互隔离的,每个线程各自拥有的数据随着线程结束而自动被回收,工作中遇到的绝大部分对象的创建是在方法上进行的方法调用结束其实这部分对象就没有用了,如果对象创建在eden空间,那么就得等到下次垃圾收集的时候进行回收,如果我们能在每个私有线程划分一块区域用来存放该线程方法中创建的对象,那么随着线程结束立即将对象销毁就会大大减少GC的负担。可以使用 -XX:+/-UseTLAB开启,JAVA8 虚拟机默认开启
需要注意的是- TLAB 是需要从堆中划分指定一部分区域的,比如100KB,那么以后每个线程访问的时候就会预先申请100KB
- 当申请的这TLAB 100KB不够用了,这个时候就需要再去向堆中申请一块内容来用
如果同时存在多个线程,那么久可能存在多个线程同时申请同一块内存,所以再次申请TLAB内存的时候需要进行同步
-
栈(线程逃逸分析技术)
由于线程逃逸技术的不断成熟,在堆上分配(TLAB本质也是使用堆内存)就变的不那么绝对了,如果能够确定线程调用方法栈帧中的对象确定不会被其它线程访问到(不存在线程共享问题)那么在栈上分配会变的更加高效因为会存在以下优点 — 这里做一个大致的介绍1. 随着栈结束对象自动销毁减少垃圾收集器的压力 2. 就可以取消对对象原本对的同步操作
虽然有很多优点SUN公司也实现了,但是现目前还不成熟
对象内存分配方式,是线程安全的吗?
通过上面的介绍我们了解到了,创建的对象会在哪一块内存区域,那么这块区域是如何划分出来的呢?如果多个线程创建对象会不会存在线程安全问题呢?下面来看看对象内存分配的具体方式主要有2种
- 指针碰撞: 假设我们的内存是规整的,用过的内存紧紧的挨在一起在左边,没有用过的内存在右边,那么在左边使用过的内存的尽头放一个指针,当新创建的对象需要分配内存的时候,往右边移动相应的大小区域就可以了
- 空闲列表(free list): 虚拟机会维护一个未使用过的内存列表,当对象需要分配内存的时候就划分一块足够大内存给它
由于当我们申请一块内存的时候,同时可能也会存在其它线程也在申请这块内存,所以这里需要做同步操作,如果使用TLAB那么只需要在TLAB内存不够给对象分配的时候再次去申请空间的时候需要进行同步这样效率会高很多
那么虚拟机根据什么情况来采用以上2中分配方式呢?这就和我们的堆内存是否规整有关了,如果是规整的就采用指针碰撞否则采用空闲列表
那么如何确定堆内存是否规整呢?这里就要大概介绍一下垃圾收集的3个算法
- 标记清楚法
进行GC的时候,首先会标记需要清除的对象,由于对象分布在各个地方,那么GC清除后,内存肯定就是不间断的也就是不规整的了。 - 复制算法
将内存划分为大小相等块,然后以每一小块为单位,划分出一个eden,2个 survivor ,创建对象分配内存的时候使用eden和其中一个survivor,当进行垃圾回收的时候一次性的把GC剩下的不规整的一个个的复制到另外一块survivor上去,然后清空掉原来使用的区域。这样内存就是规整的了。 - 标记整理法
这个步骤和标记清楚很想,区别是,标记了清除的对象后,将存活以及不存活的对象分别放在2端
然后清除掉标记的对象,这样内存也就是规整的了。
那么问题又来了,虚拟机GC时候,何时采用以上3中算法呢?这就跟我们采用什么垃圾收集器有关了,由于这里主要是在讨论堆就不详述了,大家可以去看下GC垃圾收集器和算法,我改天在写一篇文章进行讨论