Java虚拟机初探

Java内存区域

在Java的世界中由于虚拟机提供了一种相对安全的内存管理和访问机制,从而避免了绝大部分内存泄漏和指针越界问题,但是仍需要我们Java程序员了解虚拟机背后的原理,以便在出现问题的时候,不会手忙脚乱,甚至不知道问题从何开始排查。

一、运行时数据区域

一个简单的结构就如下图所示:

image-20210502152356449

需要注意的细节:

上图中所述的方法区只是一个逻辑概念,它的具体实现在不同的Java虚拟机中不尽相同,本文主要以HotSpot虚拟机为主来进行讲述,JDK1.8以前,使用的是永久代来实现方法区,JDK1.8方法区的实现方式就变为了元空间,使用的是直接内存。

1、程序计数器

程序计数器主要做的通过改变这个计数器的值来选取下一条需要执行的字节码指令,可以看作是当前线程所执行字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于程序计数器来完成,它属于线程私有,每个线程都有属于他自己的一个程序计数器,程序计数器是Java内存区域中唯一一个不会出现OOM(OutOfMemoryError)的区域。

2、Java虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,它描述的是 Java 方法执行的线程内存模型,方法每次被调用的时候数据都是被栈帧所包裹并通过栈传递的。栈帧用于存储局部变量表,操作数栈、动态链接、方法出口等信息,方法的一次完整调用,对应着栈帧的入栈和出栈这个过程。

3、本地方法栈

本地方法栈类似于Java虚拟机栈,但和Java虚拟机栈不同的是,本地方法栈只是为调用了被native关键字所修饰的本地方法使用。

4、Java堆

Java堆是虚拟机所管理的内存中最大的一块,是所有线程共享的一块区域,主要的作用是用来存放对象的实例,几乎所有的Java对象实例都是在这个地方分配内存的,由于大部分GC都是在这个地方完成的,所以Java堆也被称为"GC堆",由于现在大部分的垃圾收集器都是基于分代理论进行设计的,所以Java堆在设计上可以被分为新生代和老年代,不同的区域的划分是为了更好的进行垃圾回收。

堆通常都被分为以下几个区域:

新生代(Young Generation) : 如图所示Eden区,From Survivor和 To Survivor都属于新生代

老年代(Old Generation)

image-20210503152418142

大部分对象在分配内存的时候首先是在Eden区分配,在进行一次垃圾回收之后,存活的对象会进入From Survivor区或者 To Survivor区,并且对象的年龄会进行+1,年龄到达一定数目之后(CMS垃圾收集器的默认晋升值为6)会进入老年代。

在堆中随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常,可以通过参数-Xms与-Xmx来设置堆的最大值和最小值。

5、方法区

方法区也是各个线程共享的一块内存区域,主要用来存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

6、运行时常量池

运行时常量池属于方法区的一部分,。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

7、直接内存

直接内存并不由虚拟机直接进行管理,但使用还是较为频繁的,它的大小不受Java虚拟机大小的限制,但会被本机内存大小限制,也会出现OutOfMemoryError 错误。

二、对象

1、对象的创建

image-20210503170710546

类加载检查

虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并检查这个符号代表的类是否已被加载、解析和初始化,如果没有,那必须先去执行相应的类加载过程

分配内存

在类加载通过后,接下来虚拟机将为新生对象分配内存。对象所需的大小在类加载完成之后就可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存块从Java堆中划分出来,分配方式具体有以下两种:

指针碰撞空闲列表
使用场景:内存规整内存不规整,内存碎片较多
原理被使用过的内存都放在一边,空闲的内存被放在另一边,中间有一个指针作为分界点的指示器,将指针向空闲的一方移动与对象相同大小的距离即可虚拟机维护一个列表,记录那些内存块是可用的,在分配内存时找一块足够大的空间分配给对象实例,之后更新列表记录
具体应用Serial、ParNew垃圾收集器CMS垃圾收集器
初始化零值

内存分配完成之后,虚拟机将所分配到的内存空间(不包括对象头)都初始化为零值,这步操作保证对象的实例在Java代码中可以不赋初始值就可以直接使用,使程序能访问到这些字段的数据类型所对应的零值。

设置对象头

在进行了上一步的初始化操作之后,Java虚拟机会对对象进行设置,例如这个对象是哪个类的实例、如何能够找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都会被存放在对象的对象头之中,另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

执行init方法

上面的步骤都完成之后,在虚拟机看来,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,Class文件中的()方法还没有执行,所有的字段都为默认的零值,其他的信息也没有按照预定的意图构造好,new指令执行之后会紧接着执行()方法,按照程序员的意愿让对象进行初始化,自此,一个正真可用的对象才算完全被完全构造出来。

对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据、对齐填充。

HotSpot虚拟机对象的对象头包含两类信息:第一类是对象自身的运行时数据(Mark Word)包括哈希码、GC分代年龄偏向锁ID等。第二类是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。实例数据部分存储着对象真正的有效信息,是我们在程序代码里面所定义的各种类型的字段内容。

2、对象的访问定位

创建对象的目的在于使用对象,Java程序通过栈上的reference数据来操作堆上的具体对象。要操作对象就得先去访问对象,对象的访问方式主要有使用句柄访问和直接指针两种:

1、**句柄访问:**Java堆中将可能会划分出一块内存作为句柄池,reference中存放的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自具体的地址信息

image-20210505150107934

2、直接指针访问:使用直接指针访问对象,Java堆中对象的内存布局就必须考虑如何放置访问类型的相关信息,reference中存储的直接就是对象地址。

image-20210505150131594

两者各自的优势:句柄访问的优势在于reference中存储的是稳定句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改;使用直接指针访问的优势在于访问速度更快,节省了指针定位的时间开销,在HotSpot虚拟机中主要使用直接指针访问。

参考:

  • 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》

关注我 搜索公众号 Java驭风

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值