Java基础之JVM结构

Java基础之JVM结构

Java基础之JVM结构

对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

运行时数据区域

在这里插入图片描述
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
这些组成部分有一些线程是私有的,有的是线程共享的。
其中线程私有的有:
1.程序计数器
2.虚拟机栈
3.本地方法栈
线程共享的有:
1.堆
2.方法区
3.直接内存
我们将对这些模块去做详细的分析:

程序计数器

程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,其中分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来实现。
另外,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
从上面我们可以得知:
1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制:顺序执行、选择、循环、异常处理。
2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来时能够知道该线程上一次运行的位置。
注:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而产生,随着线程的终结而死亡。

Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同,描述的是Java方法执行的内存模型。
Java内存可以粗糙的区分为堆内存(Heap)栈内存(Stack),其中栈就是现在我们说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都有:局部变量表、操作数栈、动态链接、方法出口信息等)
局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始位置的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError。
1.StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度时,就会抛出StackOverFlowError异常。
2.OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而随着线程的创建而产生,随着线程的死亡而死亡。

本地方法栈

和虚拟机栈发挥的作用非常相似,区别在于:虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机用到的本地方法(Native)服务。在HotSpot虚拟机中和Java虚拟机合二为一。
本地方法被执行的时候,在本地方法栈创建一个栈帧,用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放空间,也会出现StackOverFlowError和OutOfMemoryError异常。

Java虚拟机所管理内存最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存存在的目的是存放对象实例,几乎所有的对象实例以及数组都在这里进行分配内存。
Java堆作为垃圾收集器管理的主要区域,因此也可以称为GC堆(Garbage Collected Heap),从垃圾回收的角度,由于现在的收集器主要采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代,当然还可以进一步划分为Eden空间,From Survivor、To Survivor空间等。进一步地划分目的是更好地回收内存,或者更快地分配内存。
在这里插入图片描述
注:在JDK1.8移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受本机物理内存限制)。
拓展链接: Java8内存模型—永久代(PermGen)和元空间(Metaspace).

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是为了和Java堆区别开来。
HotSpot虚拟机中方法区也常被称为“永久代”,本质上二者并不相同。仅是因为HotSpot虚拟机设计团队用永久代来实现方法区而已,这样HotSpot虚拟机的垃圾回收器就可以像管理Java堆一样来管理这部分内存。但这样会造成内存溢出的问题。
相对而言,垃圾回收在这个区域是很少出现的,但也不是意味着数据进入方法区后就“永久存在了”。

运行时常量池

运行时常量池是方法区的一部分,Class文件除了有类的版本 、字段、方法、接口等描述信息之外,还有常量池信息(用于存放编译器生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。
JDK1.7以后版本的JVM以及将运行时常量池从方法区移出来了,在java堆(Heap)中开辟了一块区域存放运行时常量池。
在这里插入图片描述
拓展连接: 《Java 中几种常量池的区分》

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但这一部分也会频繁的使用,当然也会导致OutOfMemoryError异常的出现。
在JDK1.4中新加入的NIO(New Input/Output)类,引入一种基于通道(Channel)与缓存区(Buffer)的I/O方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。
本机直接内存的分配不会受到Java堆的限制,但是会受到本机总内存大小以及处理器寻址空间的限制。

HotSpot虚拟机对象详解

对象的创建

在这里插入图片描述
(1)类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
(2)分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空散列表”两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

选择以上两种方式的具体哪一种,取决于Java堆内存是否规整,而Java堆内存是否规整,取决于GC收集器的算法是“标记-清除算法”,还是“标记-整理”(也称标记-压缩),需要注意的是,复制算法内存也是规整的
在这里插入图片描述

分配内存并发问题
在创建对象的时候需要注意的一个问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常虚拟机会采取两种方式来保证线程的安全:

CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突就失败重试,直到成功为止。虚拟机采用CAS+失败重试的方式保证更新操作的原子性。
TLAB:为每一个线程都预先在EDEN区分配一块儿内存,JVM在分配内存的时候,现在TLAB上分配,当TLAB内存不够或内存用尽的时候,再采用上述CAS进行内存分配

(3)初始化零值:在内存分配完后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就能直接使用,程序能访问到这些字段数据类型所对应的零值。
(4)设置对象头:初始化零值完成后,虚拟机要对对象要进行必要的设置,例如这个对象是哪个类的实例,如何才能找到这个类的元数据信息、对象的哈希码、GC的分代年龄等信息,这些信息存放在对象头中。另外,根据虚拟机当前运行状态不同,像是否y启动偏向锁,对象头会有不同的设置方式。
(5)执行init方法:在上述工作都完成后,从虚拟机的角度来说,一个新的对象产生了,但从Java的角度来说,对象的创建才刚刚开始,方法还没有执行,所有的字段都为零。所以一般在new指令后会去执行方法,把对象按照程序员的意志去进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的布局可用分为3块区域:对象头实例数据对齐填充
HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC分代年龄、锁状态标志等等),另一本部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有特别的含义,仅仅起到占位作用。因为HotSpot虚拟机的自动内存管理系统要求对象起始位置必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对齐来进行补全。

对象的访问定位

建立对象就是为了使用对象,我们Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄直接指针两种。
1.句柄:如果使用句柄的话,那么Java堆中将会划出一块内存来作为句柄池,reference中存储的对象就是句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
在这里插入图片描述
2.直接指针:如果使用直接指针访问,那么Java堆对象的布局中就必须要考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。
在这里插入图片描述

这两种对象访问方式各有优势。使用句柄来访问最大的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据指针,而reference本身不需要更改。使用直接指针访问最大的好处就是速度快,节省了一次指针定位的时间开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值