JVM详解运行时区域-JVM系列

JVM架构图「原|简」

JAVA中虚拟机的讲解,涉及「类加载机制,运行时区域,执行引擎,垃圾回收等」及对voliate, synchronized的JVM层面实现机制等。持续更新中…。 最新文章公众号持续更新中… 欢迎骚扰,分享技术,探讨生活。
adsf.png
image-20201217234438148

image-20201217224422149

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验解析初始化,最终形成可以被虚拟机直接使用的 Java 类型,在这阶段会在Stack Area区域做对应的内存分配,以及在运行过程中的堆 栈上的操作,本文主要针对内存区域进行讲解。

JVM运行时区域内存分配

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

img

包括:PC寄存器「程序计数器」,虚拟机栈,堆,方法区,运行时常量池,本地方法栈等。

程序计数器

1.每一个线程都有一个程序计数器线程私有的,用来存储指向下一条指令 「分支、循环、跳转、异常处理、线程恢复」等基础功能都需要依赖计数器完成的地址

2.创建线程时候就会创建一个程序计数器

3.执行本地方法时,程序计数器值为undefined

4.较小的内存空间,「是JVM规范中唯一不会发生内存溢出OutOfMemoryError的区域

image-20201218130759429

虚拟机栈

简介:栈由一些列帧「Frame」组成「因此Java栈也叫帧栈」,是线程私有的,生命周期和线程相同。

帧用来保存一个方法的局部变量操作数栈「Java没有寄存器,所有参数传递使用操作数栈」,常量池指针,动态链接方法出口「方法返回值」。

image-20201218135237207

每次方法调用创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。

image-20201218130534335

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double) 「每个槽「slot」存放32位数据,long, double 是64位各占两个槽位」、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。比如一直递归
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

优点:存取速度比堆块,仅次于寄存器。

缺点:存在栈中的数据大小,生存期是在编译期决定的,缺乏灵活性。

这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例数组。内部会划分出多个线程私的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

GC主要就是管理堆空间,对分代GC来说,堆也是分代的。

堆优点:运行期动态分配内存大小,自动进行垃圾回收。

堆缺点:效率相对较慢

方法区

方法区是JVM 定义的一种规范,是所有虚拟机都需要遵守的约定, 而“永久代(PermGen space)”和“元数据(MetaSpace)”都是实际某个虚拟机针对“方法区”的一种实现,“永久代”是的JDK1.7之前 Hotspot虚拟机对方法区的实现,而“元数据”则是1.8之后Hotspot虚拟机针对方法区的一种实现而已。

属于共享内存区域,存储已被虚拟机加载的类结构信息、常量、静态变量、即时编译器编译后的代码等数据。

通常和元空间关联在一起,跟具体JVM实现版本有关系 ,比如「JDK8以前和永久区关联一起,JDK8时候和元空间关联一起;string 在1.7就从方法区移到堆等」。

JVM规范把方法区描述为堆的一个逻辑部分,名称Non-head非堆,为了与Java堆区分开。

运行时常量池「方法区中」

属于方法区一部分,Class文件中每个类或接口的常量池表,运行期表示形式,通常包括:类的版本,字段,方法,接口等信息。

用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

元空间

在Java1.8中,HotSpot虚拟机已经将方法区(永久带)移除,取而代之的就是元空间不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。

image-20201218112601659

为什么使用元空间代替方法区

HotSpot虚拟机为例,设计团队使用永久带来实现方法区,并把GC的分代收集扩展至永久带。这样设计的好处就是能够省去专门为方法区编写内存管理的代码。但是在实际的场景中,这样的实现并不是一个好的方式,因为永久带有MAX上限,所以这样做会更容易遇到内存溢出问题。

在之前的版本中,字符串常量池存在于永久代中,在大量使用字符串的情况下,非常容易出现OOM的异常。此外,JVM加载的class的总数,方法的大小等都很难确定,因此对永久代大小的指定难以确定。太小的永久代容易导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张。

关于方法区的GC回收,Java虚拟机规范并没有严格的限制。虚拟机在实现中可以自由选择是否实现垃圾回收。主要原因还是方法区内存回收的效果比较难以令人满意,方法区的内存回收主要是针对常量池(1.7已经将常量池逐步移除方法区)以及类型的卸载,但是类型卸载在实际场景中的条件相当苛刻。

另外还需要注意的是在HotSpot虚拟机中永久带和堆虽然相互隔离,但是他们的物理内存是连续的。而且老年代和永久带的垃圾收集器进行了捆绑,因此无论谁满了都会触发永久带和老年的GC

因此在Java1.8中,HotSpot虚拟机已经将方法区(永久带)移除,取而代之的就是元空间。

元空间在1.8中不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。同时JVM提供参数控制元空间大小。

还有很多更多深层次的原因,可以参考 元空间出现的原因

本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowErrorOutOfMemoryError异常。

Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法

一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。

img

「如上图:该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法」

直接内存

非虚拟机运行时数据区的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。


为什么要使用之直接内存

   1. 减少垃圾回收工作,直接内存不受JVM控制,需要手动回收
      2. 加快复制的速度:操作系统会将JVM堆中的数据拷贝一份到直接内存,也就是用户态到内核态的拷贝。使用直接内存并配合其他技术(mmap、sendfile)可实现零拷贝。
                3. 进程间的共享
                   4. 堆,受限于虚拟机,直接操作1TB的内存更适合用直接内存「但要注意也受本机内存限制」

直接内存有什么限制

1. 不好控制,出现内存泄漏难排查

2. 相对于堆,不适合存储复杂对象

区域存储内容

img

HotSpot 虚拟机对象探秘

「对象的创建」

创建过程比较复杂,建议看详解,这里提供个人的总结。

1、构建对象:首先main线程会在栈中申请一个自己的栈空间,然后调用main方法后会生成一个main方法的栈帧。然后执行new操作 ,这里会根据new的类元信息先确定对象的大小,向JVM堆中申请一块内存区域并构建对象,同时对对象成员变量信息并赋默认值

**2、初始化对象:**然后执行对象内部生成的init方法,初始化成员变量值,同时执行搜集到的代码块逻辑,最后执行对象构造方法

**3、引用对象:**对象实例化完毕后,把栈中的对象引用地址指向对象在堆内存中的地址。

「内存的布局」

对象创建完成后在内存中保存了保存的信息包括对象头、实例数据及对齐填充三类信息。

对象头:

对象头里主要包括几类信息,分别是锁状态标志、持有锁的线程ID、,GC分代年龄、对象HashCode,类元信息地址、数组长度,这里并没有对对象头里的每个信息都列出而是进行大致的分类,下面是对其中几类信息进行说明。

**锁状态标志:**对象的加锁状态分为无锁、偏向锁、轻量级锁、重量级锁几种标记。

持有锁的线程: 持有当前对象锁定的线程ID。

GC分代年龄: 对象每经过一次GC还存活下来了,GC年龄就加1。

类元信息地址: 可通过对象找到类元信息,用于定位对象类型。

数组长度: 当对象是数组类型的时候会记录数组的长度。

实例数据

对象实例数据才是对象的自身真正的数据,主要包括自身的成员变量信息,同时还包括实现的接口、父类的成员变量信息。

对齐填充

根据JVM规范对象申请的内存地址必须是8的倍数,换句话说对象在申请内存大小时候8字节的倍数,如果对象自身的信息大小没有达到申请的内存大小,那么这部分是对剩余部分进行填充。

「堆、 栈、 方法区」的交互关系 、访问定位

局部变量表存放指向堆的元数据信息地址,根据User类的元数据「分配句柄池」,从方法区找出类的相关字段方法

image-20201217233437572

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象

Java 堆中会分配一块内存作为句柄池reference 存储的是句柄地址。详情见图。

img

reference 中直接存储对象地址

img

比较

使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)时只改变句柄中指向实例数据的指针,而指向对象类型数据的指针和对象引用指针reference都不需要修改。

而使用直接指针访问,当对象发生移动时对象引用reference需要修改。但是使用直接指针访问方式少了一次指针定位「少的就是句柄到对象实例数据的指针定位」的时间开销速度更快。

则优

如果是对象频繁 GC 那么句柄方法好「对象类型数据的指针和对象引用指针reference都不需要修改,只需要修改句柄中指向实例数据的指针」,如果是对象频繁访问则直接指针访问好「直接指针访问方式少了一次指针定位时间」。

本文参考《深入理解java虚拟机》

对象创建过程
JVM内存分区
方法区与元空间
数据访问定位

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值