Java虚拟机 ----运行时数据区与对象创建分配

   一、运行时数据区

     Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图下图所示。

                         

其中线程共享为:方法区、堆(Heap)

线程独有的:java虚拟机栈(stack)、本地方法栈、程序计数器

1.1 、程序计数器

     程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里 ,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  面试问题:为什么cpu在切换线程的时候,不会出现问题?

      为了线程切换后能恢复到正确的执⾏行行位置,每条线程都需要有⼀个独立的程序计数器,各条线程之间计数器器互不不影响,独⽴立存储,我们称这类内存区域为“线程私有”的内存

     内存区域中唯⼀一个没有规定任何 OutOfMemoryError 情况的区域。

1.2、java虚拟机栈

     线程私有的,生命周期和线程一样。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧 (Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

     存储局部变量表主要是存储编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始、地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

     如果线程请求的栈深度⼤大于虚拟机所允许的深度,将抛出 StackOverflowError 异常

1.3 本地方法栈

          本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

   与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出StackOverflowError和OutOfMemoryError异常。

1.4  java堆

     Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。堆是Java内存区域中⼀块⽤用来存放对象实例的区域,几乎所有的对象实例都在这⾥里分配内存。

     Java堆是垃圾收集器管理的内存区域。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词。

    如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local AllocationBuffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存

  前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。

  如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

1.5 方法区

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

     什么是类信息:类版本号、方法、接口。

    jdk7以前很多人认为方法区就是永久代,这个观点是不对的,永久代只是方法区的一个实现。在jdk8以后永久代就被元空间替换了。
特点:
    并非数据进⼊入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量量池的回收和对类型的卸载方法区也会抛出OutofMemoryError,当它⽆无法满⾜足内存分配需求时。

1.6 运行是常量池

     运行时常量量池是⽅方法区的⼀部分,Class⽂文件除了有类的版本、字段、方法、接口等描述信息外,还有⼀项信息是常量量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  特点:
     运行时常量量池是方法区的一部分,受到方法区内存的限制,当常量量池再申请到内存时会抛OutOfMemoryError异常。

public class Test {
    public static void main(String[] args) {
        String a = "abc";//在常量池分配
        String b = "abc";//在常量池分配
        System.out.println(a==b);//true
        String c = new String("abc");//new 出来对象在堆分配
        System.out.println(a==c);//false
        System.out.println(a==c.intern());//true
    }
}

1.7 直接内存

        直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
      在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    一般配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

二 对象探究

2.1 对象创建

      Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已。

如下图:

                     

   对象创建的流程步骤:

    1、 虚拟机收到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用。

    2、检查这个符号引用代表的类是否已被加载、解析和初始化过。
    3、如果没有,那必须先执行相应的类加载过程,为这个新生对象在Java堆中分配内存空间

   4、虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值

   5、设置对象头相关数据。对象头数据包含:

  •        GC分代年龄
  •        对象的哈希码HASHCODE
  •        元数据信息

   6、执行对象⽅方法  new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

2.1.1 对象 Java堆分配内存空间的方式

主要有以下两种:

    1)、指针碰撞

       Java堆中内存如果是规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

   2)空闲列表

      如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

     对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选

  方案一:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;

  方案二:一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定

   选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

2.2 对象结构

       在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。如下图:

              

 2.2.1 对象头

HotSpot虚拟机对象的对象头部分包括两类信息。

       第一部分Mark Word: 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。

    第二部分:类型指针类型指针 指向它的类元数据的指针,⽤用于判断对象属于哪个类的实例。

2.2.2 实例数据           

     实例数据部分是对象真正存储的有效信息如各种字段内容,各字段的分配策略略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到⼀起,便于之后取数据。父类定义的变量会出现在子类定义的变量量的前面

2.2.3 对齐填充

   对⻬齐填充部分仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

2.3 对象的访问定位

   创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。主流的访问方式主要有使用句柄和直接指针两种(HotSpot虚拟机采用的是第二种)

     1、使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了了对象实例例数据与类型数据的具体地址信息,相当于二级指针。

                                  

    2、直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。

                    

两者对比:

      1、垃圾回收分析:使用句柄访问当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需
要修改,仅需要修改对象句句柄的地址;使用直接指针垃圾回收时需要修改reference中存储的地址。
     2、访问效率分析,直接指针优于句柄访问,因为直接指针只进行了⼀次指针定位,节省了了时间开销,而这也是HotSpot采⽤用的实现⽅方式。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值