JVM运行时数据区域及JVM中对象的创建及对象的内存布局
什么是线程私有?
由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们就称这类内存区域为“线程私有”的内存。
对象的创建:
当虚拟机遇到一条指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定,这里分两种情况:
1:java对中的内存是绝对规整的,所有用过的内存都放在一边,空闲的放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式成为“指针碰撞”。
2:如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,此时无法使用指针碰撞,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”。
具体选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带 Compact过程的收集器时,系统采用的分配算法是指针碰撞,而CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
除了划分空间外,还要考虑创建对象带来的线程安全问题,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用的CAS配上失败重试的方式保证了更新操作的原子性;
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设置。
内存分配完成后,虚拟机要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时,这样就保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。接下来虚拟机对对象进行必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息并存放在对象头之中;接着调用init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生了出来。
对象在内存中存储的布局可以分为3块区域:对象头 实例数据 对齐填充
对象头:包括2部分
第一部分:用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。这部分数据长度在32位到64位的虚拟机(未开启)压缩指针中分别为32bit和64bit,官方称为“Mark Word”。
第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据确定java对象的大小,但是从数组的元数据中却无法确定数组的大小。
实例数据:
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承选来的,还是在子类中定义的,都需要记录起来。
对齐填充:
不是必然存在,也没有特别含义,仅仅只起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头的部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位:
建立对象是为了使用对象,我们java程序需要通过栈上的reference数据来操作栈上的具体对象,由于reference类型
在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用该如何去定位,访问堆中的对象的具体位置,
所以对象访问方式也是取决于虚拟机实现而定的,主要分为句柄和直接指针:
句柄访问:
java堆中会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据
各自的具体地址信息。
图片引用自《深入JVM》
直接指针:java堆对象的布局中就必须考虑如何访问类型数据的相关信息,而reference中存储的直接就是对象地址。
句柄的好处:reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针的好处:速度快,节省了一次指针定位的时间开销。
各区域OutOfMemoryError异常:
堆:
java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,
那么在对象数量达到最大堆容量限制后就会产生内存溢出异常OutOfMemoryError:java heap space。
虚拟机栈和本地方法栈:
如线程请求的栈深度大于虚拟机所允许的最大深度,抛出*Error异常。
如果虚拟机在拓展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常。
方法区和运行时常量池:
应用中有很多CLASS的话,就很可能出现OutOfMemoryError:PermGen space。
JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代这个字符串实例的引用,而由
StringBuilder创建的字符串实例在java堆上所以必然不是同一个引用。
而JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次引用,因此intern()返回的引用和StringBuilder创建的那个字符串实例是同一个。(必须符合首次的原则才是同一个实例)