JVM中的对象以及内存管理

“java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的‘高墙’,墙外面的人想进去,墙里面的人却想出来。”

1.基本概念

相信学过java的人都知道,在java中最基本的内存可以分为栈内存(Stack),堆内存(Heap),堆中创建对象,栈中分配变量引用指向堆内存对象,特别看过马老师视频的人,一个方法运行过程中根据栈,堆的分配显得合情合理,不亦乐乎,如图1-1所示。确实,在多数情况下,这种内存概念已经够了,程序员最关注的内存区域就是这两块,但在有些时候,我们不得不理解内存区域更加深入,例如当我们需要对项目上线时调整JVM的参数来适应自己的项目,在运行过程中宕机也需要分析内存的状态,又或者,作为一名JAVA程序员,想要在该行业走得更远,深入的理解JVM是很重要的,所以接下来的章节将介绍java中更为详细的内存分配。


图 1-1

2.JVM内存分配

JVM在java程序运行过程中会将它所管理的内存划分为若干个不同的数据区域,有些区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。在JDK1.7中,JVM所管理的内存将会包括以下几个运行时数据区域,如图2-1所示。


图 2-1

在此图中,深色背景的区域代表线程所共享的数据区,而没有背景的区域代表线程隔离的数据区,接下来将重点讲解每个数据区的作用以及所存储的数据。

2.1 虚拟机栈

生命周期与线程同步,线程私有的,描述方法执行过程的内存模型,每个方法执行过程中都会创建一个栈帧(Stack Frame)用于存储局部变量表,动态链接,操作数栈,方法出口等信息,每个方法从调用到直至完成的过程,就对应着一个栈帧的虚拟机中入栈和出栈的过程。前面所说的粗略的分为栈和堆内存的“栈”指的就是此区域,而马老师视频中经典的内存图中所指的栈区域可以说是虚拟机栈中的局部变量表部分。

局部变量表中存放了变异期间可知的各种类型的基本数据类型(int,double等等),引用,其中64位长度的long和double类型的数据会占用2个局部变量空间(4个字节),其余均占用一个。

2.2 本地方法栈

本地方法栈和虚拟机栈作用基本一样,区别为虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法(e.g: String.intern())服务,值得注意的是,在Hotspot虚拟机中,不区分本地方法栈和虚拟机栈。

扫描二维码关注公众号,回复: 2010191 查看本文章

2.3 堆

堆是JVM所管理内存区域中的最大的一块,为线程所共享,在虚拟机启动时就创建好了,此区域存放的唯一类型是实例对象,几乎所有对象都在这里分配内存。

在现代JVM中,因为垃圾收集器算法(以后将专门讲解)的原因,所以java堆还可以细分为新生代和老年代;Eden,From Survivor,To Survivor空间等等。

我们已经知道,Heap区域是线程所共享的,而一旦提及共享,我们就不得不考虑线程同步的问题。但其实在堆中,线程可能有自己专属的空间,即TLAB,Thread Local Allocation Buffer,关于TLAB,此区域使用最为人们广知的即是提供数据库的事务操作,后面也将提供专门章节进行讲解。

2.4 方法区

JVM规范对于此块区域的限制非常宽松,所以不同的JVM实现不同之处较大。严格来说,方法区其实属于堆(存在于永久代),但又存储的东西不一样,所以该区还有另外一个名字,非堆(Non Heap),也是一个线程共享的区域,用于存储已经被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据,值得注意的是,除了HotSpot虚拟机,其它虚拟机是不存在永久带的,在JDK1.8中已经表现如此。在目前的1.7及以后的区域中,一ing将原本放在永久带的字符串常量池移出。

2.4.1 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。对于Class文件来说,包含类的版本,字段,方法,接口等描述信息以及常量池。而常量池用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。值得注意的是,在运行期间也有可能有新的常量加入到池中,比如String类的intern()方法。

2.5 直接内存

直接内存不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁的使用。

在JDK1.4后,加入了新的NIO(New Input/Output)类,引入了一种基于通道与缓冲池的I/O方式,它可以使用Native函数库(比如C++实现的方法)直接分配内存,然后通过一个存储在Heap中的DirectByteBuffer对象作为这块内存的引用进行操作,从而提高了性能。避免了在java heap中和native heap中来回复制数据。显然,这部分内存不会受到java heap的影响。

2.6 程序计数器

线程私有,可以看作是当前线程执行字节码的行号指示器,改变这个指示器的值来选区下一条需要执行的字节码指令,如循环,异常处理,线程恢复等。

JVM多线程是通过线程轮流切换并分配处理器执行时间的方式来执行的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,各个线程之间计数器互不影响,独立存储。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,则计数器为空。


以上就是在JVM中的运行时内存分配情况,接下来将讲解在JVM中对象的创建以及对象的结构以及分配布局。

3. 虚拟机中的对象

本节主要讲的是一个对象在JVM中heap中的创建,对象分配,布局和访问的过程。

3.1 创建和分配

当JVM遇到new时,首先检查的是常量池中是否有对应的类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化,如果没有,那么必须先执行相应的类加载过程(将在以后章节中讲到)。

对象分配多大的内存的任务就是从heap中划分出一块内存出来,至于该块内存多大,在一个类被加载完成后就已经确定了(不包含数组的类)。在进行内存分配时,一般有两种分配方式:

1. 现有heap内存时整齐的(所有用过的内存一边,空闲的内存放在一边,中间存在一个指针,当有一个对象被创建时,将指针往空闲的那边挪动一段和该对象大小相等的距离,这种分配方式叫做“指针碰撞”。

2. 当现有heap内存不是整齐的时候,JVM维护了一个表,在这种表上记录了那些内存时空闲的,那些内存是在用的,在创建对象的时候从空闲列表划分一块出来,然后在该表上更新一下记录,这种分配方式称为“空闲列表”。

在具体的JVM实现中,两种分配方式都被采用,当垃圾收集器采用(后面会有专门章节讲)Serial,ParNew,Compact时,此类收集器有压缩整理功能,使用的是指针碰撞,而垃圾收集器采用CMS时,没有压缩整理功能,使用的是空闲列表分配方式。

值得注意的是:我们知道heap区域时线程共享的,所以再分配内存是,必然存在同步问题,而JVM则是使用CAS算法(自行百度)加上失败重试的方式保证了分配过程的原子性。 而另一种保证同步的方式即时前面所提到的TLAB,为每个线程分配一块专属内存,每个线程分配对象在此块区域分配即可,而采用哪种同步方式,却决于虚拟机是否开启了TLAB参数,  --XX:+/-UseTLAB来设置。

内存分配完成后,虚拟机需要将分配到的内存都初始化为零值(不包括对象头,至于什么是对象头,将在后面章节进行讲解),这一操作保证了对象的实例字段在java代码中不用赋值就可以直接使用。接下来,虚拟机为对象进行必要的设置,例如该对象时那个类的实例,如何才能拿到这个类的元数据信息(方法,字段名等等),哈希码,对象GC分代等信息,这些信息存放在对象的对象头当中。当所有这些工作完成后,一个对象就可以说是诞生了,接下来就是执行java程序自己的初始化了(init方法),分配初始值等等。

3.2 JVM中的对象的内存布局

JVM中堆的对象在布局中可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding),如图3-1所示。


对象头包含两部分信息,一部分用于存储自身的运行时数据,如hash code,GC分代标识,线程锁,时间戳等等。而另一部分则是存储类型指针,即该对象指向他的元数据的指针,虚拟机通过这个来判断这个对象时那个类的实例,值得注意的是,如果对象是一个数组,那么对象头中还会有一块用于记录数组长度的数据,如果3-2所示:


图 3-2

而实例数据所存储的是该对象正真存储的有效信息,也就是元数据中所定义的各个字段的字段内容。

而第三部分对齐填充不是必然存在的,仅仅有着占位符的作用,因为JVM规定对象其实地址时8字节整数倍,就是说对象大小必须是8字节的整数倍,当实例数据没有达到时,则依靠对其填充来补齐。

3.3 对象的访问定位

现在对象已经在heap区域中建立了,接下来该如何从Stack上访问它呢,JVM规范中只规定了reference类型是一个指向对象的引用,没有具体规定如何定位,所以当前主流有两种方式用于对象的定位。

1. 句柄定位: 如果使用访问句柄的话,那么在堆中将会划分出一块内存来作为句柄池,reference中就是存储了对象的句柄地址,通过该地址再追踪到具体对象,如果3-3所示:


图 3-3

2. 直接指针访问,reference就直接指向了该对象的起始地址(不是真正意义上的物理内存地址)。

两种方式各有优劣,当使用句柄访问时,对象如果被移动,只需要改变句柄当中的实例数据的指针,而reference本身则不需要修改。

当使用直接访问时,显然速度更快,但如果对象频繁移动(GC换代),以后也是一个不小的开销,而在不同的JVM实现中,两种方式均有,HotSpot中使用第二种直接指针访问。

4. 总结

在本章中,我们具体阐述了JVM中关于内存的管理,以及每块内存的存储,又谈论了JVM中关于内存的创建过程,以及对象的布局,组成,定位。 理解了JVM中关于内存,对象的原理。


觉得有用可以收藏哦。 在接下来的章节中将具体讲解TLAB的作用,它是如何实现事务管理的,以及JVM中关于垃圾收集器。


猜你喜欢

转载自blog.csdn.net/Iperishing/article/details/80948908