《深入理解Java虚拟机》第二版 第二章笔记

目录

二.Java内存区域与内存溢出异常

1  运行时数据区域

1.1  程序计数器(Program Counter Register)

1.2  Java虚拟机栈(JVM Stacks)

1.3  本地方法栈(Native Method Stack)

1.4  Java堆(Java Heap)

1.5  方法区(Method Area)

1.6  运行时常量池

1.7 直接内存

2.1 对象的创建

2.2 对象的内存布局

2.3 对象的访问定位

2.4 实战:OOM异常

2.5 总结

二.Java内存区域与内存溢出异常

1  运行时数据区域

        Java虚拟机既然是虚拟机,那它需要在内存中占据一片位置,用于存放运行时的数据和代码存放。

        这部分区域被称为运行时数据区域,当然硬件层面它就是计算机内存中的一片区域,与硬盘没有任何关系。

        它被划分为5块

        方法区,堆,虚拟机栈,本地方法栈,程序计数器。

 

1.1  程序计数器(Program Counter Register)

作用:

  • 用来指示当前线程所执行的字节码行号程序计数器在不同的虚拟机有不同的实现。
  • 字节码解释器需要通过程序计数器的值来决定下一条字节码的位置。
  • 分支,循环,跳转,异常处理,线程恢复等基础功能都需要来程序计数器完成

特点:

  • 是相对其它块,比较小的一块内存空间。
  • 为了线程切换后,能恢复上一次执行的位置,每个线程独享一个程序计数器。
  • 独享程序计数器后,各个线程之间的计数器互不影响
  • 因此程序计数器这块内存为线程私有内存。(这块区域JVM规定无OOM问题)

1.2  Java虚拟机栈(JVM Stacks)

作用:

  • 一块用于Java方法执行的内存每个方法执行时创建一个栈帧,这个栈帧(Stack Frame)主要存储了局部变量表,和操作数栈,动态链接,方法出口。我们所说的JVM 执行引擎是基于栈的时候,其中的“栈”指的就是操作数栈
  • 日常所说jvm的堆栈,栈一般指的就是虚拟机栈。或者局部变量表。

特点:

  • 局部变量表中存放了,编译期可知的各种基本数据类型对象引用(或对象引用的引用,对象引用就是存放了一个指向了该对象起始地址的指针)或一条字节码指令的地址
  • 局部变量表所需空间在编译器就确定,之后不会变。
  • 两种异常:栈溢出和内存溢出
  • 栈溢出:如果线程请求的栈深度大于虚拟机所允许的深度。比如递归调用,方法不断入栈入栈入栈入栈,啊呀,需要的栈深度大于限制的深度了。报错StackOverflowError
  • 内存溢出:但是当虚拟机支持动态扩展虚拟机栈,那么当你入栈入栈入栈超过限制,虚拟机扩大限制,当这个限制扩大到无法再申请到内存时,比如内存条爆满,或者对虚拟机限制了内存使用量,超过这个使用量,也会报错,此时报OOM。OutOfMemoryError

1.3  本地方法栈(Native Method Stack)

作用:

这个栈与Java虚拟机栈类似,区别在于它保存的方法不再是Java方法,而是Native方法,Native方法就是针对不同的运行环境,而实现的方法,它针对虚拟机所运行的环境而有不同的实现,顾名思义本地方法。

特点:

虚拟机规范中没有具体规定本地方法需要使用的语言使用方式以及数据结构

在HotSpot虚拟机(虚拟机也分很多版本)中直接将本地方法栈与虚拟机栈合二为一。

当然报的异常与Java虚拟机栈相同。栈溢出与内存溢出。

1.4  Java堆(Java Heap)

作用:

  • 存放对象的实例
  • 存放数组

特点:

  • Java虚拟机管理的内存最大的一块
  • 被所有线程共享
  • 在虚拟机启动时就创建
  • 唯一目的就是存放对象实例
  • 也被叫做GC堆(Garbage Collection Heap)
  • 被分为新生代老年代
  • 新生代又被分为一块较大的Eden区与两块较小的Survivor区
  • Java虚拟机规范中规定堆可以处于物理上不连续的内存空间中(关于堆空间的内存回收机制将作为重点放在第三章笔记中出现)

1.5  方法区(Method Area)

(在GC时也被称为永久代Permanent Generation,一般简写为Perm,一般别名叫Non-Heap非堆,我的理解是堆的一部分,因为作用和堆有所不同因此为了区分与堆不一样而得名。)

作用:

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

特点:

  • 被各线程共享
  • 被描述为堆的一个逻辑部分
  • 在HotSpot虚拟机中,在GC时常被称为永久代,实质并不等价。
  • 常被永久代来实现方法区,这样以至于GC分代收集扩展至方法区,垃圾收集器可以像管理堆一样管理方法区(永久代)
  • 其他虚拟机不存在永久代的说法
  • 有被放弃永久代,采用Native Memory 来实现方法区的规划
  • JDK 1.7的HotSpot中,原本在永久代的字符串常量池被移出
  • 虚拟机规范对方法区的限制宽松
  • 内存可以不连续,内存可选择为固定大小可扩展
  • 可以选择不实现垃圾回收
  • 垃圾回收效率很低
  • 有OOM异常

————-    最后结束修改于2018年12月 31日 22:13    待续…    ————-

1.6  运行时常量池

(需要与class文件常量池字符串常量池区分开来,后面会做详说)

作用:在类加载后,存放类被编译期的各种字面量和符号引用。

特点:

  • 运行时常量池为方法区的一部分
  • JVM没有做任何细节的规范,不同提供商实现的虚拟机可以有不同的设计。
  • 它相对于class文件常量池,具备动态性,运行期间,也可以将新的常量放入运行时常量池。例如 intern方法。
  • 有OOM异常

补充:相关博客

https://blog.csdn.net/zm13007310400/article/details/77534349

1.7 直接内存

作用:

在某些场景中增加性能

特点:

  • 不属于JVM运行时数据区的一部分
  • 堆中的DirectByteBuffer对象被当做这块内存的引用,通过引用对这块内存进行操作。
  • 不被Java堆大小限制,属于计算机本机的一块内存。
  • 有OOM异常

2.1 对象的创建

遇到一条 new 指令

                            | 检查指令的参数能否在 常量池定位到类的符号引用

                                                                                                            |符号引用是否已被加载,解析,初始化过

                                                                                                            |没有 需执行相应的类加载过程

                            |类检查过后,为新生对象分配内存,相当于在堆中分配一块确定大小的内存。

 

2.1.2 分配内存方式

  • 指针碰撞(Bump the Pointer)

假设堆中的内存绝对规整,用过与未用过的分别放一边,中间放一个指针当做分界点的指示器。

分配内存仅仅是把指针向空闲空间挪动与新对象内存大小相等的一端距离。

  • 空闲列表(Free List)

假设堆中内存相互交错,虚拟机需要维护一个列表,确定哪些内存块是可用的。

分配内存时找到一块足够大,对象能放下的内存块,

补充:

当使用SerialParNew等有Compact过程的垃圾收集器时,系统内存分配方式为指针碰撞

当使用CMS(基于Mark-Sweep标记清楚算法收集垃圾)这种收集器时,系统采用空闲列表

CG机制将在第三章的笔记详述。

2.1.3 内存空间初始化

保证实例字段在不赋初值的情况下就能直接使用。程序能访问到这些字段的数据类型对应的0值。

2.2 对象的内存布局

HotSpot虚拟机中,对象在内存中的布局可以分为3部分

对象头 (header)——- 实例数据(Instance Data)——- 对齐填充(Padding)

 2.2.1 对象头

分为两部分

  • 存储对象自身的运行时数据(Mark Word

哈希码

GC分代年龄(用于GC回收参考,将在第三章详解)

锁状态标志

线程持有的锁

偏向线程ID

偏向时间戳

  • 类型指针

虚拟机通过此指针来确定对象属于哪个类的实例

如果对象为一个数组时,对象头中还须有一块用于记录数组长度的数据。因为jvm可以通过普通Java对象元数据信息确定Java对象的大小。但是从数组的元数据区确定不了,因此需要对象头里有数组的长度记录。

2.2.2 实例数据

存储对象真正有效的信息。

程序代码中所定义的各种类型的字段内容,不论是父类继承还是子类定义的,都记录。

存储顺序手虚拟机分配策略参数(FieldAllocationStyle)源码中顺序影响。

2.2.3 对齐填充

  • 并不是必然存在
  • 占位符的作用
  • HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。

————-    最后结束修改于2019年1月 15日 21:52    待续…    ————-

2.3 对象的访问定位

当建立对象后,想使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。

目前主流的访问对象的方式有,句柄直接指针两种。

句柄:

解释:在Java堆中划分出一个句柄池,reference数据存储的是对象的句柄地址,句柄里存放的是对象的实例数据类型数据各自的具体地址信息。

优点:reference中指定句柄的地址不会变。对象变动时,只需变动句柄里的实例数据指针即可。

直接指针:

解释:reference直接指向对象的实例数据,对象的类型指针在对象的实例数据里存放。

有点:访问快,因为减少了一次指针定位的时间开销。

两种指向方式中对象的类型数据存放都在方法区

Hotspot中使用的是直接指针的方式访问对象。

2.4 实战:OOM异常

2.4.1 Java堆溢出

实践方式:不断的在堆中创建对象。限制堆的大小以尽快达到堆溢出。

限制堆大小:堆的最小值 -Xms最大值 -Xmx 设为相等可以防止堆的自动扩展

dump出堆内存转储快照:此操作用于堆溢出后的异常分析。设置参数:-XX:+HeapDumpOnOutOfMemoryError

实践过程:

https://blog.csdn.net/qq_29519041/article/details/86513666

2.4.2 Java栈溢出

HotSpot虚拟机中不区分虚拟机栈与本地方法栈。

实践方式:通过不断递归调用自己,不断入栈入栈的方式达到栈溢出。

限制栈大小:使用虚拟机参数  -Xss

实践过程:

https://blog.csdn.net/qq_29519041/article/details/86513246

————-    最后结束修改于2019年1月 16日 20:33    待续…    ————-

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

Ps:运行时常量池为方法区的一部分。

String.intern()方法:

如果字符串常量池中已包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象。

否则将此String对象包含的字符串添加到常量池中,并返回String对象的引用。

实践方式:通过创建一个数组,并且不断向数组中添加字符串,并调用intern方法,使得常量池中字符串越来越多。以致溢出。

2.4.4 本机直接内存溢出

实践方式:

本机直接内存的分配需要使用Unsafe类,但只有rt.jar(java基础类库)中的类才能使用户Unsafe类中的方法

因此这里需要用到反射获取Unsafe实例进行内存分配。

/**
 * DirectMemoryOOM.java
 * @author anyunpei
 *2019年1月23日下午5:44:34
 *VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {
	private static final int _1MB=1024*1024;
	
	public static void main(String[] args) throws Exception {
		Field unsafeField =Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe)unsafeField.get(null);
		while (true) {
			unsafe.allocateMemory(_1MB);
		}
	}
}

补充: http://www.cnblogs.com/duanxz/p/6097779.html

2.5 总结

通过第二章的学习及在网上阅读其他优秀的博客熟悉了Java的内存区域,各区域的作用特点,及内存溢出异常的处理和实践

 

 

 

      

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/qq_29519041/article/details/85340911
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注