目录
对象的创建过程
- class loading到内存
- class linking (检查、准备、解析)
- class initiailizing
- 申请对象的内存
- 成员变量赋默认值
- 调用构造方法init<>
-
- 成员变量顺序赋初始值
-
- 执行构造方法语句
-
对象在内存中的存储布局
- C:\Users\tudeen123>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=130405696 -XX:MaxHeapSize=2086491136 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops(对象内存布局有关系) -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version “1.8.0_341”
Java™ SE Runtime Environment (build 1.8.0_341-b10)
Java HotSpot™ 64-Bit Server VM (build 25.341-b10, mixed mode)
markword与class pointer 两个共同构造了对象头 ObjectHeader。 - 普通对象
- 普通对象首先有一个对象头,这个对象头在jvm或者Host pot 里面叫
markword长度8个字节
- 第二个叫ClassPointor ,一个class指针,一个对象通过C语言new出来之后,这个对象是属于那个class的是通过该指针进行指向。如果我们newT,那么这个就是T.class类型(-XX:+UserCompressedClassPointers 为4字节,不开启为8字节)
- 实例数据(就是成员变量)
- 引用类型: -XX: + UserCompressedOps为4字节,不开启8字节
- Oops Oedinary Object Pointers
- Padding对齐(4个字节,由于计算机时64位的,那就是8字节对齐,如果整个字节数不能被8整除,后面补齐为8的倍数提高效率)
- 主要是因为算出来正好是15个字节,但是作为64位的机器来讲,读的时候并不是按你多少个字节来读,而是按块来读,读16(8的倍数)个效率更高 (8的倍数效率更高)由于对象需要为8的整数倍,Padding会补充4个字节,总共占用16字节的存储空间。
- 普通对象首先有一个对象头,这个对象头在jvm或者Host pot 里面叫
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
</dependencies>
package jvm_01;
import org.openjdk.jol.info.ClassLayout;
public class test01 {
public static void main(String[] args) {
Object o = new Object();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 对象头markword
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 对象头markword
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) ClassPointers
12 4 (loss due to the next object alignment) 由于下一个对象对齐造成的损失 对齐
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 数组对象比普通对象多
- 对象头markword
- classPointers 同上
- 数组的长度 4字节
- 数组数据
- 对齐
对象头包括什么?
- markword 主要包括3个内容
- 锁信息
- GC信息
- HashCode 最原始本质的HashCode(Identity HashCode),不是重写的HashCode
对象内存布局java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
上锁在打印java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 40 f3 ff 4b (01000000 11110011 11111111 01001011) (1275065152)
4 4 (object header) 78 00 00 00 (01111000 00000000 00000000 00000000) (120)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
这就是上锁之后的区别,所以给对象上锁就是修改对象的markword ,可以去Hospot源码去看看
- class pointer 指向某一个class类型对象 默认4个字节不开启8字节
java -XX:+PrintCommandLineFlags -version cmd命令 把虚拟机启动的默认参数打印出来
-XX:InitialHeapSize=130405696 初始化的堆大小
-XX:MaxHeapSize=2086491136 堆最大占多少内存
-XX:+PrintCommandLineFlags 打印自己设置的JVM参数
-XX:+UseCompressedClassPointers 指针ClassPointers相关的,默认的指针压缩打开,64为虚拟机指针长度为64为8个字节这里的指针是经过压缩的压缩到四个字节。
当内存膨胀到32G,这里的压缩不在起作用,会膨胀到8个字节
CPU三大总线
总线分为三种:地址总线,数据总线,控制总线。
(1)地址总线的多少表示CPU可以一次对多少个内存单元进行寻址。
8080,8088,8086,80286,80386的地址总线宽度分别为:16根,20根,20根,24根,32根。
(2)数据总线的多少表示一次可以传送的数据,一般为8的倍数。
8080,8088,8086,80286,80386的数据总线宽度分别为:8根,8根,16根,16根,32根。
(3)控制总线表示CPU对系统中其他器件的控制能力。有多少根控制总线,就意味这CPU提供了对外部器件的多少种控制。
8080,8088,8086,80186,80188,80286,80386的控制总线宽度分别为:8根,16根,16根,16根,16根,32根。
一般来说,64位的机器地址总线的宽度应该为64位,但是大多数的总线寻不了这么高的地址,多数的计算机的总线宽度为48,这就导致过了32G之后总线的宽度不支持了,必须膨胀到8个字节
-XX:+UseCompressedOops(对象内存布局有关系)
这里有个String类型是引用类型,正常应该占用8个字节,计算机是64位的,-XX:+UseCompressedOops
也是默认开启的。oops指的是Oops Oedinary Object Pointers 。指的是普通的对象指针。
class指针与普通对象指针是分开的,class指针是指向我们class的指针,oops是指向成员变量的指针String name;压缩的,压缩完为4字节
-XX:-UseLargePagesIndividualAllocation
-XX:+PrintGCDetails 打印包括新生代(Eden、from、to)和老年代以及元空间的信息
-XX:+UseParallelGC 使用垃圾收集器
java version “1.8.0_341”
Java™ SE Runtime Environment (build 1.8.0_341-b10)
Java HotSpot™ 64-Bit Server VM (build 25.341-b10, mixed mode)
对象怎么进行定位?
- 句柄方式 t指向的一组指针 实例数据指针(指向真正的对象)与类型数据指针(指向类型的数据指针在指向方法区t.class)
- 直接指针 Hospot虚拟机用的这种。 是通过直接引用找到new出来的对象
优缺点:
- 直接指针效率比较高
- 句柄方式主要方便GC,GC再进行复制的时候,这里的t不需要变,因此方便GC回收
对象怎么分配?
- 当我们开始分配对象的时候优先分配到栈空间
- 不存在逃逸(这个对象就在方法里面存活没有别的引用指向)的对象直接分配到栈上,有个好处不需要GC的介入这就弹出。效率非常高 Xss最大栈的大小 扩大可以调优
在方法体中创建对象,如果该对象被方法体其他变量引用到,叫方法逃逸,被外部线程访问到叫线程逃逸。
JVM如果开启了逃逸分析,JIT会对代码进行如下优化:
【一】同步省略(锁消除),没有逃逸,则直接消除锁。
【二】将堆分配转化为栈分配,栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后栈自动销毁,而不需要垃圾回收的介入,从而提高系统性能
。
【三】分离对象或标量替换,原始数据类型(int,long等数值类型以及reference类型等)不能再进一步分解,称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化, 可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了
。
- 逃逸分析开关参数:
-XX:+DoEscapeAnalysis :表示开启逃逸分析
-XX:-DoEscapeAnalysis :表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis - 锁消除开关参数:
-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析
- 如果再栈上不能够进行分配,先判断这个对象大不大,如果很大,先放到老年代里面区去,老年代经过Full GC或者Major GC这个对象才会别回收
Minor GC
和Young GC
,“新生代”也可以称之为“年轻代”, 这两个名词是等价的。那么在年轻代中的Eden内存区域被占满之后,实际上就需要触发年轻代的gc,或者是新生代的 gc。
老年代gc,称之为“Old GC
”。
Major GC
:可以是指 old GC 也可以是指 Full GC。这是因为JVM规范没有对这些名词有具体的定义,时间久了后就使用混乱了。所以如果讨论 Major GC 的时候需要指定前提到底是讨论的是 old GC 还是 Full GC,比如周志明版的《深入理解虚拟机》就把 old GC 统称为 【Major GC】,这里我们先按照old GC == Major GC
讨论
Mixed GC
是G1中特有的概念,其实说白了,主要就是说在G1中,一旦老年代占据堆内存的45%了,就要触发Mixed GC,此时对年轻代和老年代都会进行回收。对应的参数设置:“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%,可以进行按需要修改。
对于Full GC
指的是针对新生代、老年代、永久代的全体内存空间的 垃圾回收,所以称之为Full GC。
从字面意思上也可以理解,“Full”就是整体的意思,所以就是对JVM进行一次整体的垃圾回收,把各个内存区域的垃圾都回收掉。
换句话说,Full GC就是针对JVM内所有内存区域的一次整体垃圾回收。
- 这个对象不大,并且是逃逸的,会先分配到线程本地缓冲区Thread Local Allocation Buffer TLAB ,可以配置虚拟机参数
(例如:现在又T1、T2、T3线程再虚拟机启动之后,他们都会往伊甸区里面分配对象,往同一个位置分配对象,他们三个去抢,去碰撞指针,大大浪费资源)。
堆内存被一个指针一分为二,指针的左边都被塞满了对象,指针的右边是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。
已用区域会随着垃圾收集,出现大量的碎片,造成内存不连续。解决的思路是建立一个FreeList,把释放的区域加入到FreeList中,下次分配对象的时候,优先从 FreeList 中寻找合适的内存大小进行分配,之后再在主内存中撞针分配。
堆中分配,多线程下有并发问题,方式时通过加锁、指针碰撞。加锁肯定性能不高。那么指针碰撞,在多线程下频繁的创建对象,CAS频繁的重试更新,性能也不高。所以再每个线程内部加一个小小的空间就位于伊甸区,专属于T1、T2、T3,当自己的线程要分配对象的时候优先往自己TLAB里面进行分配。通过TLAB(Thread Local Allocation Buffer),是在Hotspot1.6引入的新技术。在线程启动时,在堆的Eden区域申请一块指定大小的内存,线程私有,如果线程内需要创建对象,则在此区域内创建,避免并发,提升性能
。包含start、top(归属线程最后一次申请的尾位置)、end。如下图所示:
- TLAB分配方式
首先都会检查是否启用了 TLAB,如果启用了,则会尝试 TLAB 分配;如果当前线程的 TLAB 大小足够,那么从线程当前的 TLAB 中分配;如果不够,但是当前 TLAB 剩余空间小于最大浪费空间限制(这是一个动态的值,我们后面会详细分析),则从堆上(一般是 Eden 区) 重新申请一个新的 TLAB 进行分配。否则,直接在 TLAB 外进行分配。TLAB 外的分配策略,不同的 GC 算法不同。例如G1:
如果是 Humongous 对象(对象在超过 Region 一半大小的时候),直接在 Humongous 区域分配(老年代的连续区域)
。
根据 Mutator 状况在当前分配下标的 Region 内分配。
- 这个对象什么时候被回收呢,经历过一次GC的清除,如果YGC被回收了就结束了,如果没清除掉就复制到Surivivor区,再经过垃圾回收如果没有清除掉一直重复操作直到它的年龄足够大的时候到达老年代,old区经过FGC进行结束。
Object o = new Object()到底占用多少个字节?
在开启指针压缩的情况下,markword占用8字节,classpoint占用4字节,Interface data无数据,总共是12字节,由于对象需要为8的整数倍,Padding会补充4个字节,总共占用16字节的存储空间。
在没有指针的情况下,markword占用8字节,classpoint占用8字节,Interface data无数据,总共是16字节。
为什么hospot不使用C++对象代表Java对象?
java对象模型 Oop-Kclass 模型
- Oop-Kclass 模型组成
OOP 英文全程是Ordinary Object Pointer,即普通对象指针,看起来像个指针实际上是藏在指针里的对象,表示对象的实例信息
Kclass 元数据和方法信息(类的继承,成员变量,静态变量,成员方法等等),用来描述 Java。是Java类的在C++中的表示形式,用来描述Java类的信息
JVM内部基于 oop-kclass 模型描述一个Java类,一个是 oop, 一个是 kclass
如果你想要new一个新的对象再Hospot内部实际上会生成两个结构 - 一个是生成的对象
- 一个是生成的class
每一个C++对象里面都有一个虚函数表,如果用 C++对象代表Java对象,导致每个Java对象占的空间比较大,因为需要加个虚函数表
Class对象是在队还是在方法区?
class对象就我们的class pointer 指向的T.class,这个T.class就是class对象也是class实例
MethodArea方法区是一个逻辑概念
PermGen永久代与MetaSpace元空间是方法区的具体实现
当我们new一个Object对象的时候,这里有个指针指向Object.class对象,实际上是指向对应的C++对象,在C++对象内部有指回到堆空间,这是1.8的实现,在1.8里面,class实例位于堆里面,为什么放在对里面方便我们反射使用。