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

原文地址:https://www.cnblogs.com/harbin1900/p/9094031.html

时间: 2024-11-04 09:29:22

JVM学习-之对象的创建和内存分配的相关文章

JVM学习:对象的创建和内存分配

1.对象的创建 java是面向对象的语言,因此对象的创建无时无刻都存在.在语言层面,使用new关键字即可创建出一个对象.但是在虚拟机中,对象创建的创建过程则是比较复杂的. 首先,虚拟机运到new指令时,会去常量池检查是否存在new指令中包含的参数,比如new People(),则虚拟机首先会去常量池中检查是否有People这个类的符号引用,并且检查这个类是否已经被加载了,如果没有则会执行类加载过程. 在类加载检查过后,接下来为对象分配内存当然是在java堆中分配,并且对象所需要分配的多大内存在类

jvm学习记录-对象的创建、对象的内存布局、对象的访问定位

简述 今天继续写<深入理解java虚拟机>的对象创建的理解.这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录. (此文中所阐述的内容都是以HotSpot虚拟机为例的.) 对象的创建 java程序在运行过程中无时无刻都有对象被创建出来,那么创建对象是个怎么样的过程呢?还是看看我自己的理解吧. 判断是否已经执行类加载 当虚拟机遇到一条new指令时 ,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载

对象的创建与内存分配

创建对象 当 JVM 收到一个 new 指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,如果没有的话则要进行一次类加载. 接着就是分配内存了,通常有两种方式: 指针碰撞 空闲列表 使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可. 当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式.虚拟机会维护一个空闲的列表,用于记

python学习笔记——列表在做extend,+=和+的时候对象的变化与内存分配

        有两个list的话用'extend','+='和'+'都可以实现两个列表的连接,但是他们在对象创建和内存分配上是有差别的.一开始Jia_Dai来告诉我'extend'和'+='在对象创建上是不同的.但后来从yusheng_ding那里学会用id()函数查看变量的序号.实验证明'extend'和'+='在连接列表的时候都没有创建新的对象,只是把后一列追加在前一列对象后面.而用'+'实现的时候,会创建一个新的长对象,并把这个对象赋给'='左边的变量,就如下面代码所示. >>>

Java中类,对象,方法的内存分配

Java中类,对象,方法的内存分配 以下针对引用数据类型: 在内存中,类是静态的概念,它存在于内存中的CodeSegment中. 当我们使用new关键字生成对象时,JVM根据类的代码,去堆内存中开辟一块控件,存放该对象,该对象拥有一些属性,拥有一些方法.但是同一个类的对象和对象之间并不是没有联系的,看下面的例子: 1 class Student{ 2 static String schoolName; 3 String name; 4 int age; 5 6 void speak(String

JVM理论:(二/1)内存分配策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存. 对象的分配可能有以下几种方式: 1.JIT编译后被拆散为标量类型并间接地栈上分配 2.对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配 3.少数情况下也会直接分配在老年代 参考下图: 5种内存分配策略 1.对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor G

【深入理解JVM】:Java对象的创建、内存布局、访问定位

对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内存布局),

JVM 对象的创建、内存布局

一.对象的创建过程 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行init方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内

JVM学习记录-对象已死吗

前言 先来回顾一下,在jvm运行时数据区,分为两部分,一个部分是线程共享区,主要包括堆和方法区,另一部是线程私有区分包括本地方法栈,虚拟机栈和程序计数器.在线程私有部分的三个区域是随着线程生和灭的.栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作.每一个栈帧所用内存大小在类结构确定下来时就已知了.因此这线程私有区的内存分配和回收都具备确定性,简单概括的说:这部分内存在类加载时分配,在线程结束时回收.(个人理解) 而线程共享区(堆和方法区)则不一样,一个方法中的多个分支需要的内存可能不一样,只