详细的图又需要的,评论区留言,后面发给你
市面上虚拟机很多,虚拟机里面也有很多区域,但是为啥选虚拟机HotSpot和内存区域Java堆来举例,就像书的作者老师说的,因为常用呗,专挑熟柿子捏。
...
Dog dog = new Dog();
...
- 在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已。
- 在Java虚拟机中,普通对象(不包括数组和Class对象等)是这样的。
- 1.Java虚拟机遇到一个new指令后,先去检查这个new指令的参数是否能在常量池中定位到一个类的符号引用 (同时也会检查这个符号引用所代表的类是否已经被加载、解析和初始化过),别定位了了半天定位到了一个类的符号引用,你又给我说这个类的符号引用是个坏的用不了,不就炸了。
- 2.接着,如果类加载检查通过后虚拟机会为新生的对象dog分配内存(对象所需内存的大小在类加载完成就已经确定),或者说是从Java堆中划分一块确定大小的内存给你这个新生对象dog用呗
- 此时就需要考虑如何划分空间以及线程安全的问题等两个问题
- (如何划分空间)两种分配方式:
- 指针碰撞(Bump the Pointer):当Java堆中内存很规整用,比如
- 空闲列表(Free List):当Java堆中的内存并不是规整的用(已经使用的内存和空闲的内存)相互交错,那就没办法简单的进行指针碰撞了,虚拟机必须维护一个列表,这个列表用来记录哪些内存块是可用的,然后在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 指针碰撞(Bump the Pointer):当Java堆中内存很规整用,比如
- (如何划分空间)两种分配方式:
- 此时就需要考虑如何划分空间以及线程安全的问题等两个问题
分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾回收器是否带有压缩整理功能决定:
-
Serial、ParNew等带Compact过程的收集器时系统采用的分配算法是指针碰撞
-
使用CMS这种基于Mark-Sweep算法的收集器时通常采用空闲列表
- 线程安全:对象创建在虚拟机中非常频繁,其中哪怕是修改一个指针所指向的位置也会出现线程安全问题(比如正在给对象A分配内存,但是指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存)----有两种方法解决这个安全问题:
- 一种是对分配内存空间的动作进行同步处理(虚拟机采用CAS配上失败重试的方法以保证更新操作的原子性)
- 另一种是把内存分配的动作按照线程划分在不同的空间之中,也就是每个线程在Java堆中预先分配一小块内存(叫做本地线程分配缓冲(Thread Local Allocation Buffer,TLAB))。哪个线程要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。(虚拟机是否使用TLAB可以通过-XX:+/-UseTLAB参数来设定)
- 线程安全:对象创建在虚拟机中非常频繁,其中哪怕是修改一个指针所指向的位置也会出现线程安全问题(比如正在给对象A分配内存,但是指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存)----有两种方法解决这个安全问题:
-
3.对象的内存分配完之后,虚拟机需要将分配到的内存空间都初始化为零值(不包含对象头),这一步就保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,然后程序就能访问到这些字段的数据类型所对应的零值,比如狗头大小、狗体重、狗品种等的初始零值。
-
4.接着,虚拟机对对象的对象头中存放的信息进行必要的设置:
- 例如这个对象是哪个类的实例
- 如何才能找到类的元数据信息
- 对象的哈希码
- 对象的GC分代年龄等信息
上面四步完成后,
- 从虚拟机的视角来看一个dog对象已经产生了。
- 但是从Java程序的视角来看对象创建才刚刚开始----方法还没有执行,所有的字段都还为0。执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样才算是产生了一个真正可用的对象。
上面提到了,对象在内存中存储的布局可分为3块区域:
- 对象头(Header):与对象自身定义的数据无关
- HotSpot虚拟机的对象头包含两部分信息:
- 第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等(Mark Word)
- 另一部分是类型指针,也就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- HotSpot虚拟机的对象头包含两部分信息:
- 实例数据(Instance Data):对象真正存储的有效信息,也就是在程序代码中所定义的各种类型的字段内容,这部分内容的存储顺序会受到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义的顺序的影响。
- HotSpot虚拟机默认的分配策略(相同宽度的字段会被分配到一起)为longs/doubles、ints、shorts/chars、bytes/booleans、oops、
- 对齐填充(Padding):仅仅起着占位符的作用,HotSpotVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍(也就是说对象的大小必须是8字节的整数倍),对象头部分正好是8字节的倍数(1倍或者2倍),所以当对象实例数据部分没有对齐时就需要通过对齐填充来补全。
对象创建出来就是为了使用,我们Java程序一般是要通过Java虚拟机栈上的reference数据来操作堆上的具体对象,目前主流的访问方式(就是这个对象引用该通过哪种方式去定位或者说访问堆中的对象的具体位置呢)有两种:
- 使用句柄
- 此时Java堆中将会划分出一块内存来作为句柄池,reference(引用)中存储的就是对象的句柄地址(句柄中包含了对象的实例数据与类型数据各自的具体地址信息)
- 使用句柄的优点就是reference中存储的是稳定的句柄地址,对象被移动(垃圾回收时会经常移动对象)时只会改变句柄中的实例数据指针而reference本身不需要改变
- 使用直接指针
- 此时Java堆对象的布局中reference中存储的直接就是对象地址
- 优点就是速度快