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

2.1 概述

  主要介绍Java虚拟机内存是如何划分的。

2.2 运行时数据区域

  Java虚拟机就是一个进程,它被分配了自己的内存空间,在JVM执行Java程序的时候会把自己分配到的内存区域划分成若干不同功能的区域。

2.2.1 程序计数器

  类似于CPU寄存器里的IP的作用,用于指示下一条要执行的指令的地址。当然也不是绝对的,如果执行的是Java指令,那么指向的是下一条要执行的指令的地址,如果要执行的是Native方法,该值为0。这是内存区域里唯一不会发生OOM的区域。

  Java虚拟机多线程的实现是通过时间片流转的方式,一个线程执行一段时间后让另一个线程去执行。当线程恢复的时候必须能够找到上一次执行到了哪一条,在恢复现场的相关方法中就会恢复程序计数器里的值。所以这个是线程私有的区域。

2.2.2 Java虚拟机栈

  线程私有的内存区域,一个线程可能会执行多个方法,每执行一个方法的时候,就会把一个栈帧压入栈中,方法执行完毕后,栈帧就会出栈。栈帧里存储这局部变量表、操作数栈、动态链接等信息。

  局部变量表里存放了编译器可知的各种基本数据类型、对象引用。一个方法里要使用的变量肯定是局部变量,他就放在局部变量表里。

2.2.3 本地方法栈

  执行native方法的时候回使用的栈。有的虚拟机实现把本地方法栈和虚拟机栈合二为一。

 

2.2.4 Java堆

  新建的对象都分配在这个区域上,包括加载类后创建的Class对象。既然是对象,那么这里的内存区域大多是线程共享的,但是为了提高对象的分配速度,也会创建一些线程私有的区域,比如分配缓冲区(Thread Local Allocation Buffer)。这里是垃圾回收算法的主要关注区域。

2.2.5 方法区

  线程共享的内存区域,用于存放加载的类信息、常量、静态变量、即时编译或的代码。

2.2.6 运行时常量池

  Class文件里有接口、方法。属性等信息之外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容在类被加载进方法区后被放入了运行时常量池里。

2.2.7 直接内存

  直接内存并不是虚拟机运行时数据区的一部分。NIO的工作原理是基于通道和缓冲区的I/O方式,利用Native函数直接分配堆外内存,然后通过一个存储在堆里的对象作为堆外内存的引用对堆外内存进行操作,这样能够提高性能。虽然堆外内存不受Java堆大小的限制,但是会受到计算机本身内存大小的限制。

 

2.3 HotSpot虚拟机对象探秘

  主要讲了再Java堆中对对象的分配、布局和访问的过程

2.3.1 对象的创建

  这里讨论的对象仅仅指的是普通的对象,不包括数组和Class对象。

  new一个对象的时候,首先会去检查new指令的参数能否在常量池里定位到一个类的符号引用,并检查是否加载、解析、初始化,如果没有就执行相应的加载的过程。这是书上的原话,什么叫new的参数呢?比如People p = new People(),People是new的关键字,这种字符串会被放到Java的常量池里,Java会检查People是否指向一个类,在该字符串是一个类的名称的时候,即字符串是一个符号引用的时候,再去检查该类是否完成了加载的过程,如果没有,则加载。

  确定类加载完毕后就要创建对象了,即为创建的对象分配内存。类是对象的模板,对象是类的一个特例,所以在一个类被成功加载后,由该类创建的对象所需要的内存空间大小是确定的,为一个对象分配内存即在堆区域里找出一块分配给对象。如果堆的内存是完全规整的,用过的内存放在一边,没有用的放在另一边,中间有一个指针指向分界线,所以分配内存的时候只要挪一挪这个指针就可以了,这被称为指针碰撞。如果堆的内存不是连续的,需要设计一个列表去维护空闲内存的大小,分配内存的时候,在空闲内存里找到一个大小合适的,并分配。如何在堆里分配内存取决于堆的内存的状态,而堆的内存状态取决于垃圾回收算法。

  无论是指针碰撞,还是空闲链表,都不是线程安全的。比如指针碰撞的分配并修改的这个过程。有可能一个线程分配了内存但是没有修改指针,所以该内存会被再次分配。解决方案有两种,一种是通过某些措施使得分配的过程是线程安全的。另一种是在堆里为每个线程换分私有的区域,为线程分配内存的时候只在其私有的区域里划分。这个私有的区域就是上面提到的TLAB。

  内存被分配完毕后,全被置为0,这就是为什么一个对象的全局属性没有被初始化也可以使用。

  然后是设置对象头里,比如锁的状态,GC代。

  最后一步是执行init方法,把对象按照程序员的要求正确的初始化。这个init方法是构造器的方法,还是对象默认值那些方法??

2.3.2 对象的内存布局

  一个对象,大体可以分为三个部分,对象头,实例数据,对齐填充。

  对象头的信息可以分为两部分,一部分是对象运行时的数据,称为Mark Word。比如哈希码,Gc分代年龄,线程偏向的ID,锁的标志等等。这部分的数据在对象处于不同的状态的时候会被复用,详情可以参见《并发编程的艺术》。对象头的另一部分的内容是类型指针,指向该对象是哪个类的实例。如果对象是一个数组,那么对象头里还要存一个数组的长度。

  接下来的实例数据是对象真正存储有效信息的地方,如代码里定义的各种类型的字段的内容,也包括从父类继承。

  最后的对齐很好理解,JVM要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,所以如果不是的情况下就会填充。

2.3.3 对象的访问的定位

  当我想使用一个对象的时候,怎么定位到。我们都知道是引用reference,引用是如何使用的并没有强制的要求。分为两种:句柄和直接指针。

  直接指针最好理解,也是我们常用的虚拟机HotSpot里使用的方式,reference里存的是对象的直接的地址。句柄比较复杂,reference存的是句柄池的地址,然后在句柄池里存在着对象的真实的地址。

  两种方法各有优劣,直接地址法的优点是速度快,省去了一次定位的代价,由于对象的访问在Java里十分的频繁,这种节约可以带来可观的影响。句柄池的优点是在发生垃圾回收的时候,句柄池的地址不会改变。

 

2.1 概述

 

posted @ 2019-01-18 11:28  AshOfTime  阅读(156)  评论(0)    收藏  举报