《深入理解Java虚拟机》(一):Java内存区域与内存溢出异常

第一次读《深入理解Java虚拟机》,理解的不是很深入。所以在第二遍阅读的时候,通过博客来记录自己阅读中的思考和理解,达到更加清晰深入的认识!!!

运行时数据区域

这里写图片描述

线程私有区域

一、PC寄存器(Program Counter Register)

PC寄存器是一块较小的内存空间,它可以看作时当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。在任何一个确定时刻,一个内核只会执行一条线程。Java虚拟机的多线是通过 轮流切换并分配处理器执行时间实现的(线程频繁切换会导致性能问题)

  • 存储容量:至少保存一个returnAddress类型的数据或者一个与平台相关的本地指针。
  • 存储内容:如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行的Native方法,计数器的值为空(Undefined)。
  • 内存溢出: PC寄存器是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

二、虚拟机栈(Java Virtual Machine Stacks)

虚拟机栈描述的是Java方法执行的内存模型。Java虚拟机栈的作用与传统语言(例如C语言)中的栈非常类似,用于存储局部变量与其他一些尚未算好的结果。Java虚拟机栈所使用的内存不需要保证是连续的。(注意区分Stack、Heap与Java(VM)Stack 、Java(VM) Heap)

这里写图片描述

  • 存储内容:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态链接、方法出口等信息。
  • 存储容量:Java虚拟机规范即允许Java虚拟机栈被实现成固定大小,也允许根据计算动态的扩展和收缩。(栈容量由-Xss参数进行控制)
  • 内存溢出:
    • 在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
    • 多线程下,为栈分配的内存越大,越容易造成内存溢出异常。

三、本地方法栈(Native Method Stack)

Java虚拟机实现可能会使用到传统的栈(通常称为 C stack)来支持native方法(使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈。本地方法栈与虚拟机栈所发挥的作用非常相似,区别在于一个为的Java方法服务,一个是为native方法服务。在有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

线程共享区域

一、Java堆(Java Heap)

在Java虚拟机中,堆(Heap)是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。(这点随着技术的发展已经不是那么绝对了)Java堆是Java虚拟机管理的最大的一块内存,它存储了被自动内存管理系统(automatic storage management system),也就是常说的garbage collector(垃圾收集器)所管理的各种对象,这些受管理的对象无需也无法显式的销毁。

  • 存储内容:所有类实例和数组对象分配内存的区域。
  • 存储容量:Java堆可以固定大小,也允许随着程序执行的需求进行扩展和收缩(堆容量由最大值-Xmx、最小值-Xms进行控制)
  • 内存溢出:如果实际所需的堆超过了GC堆能提供的最大容量,虚拟机将抛出OutOfMemoryError异常,并进一步提示:Java heap space

二、方法区(Method Area)

方法区也是线程共享的内存区域。Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个别名叫Non-Heap(非堆)。(在JDK1.8中为元数据区)

  • 存储内容:存储了每一个类的结构信息,例如运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
  • 存储容量:方法区容量可以固定大小,也允许随着程序执行的需求进行扩展和收缩,方法区的内存可以不是连续的。(-XX:MetaspaceSize、-XX:MaxMetaspaceSize)
  • 内存溢出:如果方法区的内存空间不能满足内存分配要求,虚拟机将抛出OutOfMemoryError异常,并进一步提示:PermGen space

除了运行时数据区域,被频繁使用的还有直接内存(Direct Memory)。它不属于虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是它也可能导致OutOfMemoryError异常。

HotSpot虚拟机对象

对象创建

一、分配内存方式

在Java虚拟机中,新生对象分配内存主要有两种方式指针碰撞(Bump the Pointer)空闲列表(Free List)。虚拟机使用哪种分配方式的是由Java堆是否规整决定的,而Java堆是否规整又由压缩整理功能决定的。所以,在使用Serial、ParNew等带有Compact过程的收集器时,通常采用指针碰撞。使用CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表。

  • 指针碰撞:
    假设java堆是绝对规整的,所有使用过的内存在一边,没有使用过的内存在另一边,中间放有一个指针作为分界点指示器。那这样为新生对象分配内存时,只需要将指针移动对应大小的距离,这种方式称为“指针碰撞”。
  • 空闲列表:
    如果Java堆不是规整的,虚拟机就必须维护一个列表来记录哪块内存被使用了,哪块内存还未使用,在分配内存的时候,会找到足够大的内存分配给新生对象并更新列表,这种方式称为“空闲列表”。

二、并发安全措施

分配内存是一个频繁的行为,为了保证在并发情况下的线程安全,采用了两种解决方案CAS和TLAB

  • CAS:对分配内存空间的动作进行同步操作。
  • TLAB:为每个线程在Java堆中预先分配一块内存,在线程需要分配内存时,直接在预留的内存上进行分配,当TLAB用完后,分配新的TLAB时才需要同步锁定。(JDK1.7默认开启了TLAB,TLAB极大的提高了程序性能,开启/关闭TLAB -XX:+/-UseTLAB )

对象内存布局

一、对象头(Header)

  • 运行时数据:
    哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启指针压缩)中分别未32bit和64bit,官方称为Mark Word
  • 类型指针:
    对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

二、实例数据(Instance Data)

对象真正存储的有效信息,也就是在程序代码中所定义的各种类型的字段内容,这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。

  • HotSpot分配策略(默认)longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略来看,相同宽度的字段总是被分在一起。

在满足这个前提下,在父类中定义的变量会出现在子类之前。(如果CompactFields参数值为True,那么子类中较窄的变量也可能插入到父类变量的空隙中)

三、对齐补充(Padding)

对齐补充并不是必然存在的,也没有特别含义,它仅仅起着占位符的作用。

对象的访问定位

Java程序需要通过栈上的referenece数据来操作堆上的具体对象,目前主流的方式有使用句柄和直接指针两种。

  • 句柄:Java堆中会划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据和类型数据各自的具体地址。
  • 指针:reference中存储的直接就是对象地址。

读后感

原书中 | Java内存区域与内存溢出异常 | 一章的知识点比较多,上面的总结基本都是摘抄自书中和 《Java虚拟机规范8版》还有一些博客的观点。

通过学习到的内容,分析下列代码,在运行时每个内存区域存储的数据:

public class ClassMemory {
 1. private static int i=1;
 2. private String string;
 3. public ClassMemory(String string) {
 4. 	this.string=string;
 5. }
 6. public void sayHello(){
 7. 	System.out.println(string+"  "+i);
 8. }
 9. public static void main(String[] args) {
 10. 	ClassMemory classMemory= new ClassMemory("andrew");
 11.    classMemory.sayHello();
	}
}

当前类运行在main线程中。

当虚拟机运行到ClassMemory classMemory= new ClassMemory("andrew");时,虚拟机会去进行初始化,当需要对某个类进行初始化时,就会进入加载阶段

一、方法区

通过Class文件字节码工具**javap ** 解析出ClassMemory类的Class文件的常量池信息和方法字节码。(方法区存放的内容不止这两种)

  • 运行时常量池(Runtime Constant Pool):
Constant pool:
   #1 = Class              #2             // chapter2/ClassMemory
   #2 = Utf8               chapter2/ClassMemory
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               string
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <clinit>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Fieldref           #1.#13         // chapter2/ClassMemory.i:I
  #13 = NameAndType        #5:#6          // i:I
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               <init>
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Methodref          #3.#19         // java/lang/Object."<init>":()V
  #19 = NameAndType        #16:#10        // "<init>":()V
  #20 = Fieldref           #1.#21         // chapter2/ClassMemory.string:Ljava/lang/String;
  #21 = NameAndType        #7:#8          // string:Ljava/lang/String;
  #22 = Utf8               this
  #23 = Utf8               Lchapter2/ClassMemory;
  #24 = Utf8               sayHello
  ...
  • 方法信息:
...
  public chapter2.ClassMemory(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #18                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #20                 // Field string:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lchapter2/ClassMemory;
            0      10     1 string   Ljava/lang/String;

  public void sayHello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #20                 // Field string:Ljava/lang/String;
         5: new           #25                 // class java/lang/StringBuilder
         8: dup_x1
         9: swap
        10: invokestatic  #27                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        13: invokespecial #33                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        16: getstatic     #12                 // Field i:I
        19: invokevirtual #35                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #39                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: putfield      #20                 // Field string:Ljava/lang/String;
        28: return
      LineNumberTable:
        line 11: 0
        line 12: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lchapter2/ClassMemory;
...                       

在虚拟机碰到new、getstatic、putstatic、invokestatic四个字节码指令时,都会触发类加载机制。也就是说,如果在其他类通过调用ClassMemory.i也会触发ClassMemory类的加载,所以类的解析信息被放入线程共享的方法区内。

二、Java堆

通过new创建成功实例对象时,实例对象会被放入Java堆中,通过JDK自带工具Java VisualVM可以看到生成的实例对象的具体信息。

这里写图片描述

三、虚拟机栈

当前线程为main线程,当执行到classMemory.sayHello();时,main线程栈帧进行入栈出栈操作。

在进行方法操作时,局部变量表、操作数栈、动态链接、返回地址都会通过实例对象中的指针找到方法区对应的位置获取各自的内容。(栈帧中局部变量表等东西不知道在哪里可以看到,所以这块基本上是猜测的,希望可以有大神帮我解惑 _)

四、总结

经过“缜密”的分析后,发现自己的知识面还不足以解释一个类的基本运行原理和存储位置,任重道远 !!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值