JVM内存区域和JVM中对象的创建和分析

1:JVM运行数据区介绍

对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要每一个new的对象去主动进行delete和free代码操作,不容易出现内存泄露。当然也是整因为这个原因,当一旦出现内存泄露,溢出,那么排除将是一个十分难的问题。

 

 

 

 所有线程共享的内存区域:

方法区(Method Area):用来存储已经被虚拟机加载的类信息,常量,静态变量(常量池)等等。JVM对方法区的限制非常宽松,除了和JAVA对一样不需要连续的内存和可选择固定大小,还可以选择不实现垃圾回收集,这是因为在这个区域内,但是非数据进入方法区后,就如同永久代一样,永远存在下去了。在方法区的主要目标是针对,常量池的回收以及类型的卸载。

运行常量池:运行常量池是方法区的一部分,Class文件除了类的模板,字段,方法,接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号,这部分内容在类加载后进入运行常量池里面存放。运行常量池的一大重要特征就是动态性,java语言并不要求常量一定只有编译器才能产生,也就是并非预置在Class文件里的常量池才能进入运行常量池,在运行期间,也可能将新的常量放入池中,必须String类的intern方法。

堆(heap):对于大多数应用来说,堆是Java虚拟机管理内存最大的一块,该区域的唯一目的就是存放对象实例和数组,几乎所有对象实例都在这里分配内存。正式由于上面的一些应用,这里也是垃圾回收器的主要工作区域,也被成为GC堆。从内存回收角度来说,现在的收集器都采用分代收集算法,因此在Java堆上右分为,新生代和老年代。其中新生代又分为Eden空间,From Survivor和To Survivior空间。关于新生代和老年代的垃圾回收操作放在下一篇讲解。根据JVM的规范,可知Java堆可以处理物理上不连续空间,只要逻辑上连续即可。

Class对象是存放在堆区的,不是方法区,这点很多人容易犯错。类的元数据(元数据并不是类的Class对象。Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。

每个线程独有的内存区域:

程序计数器(Program Counter Register):是一块内存较小的区域,可以被看做是当前线程所执行的字节码的行号指示器,通过该计算器可以完成字节码指令的,分支,循环,跳转,异常处理,线程恢复等基础功能。正是由于JVM的多线程是通过线程切换并分配处理器执行时间来实现的,任何一个确定的时刻,一个处理器都只会执行一条指令,因此当线程切换后,为了能恢复到争取的执行位置,每一个线程都要有一个独立的线程计数器,各条线程计数器之间互相独立,单独存储。

Java虚拟机栈(Virtual Machine Stacks):该内存区域的生命周期与线程的生命周期相等,该stack描述的是Java方法执行的内存模型;每一个方法在执行期间都会创建一个栈帧,用来存储局部变量,操作数栈,方法出口等等信息,每一个方法从调用到执行完成的过程对应的就是一个栈帧在Virtual Machine Stacks 洪从入栈到出栈的过程。除了上面说的方法的栈帧外,Virtual Machine Stack中还有局部变量表,局部变量表里存放了基本数据类型,(boolean,byte,char,short,int,float,long.double)以及对象引用(reference类型该类型可以指向一个对象起始地址的引用指针,也可以是一个代表对象的句柄获得returnAddress类型。

本地方法栈(Native Method Stack):该stack的功能与Virtual Machine Stacks的功能十分相似,区别在于虚拟机栈是为JVM调用java方法(字节码)服务的,而本地方法栈是为JVM调用本地方法服务的。在JVM规范里没有对本地方法的实现语言,使用方式以及数据结构有强制规定,具体的JVM可以只有实现它,有的JVM甚至将Native Method Stack和Virtual Machine Stacks放在一起使用。

2:JVM中对象

2.1:对象的创建

第一步类加载:当JVM遇到一个new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号,并且检查这个符号代表的类是不是已经被加载过,解析和初始化过,如果没有则必须先执行相应的加载的过程(参看另外一篇文章类的加载过程)。

第二步为对象分配空间:创建类的对象所需要的大小一在第一步类的加载过程就已经确定了,所以这样一步可以直接为对象在堆上分配内存。

  • 内存分配方法
    • 指针碰撞:该方法是在Java内存堆时绝对规整的情况下,所有用过的内存放在一边,没有用过的放在另一边,中间用一个指针作为分界点的指示器。
    • 空闲列表:假如堆是不规整的,虚拟机需要维护哪些内存块是可用的列表,分配时候从列表中找出足够大的空闲内存划分,并更新列表记录。
  • 如何保证多线程中对象创建的安全性。
    • 对分配内存的过程进行同步处理,采用CAS配上失败重试的方法保证更新和创建的原子性。
    • 为每一个线程在Java堆上分配一个TLAB,称为本地线程分配缓冲。那个线程需要分配内存就在哪个线程的TLAB内存区分配即可。

第三步为内存空间初始化为零值:这一步操作可以保证对象实例字段可以在不初始值的情况直接使用,程序可以访问这些字段的数据类型的零值,默认值,0 false,null等等。

第四步为设置对象头:设置对象的Object Header,对象头包含了对象的是哪一个类的实例?如何才能找类的元数据信息,对象哈希码以及对象的GC年龄等等。

第五步执行<init>方法:上面操作完成后后,从JVM角度来说,一个对象就已经创建完成了,但是从Java程序角度来说,对象创建才刚刚开始,<init>方法还没有执行,所有字段数据都是默认是零值。在这一步会执行<init>方法,把对象按照程序员的意图进行初始化,这才是一个真正可用的对象的产生。

2.2:对象的内存布局

  • 对象头(Header):
    • 存储对象自身运行时数据,如HashCode,GC分代年龄,锁状态标志,线程锁,偏向线程ID。
    • 类型指针:即对象指向它的类数据的指针,JVM通过这个指针可以确定本对象是哪个类的实例。
    • 如果是数组对象,则还保存一个记录数据长度的数据。
  • 实例数据:
    • 程序中定义的各种字段,无论是父类继承的,还是子类定义的都需要记录下来。
  • 对齐填充:不是不需要的,因为JVM中要求对象的起始地址必须是8字节的整数倍,也就是所对象大小也必须是8字节的整数倍,当不满足是,则通过对齐字节的方法来补全。

 

2.3:对象的访问定位

上面讲解对象的创建过程,但是为了使用对象,Java程序需要使用Virtual Machince Stack中方法栈帧的reference数据来操作堆上的具体对象。但是由于referende类型在Java虚拟机规范只规定是一个指向对象的引用,并没有定义通过何种方式来去定位,访问堆中数据的具体问题。由于有类和实例的区别,所以下面对对象的定位也包括了对类的定位方法。

  • 句柄访问,Java堆中划分出一内存作为句柄池,referende中存储的就是对象的句柄地址,而句柄包括实例数据和具体类型各自的具体地址。

  • 直接指针访问:这种方式那么在Java堆中对象的布局中就必须考虑如何访问类型数据相关的信息,而reference中直接存放的就是对象的地址。

 

小结:各有优势,句柄方式中,由于reference中存放的是稳定不变的句柄地址,而在对象被移动时(新生代中采用复制算法时会经常移动对象),只会改变句柄地址的实例数据指针,而reference指针不会改变。

而直接指针访问的好处就是,速度快!节省了一次指针定位的时间开销。两个方法相互补充吧。

 

posted @ 2020-02-29 16:01  大朱123  阅读(476)  评论(0编辑  收藏  举报