JVM系列之运行时数据区(二)

本文详细介绍了Java运行时数据区的各个组成部分,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区(包括运行时常量池)。每个区域的作用、内存分配、异常情况以及对象创建流程等关键知识点都有深入阐述。
摘要由CSDN通过智能技术生成

运行时数据区

程序计数器

是一块较小的内存空间,线程私有,互不影响,独立存储,生命周期随着线程的启动开始,线程的销毁结束;一个时间点上,一个处理器都只能执行一个线程中的指令,程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复都会用到,也是唯一没有OOM的地方;

如果执行的是java方法,则存储当前线程所执行的字节码的行号指示器,通过改变计数器选取下一条需要执行的字节码指令;

如果执行的是本地方法,则为空undefined;

程序计数器的值是字节码执行引擎进行修改的。

存在的意义:当线程被挂起后,要再执行的话,需要用到程序计数器的值来进行定位,定位继续从哪行代码开始执行。

虚拟机栈

线程私有,生命周期与线程相同,用来描述Java方法执行的线程内存模型;栈内部由栈帧组成,一个方法对应一个栈帧内存区域,每个方法执行时,Java虚拟机都会同步创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完毕的过程,就是一个栈帧在虚拟机栈中入栈出栈的过程。

java运行的时候,每一个主线程某个方法,该方法存在局部变量,因此会从内存中取一小块栈区域,到该主线程下;

栈帧中放着该方法的局部变量,如果该方法执行完之后,这部分内存会释放掉,放入的过程叫入栈,销毁的过程叫出栈。

java虚拟机中采用先进后出原则(FILO),先调用的方法先分配内存,后调用的方法后分配内存,因为执行从上向下,因此,先调用的方法会后结束,后调用的方法会先结束。

异常:

  1. 请求的栈深度大于所允许的栈深度
  2. 动态扩展时无法申请到足够的空间,Hotspot不会动态扩展

局部变量表

存放该方法的局部变量(存放编译期可知的Java基本数据类型和对象引用类型,并不是对象,而是指向对象起始地址的引用指针)。

数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。

局部变量表所需空间在编译时就已经确定,进入一个方法,该方法所需的局部变量表空间是完全确定的,运行期间不会改变槽的数量。

局部变量:放的是对象的地址,可以根据内存地址,指向堆中的对象,而局部变量在栈中,相当于栈指向堆

操作数栈

是程序运行过程中,临时存放运算时的常量

动态链接

因为方法是放在方法区中的,所以会有一个关于方法区的地址,而动态链接这块存的就是方法区对应方法的地址

方法出口

将上一句调用的方法的信息保存到方法出口中,方法执行完之后,知道应该返回main方法的哪一行代码继续执行

本地方法栈

有native修饰的方法叫本地方法,线程私有,与虚拟机栈作用相似,区别在于虚拟机栈为Java方法(字节码)服务,本地方法栈为虚拟机用到的本地(Native)方法服务。Hotspot将虚拟机栈和本地方法栈合二为一,与虚拟机栈一样。

private native void start0();

线程中调用了本地方法的话,会从本地方法栈中取一小块空间。

异常:

  1. 请求的栈深度大于所允许的栈深度
  2. 动态扩展时无法申请到足够的空间,Hotspot不会动态扩展

是虚拟机管理内存中最大的一块,是所有线程共享的垃圾收集器管理的内存区域,又称gc堆。虚拟机启动时创建,用来存放对象实例。

从分配内存角度看,堆中可以划分出多个线程私有的分配缓冲区,以提升分配对象的效率。Java堆既可被实现为固定大小的,也是可扩展的。

异常:

在Java堆中没有内存完成实例分配,且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

java堆从GC的角度细分为:新生代和老年代 

如果eden放不下之后,java虚拟机的字节码执行引擎会开启垃圾回收线程,收集垃圾对象;

年轻代

Eden区,to survivor,from survivor

老年代

1.默认分代年龄达到15,还没有被回收的话,会挪到老年代,分代年龄被放在对象头中

2.对于年轻代放不下的大对象,也会放到老年代

3.对象动态年龄判断也会挪到老年代

对象动态年龄判断

如果当前放对象的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了;

例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。

这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。

对象存储布局

HotSpot对象在堆内存中的存储布局:对象头(header),实例数据(instance data),对齐填充(padding)。

对象头包含两部分信息:

(1)存储对象自身的运行时数据,哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳,数据长度在32位和64位的虚拟机中分别占32比特和64比特。Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间

(2)类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例,但是查找对象的元数据信息并不一定要经过对象本身,因此并不是所有的虚拟机实现都必须在对象数据上保留类型指针

方法区

jdk1.8之前叫永久代,jdk1.8叫元空间,各个线程内存区域共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的class文件缓存等数据,又称非堆。

JDK1.8之后将最初的永久代取消了,由元空间取代,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

方法区中的静态变量:放的是堆中的地址,静态变量在方法区中,因此相当于方法区指向堆

方法区中类信息:字节码的类信息会加载到方法区。

垃圾收集行为在这个区域较少出现,但并非数据进入了方法区就如永久代的名字一样“永久”存在。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

元空间和永久代之间最大的区别在于:

永久代使用的jvm的堆内存,但是元空间并不在虚拟机中而是使用本机物理内存

因此,默认情况下,元空间的大小受本地内存的限制,类的元数据放入native memory,字符串和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

运行时常量池:是方法区的一部分。存放的内容:

1.存放编译期生成的各种字面量与符号引用,在类加载后放入。

2.因其动态性的特性,运行期间也可以将新的常量放入池中,使用较多的便是String类的 intern()方法。

异常:常量池无法申请到内存时

方法区垃圾收集

废弃的常量:对于常量池中的常量a,如果没有任何对象引用该常量的话,就表示它是一个废弃常量。

不再使用的类:

1.Java堆中没有任何该类的实例;

2.该类的类加载器已被回收;

3.该类的Class对象没在任何地方被引用,且无法在任何地方通过反射访问该类的方法。

创建对象流程

类加载机制

当虚拟机遇到一个字节码new指令时,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已被加载,解析,初始化过,如果没有就需要先去走类加载机制进行加载

分配内存

如果加载之后,虚拟机为该对象分配内存,对象所需的内存大小在加载的时候便已经确定;

如果内存区域规整,则使用指针碰撞(下面详细介绍);

如果内存区域不规整,则使用空闲列表(下面详细介绍);

而内存区域是否规整是根据垃圾收集器中的算法决定,看其是否具有压缩整理的能力

初始化零值

内存分配完之后,虚拟机会将分配到的内存空间都初始化为零值,如果使用了tlab(下面详细介绍),则会提前到和tlab(下面详细介绍)一起执行,这步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,使程序可以访问到这些字段所对应数据类型的零值

设置对象头

之后java虚拟机还要对对象进行必要设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(object header)中。

执行init方法

执行init方法,即对象按照程序员的意愿进行初始化,对应到语言层面就是为属性赋值,(这里的赋值与上面的赋零值不同,是由程序员赋值)和构造方法。

多线程创建对象内存分配问题

在多线程情况下,虚拟机给A对象分配了一块内存,但是还没来得及移动指针,B对象进来也分配了相同的区域的解决方案

1.在分配内存时进行同步处理,虚拟机是采用CAS的方式来保证原子性

2.设置-XX:+UseTLAB开启TLAB(默认是开启的)

TLAB

TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 内存分配的一个概念,JVM 会针对每一个线程在 Java 堆中预留一个内存区域,专门在该区域为该线程创建的对象分配内存。在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作。

主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。

没有启用 TLAB的话,多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。

通过-XX:+UseTLAB决定是否开启;

通过-XX:TLABSize指定TLAB大小。

指针碰撞

若内存区域规整,已被使用过的内存在一边,未被使用过的内存在其后紧跟,中间放着一个指针作为分界点指示器,如果需要分配内存,就移动需要内存大小的指针,这种分配方式称为指针碰撞

空闲列表

若内存区域不规整,使用和未使用的内存空间交杂在一起,那虚拟机会维护一个列表,记录哪些内存块可用,在分配的时候从列表中找一块足够大的内存空间分配给对象实例,并更新列表记录,这种分配方式称为空间列表

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值