您的位置:首页 > 编程语言 > Java开发

深入理解JVM—第二章:Java内存区域与内存溢出异常

2018-04-01 22:00 746 查看

1,概述

Java较C、C++,Java可以利用虚拟机的自动内存管理机制,避免繁琐的内存分配与回收。不容易出现内存泄漏和内存溢出问题。

内存泄漏:指程序申请到的内存空间不再归还(无法归还),可使用完该内存空间的程序也不能再访问该空间(可能是丢失了该内存空间的地址)。

内存溢出:指程序想申请的内存空间,系统不能满足,超出系统空闲内存空间。

2,运行时数据区域

2.1 程序计数器

它是一块较小的内存空间,可看做是字节码的行号指示器。字节码的解释器工作时就是通过改变这个程序计数器的值来选取下一条需要执行的字节码指令。

每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此,该内存区域成为“线程私有”的内存。

若线程在执行java方法,则PC计数器记录的是虚拟机字节码指令的地址。若执行的方法是native方法,则这个计数器值为空。该内存区域是java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。

ps:native方法作用:
①直接访问操作系统底层(如系统硬件)
②访问一个老的系统或已有的库,而该系统或库不是有Java编写的
③提高程序性能

2.2 Java虚拟机栈

Java虚拟机栈是线程私有的,每个线程对应一个虚拟机栈。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,方法执行,栈帧入虚拟机栈,方法执行完成,栈帧出虚拟机栈。

栈帧主要存储的信息如下四样:
①局部变量表
存储了方法参数和方法内定义的局部变量,其最大容量在.java文件编译成.class文件的时候已经确定好。写在Class文件的Code属性的max_locals中。
②操作数栈
在方法执行过程中,字节码指令会往操作数栈中写入和读取内容。操作数栈的最大深度也是在.java文件编程为.class文件的时候就已经确定好的。写在Class文件的Code属性的max_stacks数据项中。
③动态链接
指向运行时常量池中该栈帧所属方法的符号引用。并且该符号引用在运行期间才解析成直接引用。
④方法返回地址
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令。此时返回的地址一般是调用者的PC计数器的值,其地址在栈帧中存。
异常完成出口:在方法体内出现异常没得到解决。其返回地址要通过异常处理表来确定,栈帧中不保存其信息。

Java虚拟机规范中,规定两种异常状况:
①线程器请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
②可自动拓展的虚拟机栈(大部分都可以),如果拓展时无法申请到足够的内存,抛出OutOfMemoryError异常

2.3 本地方法栈

本地方法栈类似于虚拟机栈,不过本地方法栈是为虚拟机使用到Native方法而服务。

对于Sun HotSpot虚拟机直接把本地方法栈跟虚拟机栈合二为一。

本地方法栈类似虚拟机栈也会抛出StackOverflowError和OutOfMemoryError异常。

2.4 Java堆

Java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域。Java堆可细分为:新生代和老年代。

ps: 新生代:指很快被回收或不是特别大的对象
老年代:指经历好几次回收仍或者或特别大的对象

Java虚拟机规范中,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。当前主流的虚拟机都是按照拓展来实现的,在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出OutOfMemoryError异常。

2.5 方法区

存储已被虚拟机加载的类信息、静态变量、常量池和即时编译器编译后的代码等数据。

方法区是被各个线程所共享的内存区域。方法区也被称为“永久代”

对于方法区的回收成绩往往不如人意,但是对方法区的回收是必要的。该区域的内存回收目标是针对常量池的回收和对类型的卸载。

Java虚拟机规范中,当方法区无法满足内存分配需求的时候,就会抛出OutOfMemoryError异常。

2.6 运行时常量池

存储在编译器生成的各种字面量和符号引用,以及符号引用解析后得到的直接引用。

运行时常量池具有动态性,不仅编译期可以产生常量,而且在运行期间也可以将新的常量放入池中,如String类的intern()方法。

ps:
JDK1.7前:String类的intern()方法用于判断常量池中是否有该字符串,有,则返回其引用,无,则先在常量池中添加该字符串,再返回其引用。
JDK1.7及之后:将常量池从永久代移到Java堆中,String类的intern()方法用于判断常量池中是否有该字符串,有,则返回其引用,无,则记录该实例的引用。

运行时常量池是方法区的一部分,当常量池无法申请到方法区的内存时,抛出OutOfMemoryError异常。

2.7 直接内存(堆外内存)

直接内存就是堆外内存,它并不是java虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分内存也被频繁地使用。

在JDK1.4中加入一个NIO类,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存。然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样子可以避免数据在Java堆和Native堆中来回复制。

直接内存是受OS管理的,属于内核态,Java堆是受JVM管理的,属于用户态。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了数据从用户态向内核态的拷贝。

PS:关于直接内存的详细介绍,引用库昊天的一遍博客文章,链接为:堆外内存

3,HotSpot虚拟机对象探秘

3.1 对象的创建

①遇到new指令,先判断这个类在方法区中有没有被加载了。具体操作:去常量池中找该类的符号引用,并检查该符号引用所代表的类有没有被加载、准备、解析、初始化。若有则下一步,没有则执行类加载过程。

②在类加载检查通过后,为新生对象在Java堆中分配内存。先看该Java堆中使用的垃圾收集器是否带有压缩整理功能。
p1:有压缩整理功能,说明Java内存是绝对规整的,利用一个指针作为已用内存和空闲内存的分界点指示器,分配内存时向空闲内存那边挪动一段与对象大小相等的距离,这种分配方式叫做“指针碰撞”。
p2:如果没有压缩整理功能,说明Java内存是不规整的,虚拟机需要维护一个列表,记录哪些内存块是可用的。在分配内存时找到一块足够大的内存空间划分给对象使用,同时更新列表信息。

解决多线程中,分配内存遇到的问题:
第一种:对分配内存空间的动作进行同步处理—-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性(原子性就是中途出现问题则回滚,只有同时成功或同时失败)。
第二种:把内存分配的动作按照线程划分在不同的空间上进行,每个线程先预先分配一块内存,成为本地线程分配缓冲TLAB。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

③内存分配完成后,为实例字段初始化为零值。保证对象的实例字段在Java代码中可以不赋初始值就直接使用。

④虚拟机再对对象进行设置。如这个对象是哪个类的实例、该类的元数据信息在哪里、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。

⑤执行 < init > 方法,把对象按照程序员的意愿进行初始化。

3.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头、实例数据和对齐填充

①对象头

对象头包括两部分信息。
第一部分用于存储对象自身的运行时数据,简称Mark Word。包含哈希码、GC分代年龄、锁状态标记、线程持有锁、偏向线程ID等。
第二部分是类型指针,它指向其类元数据的指针,虚拟机根据这个指针确定这个对象是哪个类的实例。
如果对象是一个数组,在对象头还需要有一块记录数组长度的数据。因虚拟机可以根据数组中java对象的元数据信息确定对象的大小,但是无法知道数组的大小。

PS:元数据就是描述数据的数据,而注解就是源代码的元数据。如方法上加上注解@override,其代码的元数据描述这是父类方法的重写。

②实例数据

实例数据就是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内存。从分配策略上来看,相同宽度的字段总是被分配到一起。

③对齐填充

对齐填充并不是必须的。它仅仅起到占位符的作用。从而保证在自动内存管理系统中要求的对象起始地址为8字节的整数倍。就是说对象的大小必须是8字节的整数倍,而对象头部分刚好就是8字节的倍数,所以当实例数据部分没有对齐的时候,就需要用到对齐填充来补全。

3.3 对象的访问定位

在使用对象的时候,需要通过栈上的reference数据来操作堆上的具体对象。

访问对象的两种主流方式:
①使用句柄:在Java堆中建立句柄池,reference指向对象的句柄地址,而句柄中包含了对象实例数据指针和对象类型数据指针。

②使用直接指针:

reference直接指向Java堆中对象的起始地址,其对象中保存了对象类型数据的指针。

两种访问方式的优势:
句柄:好处是在reference中存储的是句柄池中该对象的句柄地址,在GC后,对象被移动,仅仅需要改变句柄中的实例数据指针,而reference不用修改。
直接指针:好处就是节省一次指针定位的时间开销,在对象被频繁访问的情况下,积小成多,很有效的节约成本。Sun HotSpot使用该方式来访问对象。

—–参考《深入理解Java虚拟机》 周志明 著

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: