1.内存区域结构
方法区和堆 属于 共享内存 而剩下的 程序计数器、虚拟机栈、本地方法栈则属于线程私有的。下面一一解释这五大区域的作用。
-
堆:用于存放对象实例和数组
-
方法区:用于存储jvm加载的类信息(编译后的class文件的元信息)、运行时常量池(包括编译时生成的class文件的字面量和其符号引用,和运行时加入的常量)、JIT编译的代码等
class文件的元信息:元信息即是描述类的属性的信息,比如其字段、方法、接口、常量池等等信息
字面量:即具体的数据信息,比如:const int a = 1 其中a为基本数据类型的常量,1为字面量 ;同理 String b = “abc” ,b为变量,abc为字面量 -
jvm栈:栈描述的是Java方法执行的内存模型。每一个方法都会生成一个独立的栈帧,包含了方法的相关信息(如图)
-
Native栈:native方法指的是非java实现的方法,比如程序要与os交互,都是通过调用os的api来做的,而这些os的api大部分都是c实现的,他们会被封装成一个dll文件,然后在Java中用System.loadLibrary()方法加dll文件,这个native()方法就可以在Java中被访问了
2.对象创建
2.1 new 阶段
-
加载class:首先是在常量池中检查是否存在这个class的符号引用,若有则检查这个class是否被加载、解析和初始化过,如果没有则执行加载class的操作
-
分配内存:若堆中是连续规整的内存块则采取指针碰撞进行连续分配,若不规整则采取空闲列表法(可以参考OS的内存分配),其实这里的内存分配是和GC息息相关的,如果GC采取“标记-清扫”方式,则会产生许多碎片,如果GC采取“停止-复制”方式,则其会进行碎片整理,则可以采取连续分配。
-
防止并发:我们知道对象实例是在堆上创建的,而堆属于共享内存,在虚拟机上创建对象是非常频繁的行为,所以很有可能不同的线程在创建对象时可能会操作即将访问的同一片堆内存,因此要做到防止并发,有以下两种方式可实现:
1.堆分配内存空间的动作进行同步处理,实际上JVM采用CAS(Cmpare And Set)配上失败重试的方式保证更新操作的原子性
2.把内存分配的动作按照线程划分在不同的空间之中进行,即为每个线程在java堆中预先分配一块小内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)。分配内存时在线程的TLBA上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。JVM是否使用TLAB可以通过-XX:+UseTLAB参数来设定。 -
内存空间初始化:在分配完内存后,所有的内存空间都要初始化为零值(不包括对象头),所以实例字段在Java代码中不赋初值,仍可访问到其对应的零值。
-
设置对象头:前面完成了内存分配,虽然有了空间,但是这片空间还是没有具体的信息,所以这里就要在对象头中设置具体的信息,比如对应类的元数据,对象hash码、gc年代等。不同运行状态下的对象头设置方式不同:
这其中锁标识位需要特别关注下。锁标志位与是否为偏向锁对应到唯一的锁状态。锁的状态分为四种无锁状态、偏向锁、轻量级锁和重量级锁,不同状态时对象头的区间含义,如图所示——MarkWord。
对于不同锁状态的对象而言,其程序的执行并发程度也不同,所以在需要并发程序设计的时候,需要明确需求,设定不同的对象头锁状态。
2.2 init阶段
设置完对象头后,从JVM的角度来看一个对象已经完成了,但是从java程序的角度来看还没有创建完成呢。上面的new阶段只是分配了空间,设置了一些必要的元信息,但是实例对象的字段都为零值,此时就需要执行init方法,调用构造方法等过程,这样一个真正可用的对象才算完全的产生出来。
3.对象内存布局
对象在内存中的存储可以分为3块:对象头、实例数据、对齐填充。下面一一解释。
- 对象头:其实在上面已经说过了,对象头包含两部分数据,一部分是运行时元数据(MarkWord)(描述对象的hash码、GC代、偏向线程ID等信息),由于对象头是固定64或32bit大小的,而对象需要存储的运行时数据很多,所以根据不同的锁状态存储其必要的对象头信息;另一部分是类型指针,即对象所指的其类元数据的指针,通过这个指针,就可以知道这个对象是哪个类的实例。
- 实例数据:存储对象中的各类型的字段内容。无论是从父类继承来的还是在子类中定义的。
- 对齐填充:JVM要求对象的起始地址必须是8Byte的整数倍,所以当对象实例数据部分没有对齐时,进行对齐补全,保证是8Byte的整数倍长度。
4.对象访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。reference数据只是一个指向对象的引用,具体的对象访问根据不同虚拟机有不同的实现,主流的访问方式有两种:使用句柄和直接指针。
- 使用句柄访问:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息
- 使用直接指针访问(HotSpot采用):Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址
- 句柄来访问的最大好处:就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改
- 直接指针来访问的最大好处:就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本