深入理解java虚拟机(第二版)读书笔记

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

2.2 运行时数据区域

java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
这里写图片描述

2.2.1 程序计数器

是一块较小的内存,可以看作是当前线程所执行的字节码的行号指示器。分支,循环,跳转,异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。所以每个线程都需要有一个独立的程序计数器嘛,这个区域也是java虚拟机中唯一一个不会内存溢出的区域。

2.2.2 java虚拟机栈

也是线程私有的。每个方法执行前都会创建一个栈帧,用来存储局部变量表、操作数帧、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这里规定了两种异常:第一种是线程请求的栈的深度大于虚拟机所运行的深度,将会抛出StackOverflowError异常;第二种是如果虚拟机栈可以动态扩展,但是扩展的时候无法申请到足够的内存,就会抛出内存溢出的异常。

2.2.3 本地方法栈

与虚拟机栈类似,前者为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机用到的native方法服务。

2.2.4 java堆

被所有线程共享,用来存放对象实例。在虚拟机启动的时候创建。随着JIT编译器的发展等技术逐渐成熟,所有的对象都分配在堆上也渐渐的变得不那么“绝对”了。JAVA堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC”堆。

2.2.5 方法区

各个线程共享。用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。垃圾收集行为在这个区域是比较少出现的。但是这部分区域的回收确实是必要的。

2.2.6 运行时常量池

方法区的一部分,用于存放编译期生成的各种字面量和符号引用。具有动态性,并不一定要求常量一定只有编译期才产生。运行期间也可能将新的常量放入。(String类的intern()方法)

2.2.7 直接内存

不是虚拟机运行时数据区的一部分,也不是虚拟机规范种定义的内存区域,但是也被频繁使用,而且也可能导致内存溢出。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆种的对象对这块内存的引用进行操作。能在一些场景提高性能,因为避免了在java堆和Native堆种来回复制数据。

2.3HotSpot虚拟机对象探秘

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

2.3.1 对象的创建

类加载检查:虚拟机碰到一个New指令时,首先检查这个指令的参数是否能在常量池种定位到一个类的符号引用,并检查这个类是否已经被加载,解析和初始化过。如果没有,就必须先执行相应的类加载过程。

为新生对象分配内存:把一块确定大小的内存从Java堆种划分出来。有两种方法,一种是指针碰撞。假设堆中内存是绝对规整的,将用过的内存放一边,没用的放一边,中间放着一个指针,分配内存的时候只要把指针像没用的那边移动。第二种是空闲列表,此时堆中的内存是相互交错的。维护一个列表,记录哪些内内存块是可用的,分配的时候从列表中找出一块足够大的区域然后更新列表。

扫描二维码关注公众号,回复: 394239 查看本文章

线程安全处理:一种是对分配内存空间的动作进行同步处理(采用CAS保证更新操作的原子性)一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存。那个线程需要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

对象的一些设置:将分配的内存空间都初始化为零值,不包括对象头。

2.3.2 对象的内存布局

对象头:包括两部分信息。第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。(Mark Word)。第二部分是类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针确定这个对象是哪个类的实例。(如果对象是一个JAVA数组,那在对象头中还必须有一块用于记录数组长度的数据。)

实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充:没有特别的含义,起着占位符的作用。对象的大小必须是8字节的整数倍,对象头部分是8字节的一倍或者两倍,所以当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

2.3.3 对象的访问定位

句柄访问:java堆中会分配出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。好处:句柄的稳定,在对象被移动的时候(垃圾收集时经常需要移动对象),此时只需要改变句柄中的实例数据指针,而reference本身不用改变。
这里写图片描述
直接指针访问:reference中存储的直接就是对象地址,java堆对象应考虑如何放置访问类型数据的相关数据。最大好处就是速度快,节省了一次指针定位的世界。对象的访问在java中非常频繁,所以积少成多后就是一项非常可观的成本。(HotShop采用的就是这种)
这里写图片描述

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

java堆用于存储对象实例,不断地创建对象,并且保证GC Roots都可达,这样垃圾回收机制就无法清楚这些对象,达到最大堆的容量后就会内存溢出。解决手段通过内存映像分析工具。

2.4.2 虚拟机栈和本地方法栈溢出

两种异常查看2.2.2。实验发现:在单线程的情况下,无论是帧栈太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。如果是多线程的情况下,通过不断建立新的线程的方式可以产生内存溢出异常。这样的内存溢出与栈空间是否足够大没有联系,或者说为每个线程的栈分配的大小越大,越容易内存溢出。因为操作系统给每个进程分配的内存是有限制的,线程多了就不够分了,剩下的内存就慢慢耗尽了。

2.4.3 方法区和运行时常量池溢出

实验思路:运行时产生大量的类去填满方法区,直到溢出。
方法区溢出也是一种常见的内存溢出异常,因为一个类要被垃圾收集器回收掉,判定条件是非常苛刻的。所以在经常动态生成大量class的应用中,需要特别注意类的回收状况。

2.4.4 本机直接内存溢出

计算得知内存无法分配, 于是手动抛出异常。

猜你喜欢

转载自blog.csdn.net/ArcheH/article/details/80262758