运行时数据区之方法区和字符串常量池
一、运行时数据区概述
1.JVM运行时数据区规范
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ilNcqrmT-1628493578073)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\f242cfdb31d6edc52c2703def013e78.png)]
按照线程使用情况和职责分成俩大类:
-
线程独享(程序执行区域)
- 不需要垃圾回收
- 虚拟机栈、本地方法栈、程序计数器
-
线程共享(数据存储区域)
- 垃圾回收
- 存储类的静态数据和对象数据
- 堆和方法
2.分配JVM内存空间
分配堆的大小
-Xms (堆的初始容量)
-Xmx (堆的最大容量)
-XX:InitialHeapSize=268435456
-XX: MaxHeapSize=4294967269
#如果为了提高性能,可以考虑去浪费空间,就是将初始容量和最大容量相等
分配方法区的大小
-XX:PermiSize
永久代的初始容量
-XX:MaxPermisze
永久代的最大容量
-XX:MetaspaceSize
元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize
最大空间,默认是没有限制的。
除了上面俩个指定大小的选项以外,还有俩个与GC相关的属性:
-XX:MinMetaspaceFreeRatio
在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
分配线程空间的大小
-Xss:
为JVM启动的每个线程分配的内存大小,默认JDK1.4中是256k,JDK1.5+中是1M
二、方法区
1.方法区存储什么数据
类型信息,比如Class(com.test.User类)
方法信息,比如Method(方法名称,方法参数列表,方法返回值信息)
字段信息,比如Field(字段类型,字段名称需要特殊设置才能保存的住)
Code区,存储的是方法执行对应的字节码指令
方法表(方法调用的时候)在A类的main方法中调用B类的method1方法,是根据B类的方法表去查找合适的方法,进行调用的。
静态变量(类变量) ---JDK1.7知乎,转移到堆中存储
运行时常量池(字符串常量池) ---从class中的常量池加载而来---JDK1.7中后,转移到堆中存储
* 字面量类型
*双引号引起来的字符串值,比如"USER" ---- 会进入字符串常量池(StringPool)
*final修饰的变量
*非final修饰的变量,比如long,double,floa
*引用类型-->内存地址
*类的符合引用
*方法
*字段
JIT编译器编译之后的代码缓存
如果需要访问方法区中类的其他信息,都必须先获得Class对象,才能去访问该Class对象管理的方法信息或者字段信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQ9UVCcK-1628493578076)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\83bee7a499caa09e5d6ded5315cfe5a.png)]
1.1类型信息(重点)
- 类型的全限定名
- 超类的全限定名
- 直接超接口的全限定名
- 类型标志(该类是类类型还是接口类型)
- 类的访问描述符(public ,private,default,abstract,final,static)
1.2类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符合引用。
常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中报酬着所有类型使用到的类型、字段、方法的字符引用,所以它也是动态链接的主要对象(在动态连接中起到核心作用).
1.3字段信息(重点)
- 字段修饰符(public、protect、private、default);
- 字段的类型
- 字段名称
1.4方法信息(重点)
方法信息中包含类的所有方法,每个方法包含以下信息:
- 方法修饰符
- 方法返回类型
- 方法名
- 方法参数个数、类型、顺序等
- 方法字节码
- 操作数栈和该方法在栈帧中的局部变量区大小
- 异常表
1.5 类变量(重点)
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。他们与类进行绑定。
1.6指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到
1.7指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
1.8方法表(重点)
为了提高访问效率,JVM可能会对每个装置的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。
1.9运行时常量池
(Runtime Constant Pool)
class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面常量和符合引用,这部分内容被类加载后进入方法去的运行时常量池中存放。
运行时常量池相对于class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池中(典型的如String类的intern()方法).
永久代和元空间的区别是什么?
- JDK1.8之前使用的方法区实现是永久代,JDK1.8及以后使用的方法区实现是元空间。
- 存储位置不同,永久代所使用的内存区域是JVM进程所使用的区域,它的大小受整个JVM的大小所限制。元空间所使用的内存区域是物理内存区域。那么元空间的使用大小只会受物理内存大小的限制。
- 存储内容不同,永久代存储的信息基本上就是上面方法区存储内容中的数据。元空间只存储类的元信息,而静态变量和运行时常量池都挪到堆中。
3.为什么要使用元空间来替换永久代?
- 字符串存在永久代中,容易出现性能问题和永久代内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为GC带来不必要的复杂度,并且回收效率偏低。
- Oracle计划将HotSpot与JRockit合二为一
结论
其实,移除永久代的工作从JDK1.7就开始了。
JDK1.7中,存储在永久代的部分数据就已经转移到Java Heap.
但永久代仍存在于JDK1.7中,并没完全移除,譬如字面量(interned strings)转移到java heap;类的静态变量(class statics)转移到了java heap.
4.方法区异常演示
4.1.类加载导致OOM异常
1)案例代码
我们现在通过动态生成类来模拟方法区的内存溢出:
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new
ArrayList<ClassLoader>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new
URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.kkb.test.memory.Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
2)JDK1.7分析
指定的PermGen区的大小为8M:
最典型的场景就是,在jsp页面比较多的情况,容易出现永久代内存溢出。
3)JDK1.8+分析
现在我们在JDK8下重新运行一下案例代码,不过这次不在指定PermSize和MaxPermiSize.而是指定MetaSpaceSize和MaxMetaSpaceSize的大小。输出结果如下:
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。
4.2字符串OOM异常
1)案例代码
一下这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存:
public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
JDK1.6
JDK1.6的运行结果:
在JDK1.6下,会出现永久代的内存溢出。
3)JDK1.7在JDK1.7中,
会出现堆内存溢出。结论是:JDK1.7已经将字符串常量由永久代转移到堆中。
4)JDK1.8+
在JDK1.8中,也会出现堆内存溢出,并且显示JDK1.8中PermSize和MaxPermGen.因此,可以验证JDK1.8中已经不存在永久代的结论。
字符串常量池
1.三种常量池区别
class常量池(静态常量池)
-
字面量
- 数值型 int,float,long,double等
- 双引号引起来的字符串值
-
符号引用
- Class,Method,Field等
一个Class文件中只有一个class常量池
JVM运行时数据区
-
运行时常量池
-
字面量
- 数值型:int,float,long,double等
-
符号引用
-
Class,Method Field等
一个Class对象,一个运行时常量池
-
-
-
字符串常量池
- 双引号引起来的字符串值
全局只有一个字符串常量池
2.字符串常量池中如何存储数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0vcxxDf-1628493578077)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\9e84eeafd6c925494378cb01bb2acb4.png)]
字符串常量池查找字符串的方式:
-
根据字符串的hashcode找到对应entry.
-
如果没冲突,它可能只是一个entry.
-
如果有冲突,它可能是一个entry链表,然后java再遍历entry链表,匹配引用对应的字符串。
-
如果找得到字符串,返回引用。
-
如果找不到字符串,在使用intern方法的时候,会将intern方法调用者的引用放入stringtable中
为了提高匹配速度,即更快的查找某个字符串是否存在与常量池,Java在设计字符串常量池的时候,还搞了一张【stringtable】,stringtable有的类似与我们的hashtable,里面保存了【字符串的引用】.
hashtable中数据存储到数组中的位置是通过以下计算得来的:
- hash(字符串值)/数组长度=余数(数组下标)
- 数组中存储的元素都是一个Entry对象(next指针)
- Entry通过next指针可以形成链表。
hashtable会存在俩种问题:hash冲突问题,rehash问题
hash冲突问题:链地址法,也就是使用链表
一旦导致hash冲突之后,就会形成链表
链表是增删快,查找慢。
在jdk7+,‘StringTable’的长度可以通过一个参数指定:-XX:StringTableSize=99991
字符串常量池案例分析
public class Test {
public void test() {
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2);
String str3 = new String("abc");
System.out.println(str3 == str2);
String str4 = "a" + "b";
System.out.println(str4 == "ab");
final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab");
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());
}
}
结论:
1.单独使用“”引号创建的字符串都是常量,编译器就已经确定存储到StringPoo中。
2.使用new String("")创建的对象会存储到heap中,是运行期新创建的。
3.使用只包含常量的字符串连接符如“aa"+"bb"创建的也是常量,编译期就能确定已经存储到String Pool中。
4.使用包含变量的字符串连接如“aa"+s创建的对象是运行期才创建的,存储到heap中
5.运行期调用String的intern()方法可以向String Pool中动态添加对象。
String的Intern方法详解
4.1.intern的作用
- 返回stringtable中对应字符串对象的引用值
- 如果stringtable中没有对应字符串对象的一条记录,则动态添加字符串对象到stringtable中。
4.2 intern方法的好处
测试案例:
import java.util.Random;
public class TestIntern {
//字符串数组的长度
static final int MAX = 1000 * 1000;
//字符串数组
static final String[] arr = new String[MAX];
public static void main(String[] args) {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
//产生10个随机数,放入DB_DATA数组中保存
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
arr[i] = new String(String.valueOf(DB_DATA[i %
DB_DATA.length])).intern();
}
System.out.println(System.currentTimeMillis() - t);
System.gc();
}
}
以上陈旭回应很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过intern显示的将其加入常量池,这样可以减少很多字符串的重复创建。
JDK6中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过JDK7将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用;
String.intern()方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行笔记操作时减少时耗,提高程序性能。
4.3intern案例分析
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
四、Java堆
堆内存分配
堆内存划分
- 堆(Heap)线程共享(-Xms -Xmx)
- 新生代(Young 1/3) -Xmn Minor GC
- Eden(8/10)
- Survivor From(1/10)
- Survivor To(1/10)
- 老年代 Tenured(2/3) Major GC
- 新生代(Young 1/3) -Xmn Minor GC
Java堆在JVM启动时拆改那就,是通过【垃圾收集器】去实现内存分配。它是虚拟机管理最大的一块内存。也是垃圾回收的主要区域,而且主要采用【分代回收算法】。
堆空间的参数设置
-XX:+PintFlagsInitial:查看所有参数的默认初始值
-XX:+PrintFlagsFinal:查看最终值(初始值可能被修改掉)
-Xms:初始堆空间内存(默认为物理内存的1/64)
-Xms:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden和SO/S1空间的比例
-XX:MaxTenuringThreshold设置新生代垃圾的最大年龄
-XX:+PrintGCDetails:输出详细的GC处理日志 打印GC简要信息:
-XX:PrintGc -verbose:gc
-XX:HandlePromotionFailure:是否设置空间担保。
通过工具查看堆内存信息
1.jvisualvm工具
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmsgkZ47-1628493578079)(E:\笔记\3e6089d2388e94f68488ea4150d4dcf.png)]
2.VM options -XX:+PrintGCDetails
/**
* -Xmx600m -Xms600m -XX:+PrintGCDetails
* @param args
*/
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<Picture>();
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024*1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels = new byte[length];
}
}
停止进程即打印日志
Heap
PSYoungGen total 2560K, used 1498K
[0x00000007bfd00000, 0x00000007c0000000,
0x00000007c0000000)
eden space 2048K, 73% used
[0x00000007bfd00000,0x00000007bfe76bb8,0x00000007bff00000)
from space 512K, 0% used
[0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
to space 512K, 0% used
[0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
ParOldGen total 7168K, used 0K [0x00000007bf600000,
0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 0% used
[0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)
Metaspace used 2713K, capacity 4486K, committed
4864K, reserved 1056768K
class space used 289K, capacity 386K, committed 512K,
reserved 1048576K
3.jstat命令
- jps -l | grep 'HeapDemo'
52089 com.HeapDemo
- jstat -gc 52089
S0C S1C S0U S1U EC EU OC
OU MC MU CCSC CCSU YGC YGCT FGC
FGCT GCT
512.0 512.0 0.0 483.9 2048.0 59.9 7168.0
464.0 4864.0 3202.6 512.0 349.4 1 0.006 0
0.000 0.00
- SOC:第一个幸存区的大小
- S1C:第二个幸存区的大小
- SOU:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩卷空间大小
- CCSU:压缩机空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT: 垃圾回收消耗总时间
4.jmap命令
➜ ~ jmap -heap 52089
Attaching to process ID 52089, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 10485760 (10.0MB)
NewSize = 3145728 (3.0MB)
MaxNewSize = 3145728 (3.0MB)
OldSize = 7340032 (7.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 2097152 (2.0MB)
used = 61336 (0.05849456787109375MB)
free = 2035816 (1.9415054321289062MB)
2.9247283935546875% used
From Space:
capacity = 524288 (0.5MB)
used = 495544 (0.47258758544921875MB)
free = 28744 (0.02741241455078125MB)
94.51751708984375% used
To Space:
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
PS Old Generation
capacity = 7340032 (7.0MB)
used = 475168 (0.453155517578125MB)
free = 6864864 (6.546844482421875MB)
6.473650251116071% used
2143 interned Strings occupying 151616 bytes
演示
package example;
/**
* 1.设置堆空间大小的参数
* -X是jvm的运行参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -Xms 用来设置堆空间(年轻代+老年代)的最大内存大小
* 2.手动设置 -Xms600m -Xmx600m
* 开发中建议将初始化堆内存和最大的堆内存设置成相同的值
* 3.查看设置的参数: 方式一: jps / jstat -gc 进程id
* 方式二: -XX:+PrintGCDetails
* 4.默认值
* 初始内存大小:物理电脑内存大小 /64; 最大内存大小:物理电脑内存大小 /4
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
long initMemory = Runtime.getRuntime().totalMemory()/1024/1024;
long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;
System.out.println("-Xms:"+initMemory+" M");
System.out.println("-Xmx:"+maxMemory+" M");
System.out.println("系统内存⼤⼩:"+initMemory*64.0/1024+" G");
System.out.println("系统内存⼤⼩:"+maxMemory*4.0/1024+" G");
}
}
对象内存分配
对象内存的分配原则
序号 | 介绍 |
---|---|
1 | 优先在Eden分配,如果Eden空间不足虚拟机则会进行溢出MinorGC |
2 | [大对象]直接进入老年代,【大对象】一般指的是【很长的字符串或数组】,默认是0 |
3 | 【长期存活的对象】也会进入老年代,每个对象都有一个【age】,当age到达设定的年龄的时候就会进入老年代,默认是15岁。 |
对象内存分配方式
内存分配的方法有俩种:指针碰撞(Bump the Pointer)和空闲列表(Free List)
分配方法 | 说明 | 收集器 |
---|---|---|
指针碰撞 | 内存地址是连续的(年轻代) | Serial和ParNew收集器 |
空闲列表 | 内存地址不连续(年老代) | CMS收集器和Mark-Sweep收集器 |
对象内存分配安全问题
在分配内存的时候,虚拟机给A线程分配内存过程中,指针未修改。此时B线程同时使用了同样一块内存。
在JVM中有俩种解决办法:
-
CAS是乐观锁的一种实现方式。虚拟机采用CAS配上失败重试的方法保证更新操作的原子性。
-
TLAB,本地线程分配缓冲(Thread Local Allocation Buffer即TLAB):为每一个线程预先分配一块内存
JVM在第一次给线程中的对象分配内存时,首先使用CAS进行TLAB的分配。 当对象大于TLAB张宏的剩余内存或TLAB的内存已用尽时,在采用上述的CAS进行内存分配
对象内存分配担保(老年代)
在新生代无法分配内存的时候,我们想把新生代的对象转移到老生代,
然后把新对象放入腾空的新生代。此时就需要内存担保机制。
JVM参数:-Xms20M、-Xmx30M、-Xmn10M
分配三个2MB的对象和一个4MB的对象。
代码如下:
package example;
/**
* 内存分配担保案例
*/
public class MemoryAllocationGuarantee {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
memoryAllocation();
}
public static void memoryAllocation() {
byte[] allocation1, allocation2, allocation3,
allocation4;
allocation1 = new byte[2 * _1MB];//2M
allocation2 = new byte[2 * _1MB];//2M
allocation3 = new byte[2 * _1MB];//2M
allocation4 = new byte[4 * _1MB];//4M
// allocation4 = new byte[5 * _1MB];//4M
// allocation4 = new byte[3 * _1MB];//4M
System.out.println("完毕");
}
}
堆内存分配情况如下:
假设:新生代总可用空间为9216KB
eden space(8192K)
Survivor区 from space(1024K) to space(1024K)
串行垃圾收集器案例
设置JVM参数:
-Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
担保机制在JDK1.5以及之前的版本中是默认关闭的,需要通过HandlePromotionFailure手动指定,JDK1.6之后就默认开启。这里我们使用的是JDK1.8,所以不用在手动去开启担保机制。
串行垃圾收集器案例结论
- 当Eden去存储不下新分配的对象时,会触发minorGC
- GC之后,还存活的对象,按照正常逻辑,需要存入到Survivor区(幸存区)。
- 当无法存入到幸存区时,此时会触发担保机制
- 发生内存担保时,需要将Eden区GC之后还存活的对象放入老年代。后来的新对象或者数组放入Eden区
并行垃圾收集器案例
设置JVM参数:
-Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseParalleGC
修改GC组合为(Paralle Scavenge+Serial Old的组合)
发现当我没使用ParalleGC收集器组合(Parallel Scavenge+Serial Old的组合)下,担保机制的实现和之前的Client模式下(SerialGc收集器组合)有所变化。
注意点:
在GC前还会进行一次判断,如果要分配的内存>=Eden区大小的一半,那么会直接把要分配的内存放入老年代中。否则会进入担保机制。
对象创建与访问
对象创建
Student stu = new Student();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZ66Z09f-1628493578080)(E:\笔记\4d65fe3f98f8c7458916a057c163c65.png)]
对象的内存布局
对象在内存中存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对其填充(Padding).
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YkluCQMr-1628493578082)(E:\笔记\35bc01a610b70454839d916b434a2bd.png)]
1)对象头
对象头包括俩部分信息:
一部分是用于存储对象自身的运行数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。当对象是一个java数组的时候,那么对象头还必须有一块用于记录数组长度的数据,,因此虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。
2)实例数据
存储的是对象真正有效的信息。
3)对齐填充
这部分并不是必须要存在的,没有特别的含义,在jvm中对象的大小必须是8字节的整数倍,而对象头也是8字节的倍数,当对象实例数据部分没有对齐时,就需要通过对其填充来补全。