JVM学习-之对象的创建和内存分配
最近看JVM内存模型,看了很多文章,大都讲到JVM将内存区域划分分:Mehtod-Area(No heap) 方法区,Heap(堆)区,Program Counter Register(程序计数器),VM Stack(虚拟机栈),Native Mehtod Stack(本地方法栈),其中方法区和堆区是线程共享的。而虚拟机栈,本地方法栈,程序计数器是非线程共享的。每个java程序在自己的虚拟机上,然后告知虚拟机程序的运行入口。再被虚拟机字节码解释器加载运行。JVM运行的时候都会 分配好方法区和堆区,每遇到一个线程则分配程序计数器,虚拟机栈,本地方法栈。当线程运行结束时,则程序计数器,虚拟机栈,本地方法栈的内存空间也会被释放掉。这也就是为什么把内存区域划分分线程共享和非共享的原因,非线程共享的那三个区域其生命周期和所属的线程相同,随线程的结束而结束。而线程共享的区域和JAVA程序运行的生命周期相同。这也是垃圾回收总是发生在线程共享区域的原因。接着就是把各个区域的作业以及运行时存储类的那些数据做了分类概述:
先引入一张借鉴的图片,有个清晰的轮廓:
1. 程序计数器:
程序计数器用于保存当前正在执行的程序的内存地。JAVA中程序的分支,跳转,循环,异常处理和多线程环境中线程的恢复,都依赖于这个功能。因为程序执行的轨迹不可能一直是线性执行的,当有多个线程交叉执行时,被中断的线程的程序当前执行到那一条内存地址必然要记录下来。以便被中断的线程得到恢复执行的机会时可以继续按中断时的指令执行下去。所以每个线程都有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,属于线程所私有的。
2. JVM 栈:
虚拟机栈,也叫栈内存,是线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放了。JAVA栈总是线程关联到一起,每当创建一个线程,JVM就会为该线程创建对应的栈,这个JAVA栈中有包含有多个栈帧。这个栈帧是和每个方法关联的,每运行一个方法就创建一个栈帧。每个栈帧包含局部变量表(包含了对应的方法参数和局部变量),操作栈(Operand Stack,记录出栈、入栈的操作),动态链接和方法出口等信息。每个方法被调用直到执行完毕的过程,对应这栈帧在虚拟栈的入栈和出栈的过程。由于JVM 栈和线程关联起来的,Java栈数据不是线程共享的,所以不用关心其数据的一致性问题,也不会存在同步锁的问题。但在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
3. Heap 堆:
堆,一种数据结构,FIFO,先进先出。堆也是是JVM管理内存中最大的一块,是被所有JAVA线程所共享的,是非线程安全的。在JVM启动时创建,专门用来保存对的实例。例:new Person();出来的对象都存放在堆中,还有数组对象。实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型,并不保存对象的方法(已帧的形式保存在栈中),在堆中分配一定的内存保存对象的实例。对象实例在堆中分配好以后,需要在栈中保存4个字节的heap内存地址,用来定位对象实例在堆中的位置,便宜找到该对象。所以,JAVA堆区也是GC主要工作的场所。从内存回收的角度来看,由于现在的GC都采用分代回收的算法,所以java堆还可以细分为,新生代,老年代;新生代再细分为Eden空间,From survivor,To survivor空间等。
4. Method Area 方法区:
方法区主要存放了加载类定义的数据(名称,修饰符等),类中的静态常量,类中定义为final类型的常量,类中的Field信息,类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法获取信息时,这些数据都来自方法区。java方法区是所有线程所共享的。不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分(正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap),就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。
5. Constant Pool常量池:
方法区有一个非常重要的区,叫做运行时常量池(RCP)。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,在字节码文件(Class文件)中,除了有类的版本、字段、方法、接口等先关信息描述外,还有常量池(Constant Pool Table)信息。用于存储编译器产生的字面量和符号引用。这部分内容在类被加载后,都会存储到方法区中的RCP。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。
6.Native Method Stack 本地方法栈:
本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
这里重点要说的是堆里对象实例的分配和存储:
java是面向对象的语言,因此对象的创建无时无刻都存在。在语言层面,使用new关键字即可创建出一个对象。但是在虚拟机中,对象创建的创建过程则是比较复杂的。
首先,虚拟机运到new指令时,会去常量池检查是否存在new指令中包含的参数,比如new People(),则虚拟机首先会去常量池中检查是否有People这个类的符号引用,并且检查这个类是否已经被加载了,如果没有则会执行类加载过程。
在类加载检查过后,接下来为对象分配内存当然是在java堆中分配,并且对象所需要分配的多大内存在类加载过程中就已经确定了。为对象分配内存的方式根据java堆是否规整分为两个方法:1、指针碰撞(Bump the Pointer),2、空闲列表(Free List)。指针碰撞:如果java堆是规整的,即所有用过的内存放在一边,没有用过的内存放在另外一边,并且有一个指针指向分界点,在需要为新生对象分配内存的时候,只需要移动指针画出一块内存分配和新生对象即可;空闲列表:当java堆不是规整的,意思就是使用的内存和空闲内存交错在一起,这时候需要一张列表来记录哪些内存可使用,在需要为新生对象分配内存的时候,在这个列表中寻找一块大小合适的内存分配给它即可。而java堆是否规整和垃圾收集器是否带有压缩整理功能有关。
1. 在为新生对象分配内存的时候,同时还需要考虑线程安全问题。因为在并发的情况下内存分配并不是线程安全的。有两种方案解决这个线程安全问题,1、为分配内存空间的动作进行同步处理;2、为每个线程预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer, TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配。内存分配后,虚拟机需要将每个对象分配到的内存初始化为0值(不包括对象头),这也就是为什么实例字段可以不用初始化,直接为0的原因。接来下,虚拟机对对象进行必要的设置,例如这个对象属于哪个类的实例,如何找到类的元数据信息。对象的哈希吗、对象的GC年代等信息,这些信息都存放在对象头之中。执行完上面工作之后,所有的字段都为0,接着执行<init>指令,把对象按照程序员的指令进行初始化,这样一个对象就完整的创建出来。
2、对象的内存布局
对象在内存的存储布局中包括:对象头、实例数据、对齐填充
对象头(Header):包含两部分信息。1、存储对象自身的运行时数据,比如哈希码、GC分代年龄等;2、类型指针:通过这个指针确定这个对象属于哪个类。
实例数据(Instance Data):存储代码中定义的各种类型的字段内容。
对齐填充(Padding):这部分信息没有任何意义,仅仅是为了使得对象占的内存大小为8字节的整数倍。
3、对象的访问定位
创建对象是为了使用对象,java程序需要通过栈上的reference数据来操作栈上的具体对象。目前主流的访问对象方式有使用句柄和直接指针两种。1、使用句柄方式:会在java堆中创建一个句柄池,reference指向的这块句柄池,句柄池中包括两个指针,其中一个指针指向对象实例数据,另外一个指针指向对象的类型数据。2、使用指针的方式:reference存储的直接就是对象的地址。
两种方式各有各的特点,如果使用句柄方式的话,最大的好处是reference存放的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用指针的方式优势则是速度快,并且省去了一次指针定位的开销。
更详细的可以参考:
https://www.cnblogs.com/lewis0077/p/5143268.html
https://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.html