目录
- 一、JVM内存分区
- 1.程序计数器
- 2.栈
- 3.堆
- 4.方法区(元空间)
- 5.字符串常量池
- 二、对象创建过程
- 1.类加载检查
- 2.分配内存
- 3.初始化0值
- 4.设置对象头
- 5.执行init()方法(构造方法)
一、JVM内存分区
Java虚拟机(Java Virtual Machine, JVM)是在运行阶段执行Java字节码(.class)的虚拟操作系统。引入JVM是为了实现了对象内存的自动分配和回收,不用向C++一样手动分配和回收内存,编码更简单,但有得有失,由于引入了JVM,编译阶段无法直接编译成指令序列,而是运行阶段实时编译,运行速度相较于编译型语言慢。
JVM在运行过程中会把它管理的内存划分成若干个不同的数据区域:
1.程序计数器
CPU对于指令是顺序执行的,每次读取内存空间中的一部分数据(指令)执行,程序计数器就是下一条要执行的指令的内存地址,所以每个线程(CPU)都有自己的程序计数器。
线程之间的切换可能当前线程并未执行完,那么就要保存程序计数器的内容,等到线程再次获得CPU后恢复程序计数器现场,实现线程从上次中断的位置继续执行。
2.栈
当调用start()方法时开启新的线程,JVM就会为新的线程分配独立栈空间,并在栈空间中执行run()方法。当run()方法执行完毕,线程被销毁时栈空间也会被释放。
Java 线程
每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法执行完(正常/异常)栈帧被弹出,每个栈帧中都拥有:
- 局部变量表:存放数据、对象引用。
- 操作数栈:用于存放方法执行过程中产生的中间计算结果、临时变量。
- 动态链接:首先需要明确,所有方法都保存在方法区中,而要调用这些方法,就需要拿到方法在常量池中的符号引用才能定位到方法区的方法代码执行,由于多态的存在(父类引用指向一个子类对象),编译阶段javac无法确定引用执行的是父类的方法还是子类的方法,就需要动态链接确认要执行的方法并指向常量池中的符号引用。
3.堆
堆是所有线程共享的一块内存区域,用于存放对象实例,因此是垃圾回收器管理的主要区域。
堆又细分为多个空间,目的是更有效的分配和回收内存。默认首先在Eden为对象分配空间,在垃圾回收器执行后,如果该对象还存在那么将对象转移到S0、S1,当对象用于记录年龄的4bit=1111时会进入Tenured。
4.方法区(元空间)
线程共享的内存区域,JVM加载类时会从Class文件中解析类的结果、每个方法的指令序列存储到方法区。同时在字符串常量池中保存每个类、每个方法的符号引用。
5.字符串常量池
字符串常量池位于堆空间中,是JVM为了减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免引用类型String对象的重复创建。
- 每一个唯一字符串会在常量池中创建对应的对象,堆中String对象存储常量池中对象的内存地址,并返回给引用。
String s1="abcdef";会直接将常量池中的字符串对象赋值给当前引用,不会再堆中实例化额外对象,如果常量池中没有"abcdef"则会在常量池中创建字符串对象。String s2 = new String("xy");会首先在堆空间实例化对象并指向常量池中的对象,然后返回给当前引用,如果常量池中没有"xy"则会在常量池中创建新的字符串对象。
以下代码创建了几个对象?
String s1 = new String("abc"); String s2 = new String("abc");答案:3个。字符串常量池中创建一个“abc”, 堆内存中有两个String 对象。
二、对象创建过程
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中找到符号引用,并且检查这个符号引用代表的类是否已加载到方法区,如果没有,那必须先执行相应的类加载过程。
2.分配内存
类加载后,JVM从堆中划分一块内存空间给当前对象。分配方式有两种:
- 指针碰撞:已分配的内存全部整合到一边,空闲内存放在另一边,中间有一个分界指针,向空闲内存方向移动指针即可划分一块内存空间。
- 空闲列表:JVM维护一个列表,该列表中会记录哪些内存块是可用的,从列表中找一块儿足够大的内存块划分给对象,最后更新列表记录。
内存分配的并发问题:分配内存的过程由于多处理器也会产生并发问题,JVM的思路是首先为每个线程预分配一块堆空间,线程操作自己的堆空间不会有并发问题,当分配的空间不够了,使用CAS+自旋保证内存分配的原子性。
3.初始化0值
内存分配完成后,JVM将分配到的内存空间内容都初始化为0,这一步是为了保证将原来使用这片内存空间的对象数据清空,防止脏读。
4.设置对象头
JVM对对象进行必要的设置:
- 标记字段(Mark Word):对象的锁状态标志、线程持有的锁、4bit对象年龄(熬过一次GC年龄+1)。
- 类型指针(Klass pointer):指向类定义在方法区存放的位置。
5.执行init()方法(构造方法)
设置成员变量的初始值。