《深入理解java虚拟机》读书笔记2(java内存区域与OOM)

时间:2022-04-11 16:49:03

1.java运行时内存划分

《深入理解java虚拟机》读书笔记2(java内存区域与OOM)

》程序计数器

学过汇编的童鞋都知道程序执行时会记录当前执行的位置,以便确认接下来执行什么。这里的程序计数器就是用来存储当前线程所执行字节码的行号指示器,也就是地址,字节码指示器通过改变程序计数器的值来指定下一条执行的指令,诸如循环,跳转,异常处理,线程恢复等都是这样。

而这样做必须保证顺序执行,否则就乱套了。我们知道顺序执行的最小单位是线程,所以对于每条线程必须拥有独立的程序计数器,如图所示这类内存称为线程隔离(线程私有)的数据区。

java开发中时常会涉及到调用native方法,比如安卓手机的内核是linux,开发安卓应用程序比如视频编解码,需要用native的c语言方法调用。调用java方法时,程序计数器记录的是虚拟机器字节码指令的地址,这时候计数器值为空。

程序计数器的内存区域是java虚拟机规范中唯一一个没有规定任何出现OOM异常的区域。

》java虚拟机栈

从上图看出这又是一个线程私有的内存区域。它是干什么用的呢?虚拟机栈描述的是java方法执行时的内存模型。每个方法执行开始,都会创建一个栈帧,用于保存局部变量表,操作数栈(虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。),动态链接(动态连接是一个将符号引用解析为直接引用的过程),方法出口等信息。方法调用就是入栈出栈的过程。

局部变量表存放了编译器的各种基本数据类型,对象引用(指针,句柄或是相关位置)和returnAddress类型(指向字节码指令的地址)。局部变量表所需空间在编译器完成分配,当进入方法时,在栈帧中分配多大空间是完全确定的,long和double会占用2个局部变量空间(Slot),其他一个

当线程请求栈深度大于虚拟机允许的深度,将抛出我们熟知的*Error异常,如果虚拟机栈可以动态扩展,当扩展到内存不够的情况则会抛出OOM异常

》本地方法栈

这个是虚拟机为使用到的Native方法开辟的空间,与虚拟机栈类似。不同虚拟机都是自定义这一块,甚至有的和虚拟机栈合二为一。会出现*Error和OOM异常。

》java堆

java堆在虚拟机启动时创建,唯一目的是存放对象实例,“几乎所有”都在这里分配,可以看到这块区域是线程共享的。java堆是垃圾回收的主要区域,也称作GC堆。它可以处于物理不连续逻辑连续的内存空间,因此可扩展,当然也会出现OOM异常。

》方法区

方法区也是线程共享的,用于存储已被加载的类信息,常量,静态变量,即时编译后的代码等数据。java虚拟机规范对这块区域的限制非常宽松,一样可扩展,可以不实现垃圾回收,因为回收概率小。这个区域回收主要是针对常量池和对类型的卸载,不过回收效果差,但是却是必要的。

方法区的运行时常量池。Class文件中除了有类的版本,字段,方法,接口描述外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,在类被加载到方法区后存放到运行时常亮池中。一般来说,除了符号引用,翻译过来的直接引用也保存在这里,同时并非只有编译时,运行时也可以把常量保存进来。同样的存在OOM异常

》直接内存

并不是这个体系的一部分,在jdk1.4中引入了NIO,可以使用native函数库直接分配堆外内存,避免在java堆和native堆中来回复制,有可能出现OOM异常。

2.对象访问

拿一个以前常用的例子说,Object obj = new Object(); 这句中的变量Object obj将会被存储于虚拟机栈中的局部变量表中,作为一个reference类型出现,而new Object()显然是一个对象实例,保存在java堆中。

java虚拟机规范规定reference类型是指向对象的引用,没有定义如何指向。主流的访问方式有两种:使用句柄和直接指针。1)句柄访问,java堆会开辟一块内存作为句柄池,reference此时存储对象的句柄地址,句柄则包含对象类型数据Object类和对象实例数据new Object()的具体地址。2)直接指针方式,reference存储对象实例数据new Object()的具体地址,该实例数据中又包含了对象类型数据地址。各有优势:采用句柄访问,当对象发生移动时,只要改变句柄中的对象实例指针,不需要改变reference;而直接指针的优势则是一次定位.

3.OOM实验

》java堆OOM

java堆存放对象实例,所以无限创建实例就会引起OOM

编写java代码
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
右键进行debug配置
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
配置虚拟机参数:内存20M,当OOM时dump
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
运行结果
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)

结果分析,利用eclipse的memory analyzer插件
参考https://www.cnblogs.com/dennyzhangdd/p/5647469.html
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
透过树形结构,结果指出内存被Object对象占用

》java栈溢出实验

java栈保存栈帧,只要无限调用方法就行。
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
运行结果如下:
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
实验结果表明:在单线程下,无论是栈帧太大,还是栈容量太小,抛出的都是*Error异常。在多线情况下,则可以出现OOM异常。如果在多线程发生OOM,在不能减少线程和更换64位虚拟机的情况下可以通过减少最大堆和减少栈容量来换取更多线程。

这里实验时发现一个问题,jdk1.8这里栈容量最低要160k,所以换成-Xss160k

》运行时常量池内存溢出实验
前面提到常量池在运行时也能动态添加常量,这里可以无限添加常量就行了。利用String.intern()这个native方法,该方法用于向常量池中添加没有的常量。
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)

运行时发现,java 8不支持VM使用这两个方法区参数了。Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0,查了一下发现机制已经变了,尴尬。
替换成java 7执行,发现没有报错,尴尬+1。于是减小方法区大小,10k,提示初始化时不能太小,尴尬+2。最后改为2M,运行得到结果:
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)

》方法区内存溢出实验

前面说了,方法区用于存放已被加载的类信息,常量,静态变量,即时编译后的代码等数据。这里让它无限加载类就行。可以通过反射,当然也有简单的办法。作者使用了一个CGLIB代理,作用跟java动态代理类似。
参考:http://blog.csdn.net/danchu/article/details/70238002

由于我是在javaweb项目里测试的,spring框架已经集成了CGLIB所以可以直接使用里面的方法。import org.springframework.cglib.proxy.Enhancer;
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
运行结果:
《深入理解java虚拟机》读书笔记2(java内存区域与OOM)
方法区的内存回收是比较困难的,所以在动态生成大量class的时候,特别要注意类的回收状态。(ps:这里的PermGen space只的就是方法区,也就是HotSpot虚拟机中的永久代,是对JVM规范的一种实现,别的虚拟机是没有的。在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。)

》直接内存溢出实验

这个没有找到Unsafe类无法测试,大写的尴尬。据说这个类可以操作内存空间,作者也用它来动态申请内存空间,每次1M,直到超出虚拟机限定的10M。