首页 > 代码库 > 第二章 Java内存区域与内存溢出异常

第二章 Java内存区域与内存溢出异常

技术分享
 1、程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
 
  2、Java虚拟机栈(Stack)线程私有,每一个方法调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)、对象引用(reference类型)和returnAddress类型。
 
  3、本地方法栈(Native Method Stack),为虚拟机的本地方法提供的栈
 
  4、java堆(Heap) 所有线程共享的一块内存区域,存放对象实例。可能会被分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
 
  5、方法区(Method Area) 各纯种共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。通常被 称为 永久代(Permanent Generation)
 
  6、运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
 
  7、直接内存(Direct Memory)

2.3对象的创建

         1、检查加载:遇到New指令 ,首先检查这个指令的参数是否能在常量池定位到类的符号引用,并且检查这个符号引用的代表的类是否已经被加载、解析和初始化过;

         2、划分内存:为对象分配空间等同于把一块确定大小的内存从Java堆中划分出来,而这时根据内存在堆中的存储方式分为“指针碰撞”和“空闲列表”,但这时在并发情况下并不是线程安全的,解决方法有两种,一种对分配内存空间动作进行同步处理,另一种内存分配的动作按照线程划分在不同的空间中进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB);

         3、初始化:内存分配后将分配的内存空间都初始化为0(不包括对象头);

         4、对对象进行必要设置(类得元数据信息、对象的哈希码、GV分代)存储在对象头中


对象的内存分布

         对象头、实例数据、对齐填充

         对象头第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,线程持有的锁等(Mark work),第二部门是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实力

『注』如果对象是一个数组,那在对象头中还必须有一块用于记录对象数组长度的数据,应为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小,这里也就说明for(int i= arr.length; i>0; i--)和for(int i= 0; i<arr.length;i++)并不会有本质区别。

         实例数据:无论从父类继承的还是子类定义的都需要记录下来,受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响;例如相同宽度的字段分派在一起(double,long)

         对齐填充并不是必然存在的起着占位符的作用

对象的访问定位

         需要通过栈上的reference护具来操作堆上的具体对象。主流的访问方式有两种,句柄和直接指针访问;

         句柄访问:堆中会划分出一块内存作为句柄池,reference中存储就是对象的句柄地址,句柄中也包含了对象实例数据与类型数据各自的具体地址信息

         直接指针访问,在regerence存储的就是对象的地址

         优劣分析:句柄最大的好处就是regerence中存储的是稳定的句柄地址,在对象被移动是只会改变句柄中的实例数据指针,而reference本身不受改变,直接指针的好处就是速度更快,



内存溢出情况:
    虚拟机栈的溢出,,一般都会提示stackOverflowError,如果提示OutMemoryError则可能是多线程导致的,当java虚拟机为每一个线程都分配一个虚拟机栈时,此时每个线程的栈都没有溢出也就不会触发StackOverflowError,但是线程一直在分配导致了OutMemoryError。

    其他的溢出情况都比较简单。



1.定义
     栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
     每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
     对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

技术分享


2.组成
 (1)局部变量表
      局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的
      最大局部变量表的容量。
      局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
      reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
      returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
      虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
      Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
  系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
 (2)操作数栈
      Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
      操作数栈也常被称为操作栈。
     和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
      虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
      虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:


  1. begin
  2. iload_0 // push the int in local variable 0 onto the stack
  3. iload_1 // push the int in local variable 1 onto the stack
  4. iadd // pop two ints, add them, push result
  5. istore_2 // pop int, store into local variable 2
  6. end
    在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。

技术分享










第二章 Java内存区域与内存溢出异常