浅谈Java内存区域与对象创建过程

一、java内存区域

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

1.程序计数器(线程私有)

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

每条线程都有一个独立的程序计数器,各个线程之间互不影响,独立储存。

如果线程正在执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;

如果线程正在执行的是一个Native方法,计数器值为空(undefined)。

此内存区域是唯一一个在Java规范中没有规定任何OutOfMemoryError(OOM)情况的区域

2.Java虚拟机栈(线程私有)

虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。

通常大家所说的虚拟机堆内存和栈内存中,栈内存一般是指这里的局部变量表部分。

局部变量表存放了编译期可知的各种基本数据类型、对象引用(不是对象本身,可能是指向对象的指针或代表对象的句柄或其他的与此对象相关的位置,涉及到下文的“对象的访问定位”)和returnAddress类型。

3.本地方法栈(线程私有)

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,区别不过是虚拟机栈为Java方法(字节码)所服务,而本地方法栈则为虚拟机使用的Native方法服务。Java虚拟机规范没有对本地方法栈使用的语言、方式和数据结构有强制规定。因此虚拟机可以自由实现它,甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一。

4.Java堆(线程共享)

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。唯一的目的就是存放对象实例。所有的对象实例和数组都要在堆上分配内存。

Java堆是垃圾收集器管理的主要区域。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,类似于磁盘空间。在实现时既可以是固定大小,也可以是可扩展的,当前的主流虚拟机都是按照可扩展实现的。如果在堆中没有内存完成实例化分配,并且堆也无法扩展时,会抛出OOM异常。

5.方法区(线程共享)

方法区(Method Area)用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虚拟机规范把方法区描述为了堆的一个逻辑部分,但有个别名Non-Heap。

6.运行时常量池(线程共享) 

运行时常量池(Runtime Constant Pool) 是方法区的一部分。

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

Java虚拟机对Class文件中的每一部分的格式都有严格的规定。每一个字节用于存储哪种数据都必须符合规范才会被虚拟机认可、装载和执行,但是对于运行常量池,Java虚拟机规范没有做任何细节的要求,不同的虚拟机可以按照自己的需求进行实现。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定要只有编译的时候产生,运行期间也可能将新的常量放入池中,利用的多的便是String类的intern()方法。

在无法再申请到内存是会排除OOM异常。

二、HotSpot虚拟机对象探秘

HotSpot虚拟机在Java堆中对象分配、布局和访问过程的探讨。

1.对象的创建

Java是面向对象的语言,在程序的执行过程中随时都有可能会有对象被创建,从语言层面上讲,创建对象(例如克隆,反序列化)通常仅仅是new关键字。

过程:

加载类:当虚拟机遇到一个new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且通过检查这个符号引代表的类是否已经被加载、解析和初始化过。如果没有则执行相应的类加载过程。(此处不讨论这部分的具体内容,详情参考第七章)

分配内存:在类加载通过后,虚拟机要为新生对象分配内存,对象所需要的内存在类加载后就可以确定,为对象分配空间就是从Java堆中划分出来。此时常见有两种方法:(1)指针碰撞:若堆中内存是绝对规整的,已经使用的在一边,没有使用的在一边,中间有个分割指针,则只需要把分割指针向没有使用的内存移动所需要的固定大小(2)空闲列表:Java堆内存不是规整的,已使用的未使用的是相互交错的,虚拟机就要维护一个列表,列表保存哪些是已经使用的,哪些是未被使用的,分配的时候只需要从未被使用的内存中分配固定的大小给新对象。

选择哪种方法由堆内存是否规整所决定,而是否规整取决于采用的垃圾收集器是否带有压缩整理功能。

可以很明显的看出,这种分配空间的方法不是线程安全的,可能在对A对象分配时指针还没来得及修改,对象B就采用了原来的指针,解决这个问题有两种方案:

 (1)对分配空间的动作进行同步化处理:实际上虚拟机采用CAS配上失败重试的方式保持更新操作的原子性(要么全部做完,要么一点也不做)。

 (2)把内存分配的动作按照线程划分在不同的空间上进行,即每个线程在Java堆中预先分配一块内存,成为本地线程分配缓冲TLAB。哪个线程要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。

内存空间初始化:内存分配结束后,虚拟机将分配的内存空间都初始化为零值(不包括对象头,分配的内存空间的具体内容下一部分会解释),如果使用TLAB来完成,这一工作可以提前到TLAB分配时进行,这一操作保证了对象的实例字段在Java中不赋初始值就可以直接使用,即零值。

对象头设置:接下来,虚拟机要对对象进行必要的设置,例如:是哪个类的实例?如何找到类的元数据信息?哈希码?对象的GC分代年龄等。这些信息存放在对象的对象头(Object Header)中。

初始化:从虚拟机视角来看,一个新的对象已经产生,但是从Java程序来看,这才刚刚开始,因为内存的数据内容都是零值,执行new之后,会执行<init>方法,按照程序员的要求来进行初始化。

2.对象的内存布局

分为三部分:

(1)对象头

 ①存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。32位虚拟机中为32bit,64位虚拟机中为64bit,官方称之为"Mark Word"。但是需要的存储的运行时数据太多,其实已经超过了32bit或者64bit,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的效率Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态来复用自己的存储空间。例如在32位的HotSpot虚拟机中,如果对象处于未被锁的状态下,那么就不储存和锁有关的信息,25bit存储hashcode,4bit存储GC,2bit存储锁标志位、1bit固定为0。

 ②类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的示例。并不是所有的虚拟机实现都必须通过数据上保留类型指针(和对象的访问定定位有关,下面会进行解释)。如果对象是一个Java数组,那在对象头还必须有一块来记录数组长度的数据,因为从数组的元数据中无法确定数组的大小。 

(2)实例数据

对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容,无论是父类继承下来的还是子类中定义的,都要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldAllocationStyle)和字段在Java源码中定义的顺序有关。HotSpot虚拟机默认的分配策略为: longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers),从策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个的前提下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认就是true),那么子类之中较窄的变量也可能插入到父类变量的空隙之中。

(3)对齐填充

 不是必然存在的,也没有特殊的含义,仅仅是占位符的作用。由于HotSpot VM 的自动内存管理必须要求对象的起始地址必须是8字节的整数倍,换句话说就是对象的内存大小必须是8字节的整数倍,如果不足,则填充到8的整数倍。

3.对象的访问定位

即栈内存中的引用如何访问堆内存中的对象。目前主流的方式有两种:

(1)使用句柄:Java堆中会划分一块区域作为句柄池,栈中存储的就是对象的句柄地址,而句柄中包含了对象实例数据指针(指向堆内存中)和类型数据指针(指向方法区)。

(2)直接指针:栈中存储的直接就是对象地址,而堆中对象的布局要包括对象示例数据和类型数据指针(指向方法区)。

使用句柄的优点: 引用中存储的稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的行为),只会改变句柄的实例数据指针,而引用本身不会改变。

直接指针的优点: 速度快,节省了一次指针定位的时间开销。由于对象访问十分频繁,极少成多之后也是可观的执行成本。HotSpot虚拟机是使用第二种进行对象访问的。

三、实战:OOM异常

原书阐述了产生几种异常的方式和简单代码,具体参考原书内容。

以上这篇浅谈Java内存区域与对象创建过程就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java的内存机制详解

    Java把内存分为两种:一种是栈内存,另一种是堆内存.在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后(比如,在函数A中调用函数B,在函数B中定义变量a,变量a的作用域只是函数B,在函数B运行以后,变量a会自动被销毁.分配给它的内存会被回收),Java会自动释放掉为该变量分配的内存空间,该内存空间可以立即另做他用. 堆内存用来存放由new创建的内存数组,在堆中分配的内存,由Jav

  • 深入探讨Java内存区域

    一.概述 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间.Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示: 下面就每一个区域进行阐述. 二.运行时数据区域 程序计数器 程序计数器,可以看做是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里,字节码解释器工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支.循环.跳转.异常处理.线程恢复等基础功能都要依赖这个计数器来完成.

  • Java中五种不同方法的创建对象

    前言 作为Java开发者,我们每天都会创建大量的对象,但是,我们总是使用管理依赖系统(如Spring框架)来创建这些对象.其实还有其他方法可以创建对象,在接下来的文章中我会进行详细介绍. 1.使用new关键字 这是最常见的创建对象的方法,并且也非常简单.通过使用这种方法我们可以调用任何我们需要调用的构造函数. Employee emp1 = new Employee(); 0: new #19 // class org/programming/mitra/exercises/Employee 3

  • 深入分析Java内存区域的使用详解

    Java 内存划分: 在Java内存分配中,java将内存分为:方法区,堆,虚拟机栈,本地方法栈,程序计数器.其中方法区和堆对于所有线程共享,而虚拟机栈和本地方法栈还有程序计数器对于线程隔离的.每个区域都有各自的创建和销毁时间. 程序计数器:     作用是当前线程所执行的字节吗的行号指示器.Java的多线程是通过线程轮流切换并分配处理器执行时间方式来实现的.因此,每个线程为了能在切换后能恢复到正确的位置,每个线程需要独立的程序计数器. Java 虚拟机栈: 每个放在被执行的时候都会同时创建一个

  • 浅谈Java内存区域与对象创建过程

    一.java内存区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁.根据<Java虚拟机规范(JavaSE7版)>的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域. 1.程序计数器(线程私有) 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码

  • 浅谈Java内存区域划分和内存分配策略

    如果不知道,类的静态变量存储在那? 方法的局部变量存储在那? 赶快收藏 Java内存区域主要可以分为共享内存,堆.方法区和线程私有内存,虚拟机栈.本地方法栈和程序计数器.如下图所示,本文将详细讲述各个区域,同时也会讲述创建对象过程,内存分配策略, 和对象访问定位原理.觉得写得好的,可以点个收藏,绝对不亏. Java内存区域 程序计数器 程序计数器,可以看作程序当前线程所执行的字节码行号指示器.字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支.循环.跳转.异常处理都需

  • 浅谈java+内存分配及变量存储位置的区别

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象) ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久

  • 浅谈java内存管理与内存溢出异常

    说到内存管理,笔者这里想先比较一下java与C.C++之间的区别: 在C.C++中,内存管理是由程序员负责的,也就是说程序员既要完成繁重的代码编写工作又要时常考虑到系统内存的维护 在java中,程序员无需考虑内存的控制和维护,而是交由JVM自动管理,这样就不容易出现内存泄漏和溢出的问题.然而,一旦出现内存泄漏和溢出方面的问题,如果不了解JVM的内存管理机制就很难找到错误所在. 1.JVM运行时数据区 JVM在运行java程序的时候会将它所管理的内存划分为若干个不同的区域,这些区域不仅有自己的用途

  • 浅谈Java中File文件的创建以及读写

    1.创建一个文件 @Test public void test6() throws IOException { File file1 = new File("C:\\IDEA\\h1.txt"); if(!file1.exists()){//文件不存在 file1.createNewFile(); System.out.println("创建成功"); }else{//文件存在 file1.delete(); System.out.println("删除成

  • 浅谈java中对集合对象list的几种循环访问

    java中对集合对象list的几种循环访问的总结如下  1 经典的for循环 public static void main(String[] args) { List<String> list = new ArrayList(); list.add("123"); list.add("java"); list.add("j2ee"); System.out.println("=========经典的for循环=======

  • 浅谈java web中常用对象对应的实例化接口

    1. request对象 是javax.servlet.HttpServletRequest接口的实例化 2. response对象 是javax.servlet.HttpServletResponse接口的实例化 3. session 对象 是javax.servlet.HttpSession接口的实例化 4. application对象 是javax.servlet.ServletContext接口的实例化 以上是常用的对象 5. pageContext对象 是javax.servlet.j

  • 浅谈Java内存泄露

    纳尼,Java 不是自动管理内存吗?怎么可能会出现内存泄泄泄泄泄泄漏! Java 最牛逼的一个特性就是垃圾回收机制,不用像 C++ 需要手动管理内存,所以作为 Java 程序员很幸福,只管 New New New 即可,反正 Java 会自动回收过期的对象... 那么 Java 都自动管理内存了,那怎么会出现内存泄漏,难道 Jvm 有 bug? 不要急,且听我慢慢道来.. 怎么判断可以被回收 先了解一下 Jvm 是怎么判断一个对象可以被回收.一般有两种方式,一种是引用计数法,一种是可达性分析.

  • 浅谈JAVA内存分配与参数传递

    JAVA中方法的参数传递方式只有一种:值传递. JAVA内存分配: 1.栈:存放 基本类型的数据.对象的引用(类似于C语言中的指针) 2.堆:存放用new产生的数据 3.静态域:存放在对象中用static定义的静态成员 4.常量池:存放常量 5.寄存器 6.非RAM存储 class BirthDate{ private int day; private int month; private int year; public BirthDate(int d,int m,int y){ day=d;

  • 浅谈JAVA 内存流的实现

    掌握内存操作流 输入和输出都是从文件中来的,当然,也可将输出的位置设置在内存上,这就需要ByteArrayInputStream和ByteArrayOutputStream ByteArrayInputStream:将内容写入到内存中, ByteArrayOutputStream:将内存中数据输出 此时的操作应该以内存为操作点. 利用此类 完成一些功能. 常用方法 ByteArrayInputStream :是InputStream子类. public class ByteArrayInputS

随机推荐