1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常

【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常

时间:2020-03-04 13:43:52

相关推荐

【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常

最近想好好复习一下java虚拟机,我想通过深读【理解Java虚拟机 jvm 高级特性与最佳实践】(作者 周志明)并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强学习深度的同时方便进行知识的回顾之用。如涉及版权还望周大神看到后告知一下小弟,我会第一时间将文章下线,在此强烈推荐大家买纸质图书【理解Java虚拟机 jvm 高级特性与最佳实践】(作者 周志明)进行阅读,学习java虚拟机必备。努力学习只为遇到更好的你!

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

后面的每章介绍我都会先上引言:

Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面 的人却想出来

该章主要介绍了3块内容

运行时数据区域HotSpot 虚拟机对象探秘实战:OutOfMemoryError 异常

1 运行时数据区域

运行是数据区域有5块 分别为程序计数器Java 虚拟机栈本地方法栈Java 堆方法区各个内存示例如下图:

1.1 程序计数器(Program Counter Register)

程序计数器 是记录程序下一条运行指令,运行指令包括循环,跳转等。 它是线程私有的 每个线程都有一个独立的程序计数器。由于其内存空间小 是唯一一个没有OutOfMemoryError的区域。

在Java 虚拟机中多线程是通过线程轮流切换给处理器执行的 因此我们可以通过程序计数器来保证线程互不影响。如果运行的是java 方法程序计数器会记录下一条运行要执行的指令 如果是Native方法则 计数器的值则为空。

1.2 Java 虚拟机栈(Java Virtual Machine Stacks)

Java 虚拟机栈 是用来记录java 方法执行过程的内存区域,它是线程私有的,生命周期和线程相同。

当我们方法在创建时会创建一个栈帧用于存储局部变量,动态链接,方法出口等信息。方法的执行就是栈帧在Java 虚拟机栈中的入栈和出栈。

Java 虚拟机栈中线程请求栈的深度大于虚拟机允许的深度将抛出StackOverflowError如果扩展无法申请到足够的内存抛出OutOfMemoryError异常。

1.3 本地方法栈(Native Method Stack)

本地方法栈和Java 虚拟机栈作用类似 区别是 Java 虚拟机栈执行java方法 本地方法栈执行Native方法。本地方法栈也会抛出StackOverflowErrorOutOfMemoryError异常。

我们的Sun HotSopt 直接将本地方法栈和Java 虚拟机栈 合二为一。

1.4 Java 堆(Java Heap)

Java 堆的作用是 存放对象实例。它是线程共享也是垃圾收集器管理主要区域。也被成为GC 堆。该内存在虚拟机启动时进行创建。如果堆中没有内存存放我们创建的实例,并且无法进行扩展时 会抛出OutOfMemoryError异常

1.5 方法区(Method Area)

方法区作用是用来存储已加载类的信息,常量, 静态变量, 即时编译器编译后的代码等,它是线程共享的内存区域。为了与Java堆区分开也被称之为 Non-Heap 非堆。

垃圾收集器主要对方法区中的类型卸载 和常量池的内存回收 。方法区无法满足内存分配时将抛出OutOfMemoryError异常

垃圾收集子该区域的操作比较少 很多程序员也称之为永久代,在 JDK1.7 已经将字符串常量从永久代中移除。

1.5.1 运行时常量池(Runtime Constant Pool)

运行时常量池 是方法区的一部分 该区域用于存放编译器生成的各种字面量和符号引用。在类加载后存放到运行时常量池中。

当常量池无法在申请到内存时抛出OutOfMemoryError异常

1.6 直接内存(Direct Memory)

JDK1.4 引入 NIO 通过通道与缓存的I/O方式 它可以使用Native 函数库直接分配的堆外内存 而这个存储就是我们直接内存。

它不是java 虚拟机中规定的内存,但是容易被我们忽视。直接内存受我们的本机总内存的影响 如果java虚拟器内存过大 导致我们的直接内存无法扩展是会抛出OutOfMemoryError异常

2 HotSpot 虚拟机对象探秘

虚拟机对象创建主要从三方面进行介绍对象的创建对象的内存布局对象的访问定位

2.1 对象的创建

书中进行了大量的文字描述,我们就不再进行粘贴复制。通过一个流程图让大家大致了解一下对象在虚拟中创建的过程。

2.2 对象的内存布局

在HotSpot 虚拟机中 对象在内存中存储可以分为3块对象头实例数据对齐填充

2.2.1 对象头

对象头有2部分数据

1. 存储对象自身运行是数据

这部分包括 哈希码 GC分代年龄 锁状态标志 线程持有的锁 等

2. 类型指针

对象指向元数据的指针。通过这个来指定对象是那个类型的实例。

2.2.2 实例数据

实例数据 是对象真正存储的有效信息。就是代码中定义各种类型字段的内容。

2.2.3 对齐填充

对齐填充并不是必然存在,由于对象起始地址必须是8字节的整倍数,当这个地址大于8整倍数时 就无法对齐,需要通过对其填充来补全。

2.3 对象的访问定位

java 程序通过栈上的引用数据来操作堆上的对象 怎么去访问对象具体的位置分为2中方式:

句柄访问直接指针操作方式如下面2个图进行展示:

句柄访问 我们的引用都放在句柄池中 好处是当我们的对象被移动 只改句柄实例中的指针。 而引用本身不用修改。

直接指针 好处访问数度快 不用维护指针定位开销。

3 实战:OutOfMemoryError 异常

这一块主要通过正在java 代码演示如何产生 内存异常的操作,书中使用Eclipse 进行演示和介绍。强烈推荐各位将示例代码在Eclipse真实操作一遍 在结合上面概念介绍。你肯定会对Java 内存管理有深层次的理解。

3.1 Java 堆溢出

演示代码:

package cn.zhuoqianmingyue.heap;import java.util.ArrayList;import java.util.List;public class HeapOOM {static class OOMObject{}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while(true) {list.add(new OOMObject());//强引用GC不能释放空间}}}

通过-Xmx 设置最大堆内存

安装 Elipse Memory Analyzer 插件,具体操作按下图方式进行

安装完成后重启Eclipse

通过 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在内存溢出时Dum当前内存存储的快照用于我们内存溢出分析

执行完成后

刷新一下我们的项目在Eclispe中即可看到我们的Dump 文件如下图:

通过 Memory Analyzer可打开该文件,具体操作请根据图例一步步执行。

在上图中我们可以根据GCRoot 引用链分析 我们可以确定当前对象是否必须存活,通过调节-Xms

与 -Xmx 与机器物理内存比对看是否可以调大。或者从代码的角度检查某些对象的声明周期过长的情况 尝试减少程序运行期内存消耗。

就拿我们上面的代码如果我们代码必须需要这样处理 那么我们可以将list 根据数量进行清理一下就不会有内存溢出的情况具体代码如下:

package cn.zhuoqianmingyue.heap;import java.util.ArrayList;import java.util.List;public class HeapOOM {static class OOMObject{}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while(true) {list.add(new OOMObject());//强引用GC不能释放空间if(list.size() == 100) {list.clear();}}}}

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

在HotSopt虚拟机中由于不区分 虚拟机栈和本地方法栈所以我们设置 -Xoss 参数(本地方法栈)实际上是无效的。

3.2.1 StackOverflowError 异常

如果线程请求的栈深度大于虚拟机所允许的最大的深度 将抛出 StackOverflowError 异常

测试代码

package cn.zhuoqianmingyue.stack;public class JavaMVStackSOF {private int stackLength = 1;public void stackLeak() {stackLength++;stackLeak();}public static void main(String[] args) {JavaMVStackSOF oom = new JavaMVStackSOF();try {oom.stackLeak();} catch (Throwable e) {System.out.println("stack length :"+oom.stackLength);e.printStackTrace();}}}

通过 -Xss设置虚拟机栈的大小。

我们将虚拟机栈的大小提升为2M 结果如下:

栈的深度明显提升了很多。

3.2.2 OutOfMemoryError 异常

如果虚拟机在扩展栈时无法申请到足够的内存空间 则抛出 OutOfMemoryError 异常。

单线程下无论是栈帧太大还是栈内存太小都会抛出StackOverflowError 异常 我们可以通过多线程的方式来进行 OutOfMemoryError 异常展示

如果你看到下面的代码切记不要在自己Windows电脑上运行 会造成系统的假死。本人亲身体验

重要的事情说三遍:不要在自己Windows电脑上运行不要在自己Windows电脑上运行不要在自己Windows电脑上运行,我没有Mac 所以不知道 Mac 运行不知到是一个什么效果。

package cn.zhuoqianmingyue.stack;public class JavaVMStackOOM {private void dontStop() {while(true) {}}public void stackLeakByThread() {while(true) {Thread threa = new Thread(new Runnable() {@Overridepublic void run() {dontStop();}});threa.start();}}public static void main(String[] args) {JavaVMStackOOM oom = new JavaVMStackOOM();oom.stackLeakByThread();}}

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

String.intern() 是一个Native方法 它的作用是 如果字符串常量池中已经包含一个等于此String对象的字符串 则返回代表池中这个字符串String对象 否则将该对象加入到常量池中并返回该对象的引用。

需要注意的是下面代码演示只在JDK1.6及以前的版本中生效 在JDK1.7 和更高的版本中不会生效

在JDK1.7 中已经将String 常量池溢出持久代了。

package cn.zhuoqianmingyue.runtimeconstant;import java.util.ArrayList;import java.util.List;public class RuntimeConstant {public static void main(String[] args) {List<String> list = new ArrayList<String>();int i = 0;while(true) {list.add(String.valueOf(i++).intern());}}}

下面是原书中提供的另一个有意思的代码:

这里都是false的原因是 Jdk1.6中 intern() 方法会把首次遇到字符串示例复制到永久代中 返回的是永久带中的示例 而StringBuilder 创建在java堆上所以是false

Jdk1.7 中第一次次返回true 是因为intern() 实现不会在复制实例 返回的引用和StringBuilder 的实例是同一个。 第二次为false 是因为 StringBuilder.toString() 之前已经出现过 字符串常量池已经有它的引用。 不符合首次出现的原则 因此返回false;

虽然我们不能用String.intern() 方法在JDK 1.7+ 上进行演示 但是我们可以通过CGLib 直接操作字节码运行是产生大量的动态类来演示永久带内存溢出 具体代码如下:

package cn.zhuoqianmingyue.runtimeconstant;import java.lang.reflect.Method;import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;/*** -XX:PermSize=10M -XX:MaxPermSize=10M* @author Administrator**/public class JavaMethodAreaOOM {public static void main(String[] args) {while(true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy arg3) throws Throwable {return arg3.invoke(obj, args);}});enhancer.create();}}static class OOMObject{}}

设置永久代的大小通过下面的参数:

-XX:PermSize=10M -XX:MaxPermSize=10M

执行程序的时候出现下图中异常:

引入asm 的jar包即可

下图是测试结果:

3.4 本机直接内存溢出

本机直接内存 DirectMemory 通过设置 -XX:MaxDirectMemorySize 指定 如不制定和Java 堆最大值(-Xmx)一样。

下面的代码是越过啦 DirectByteBuffer类 直接通过反射获取Unsafe实例进行内存分配。

package cn.zhuoqianmingyue.directmemory;import java.lang.reflect.Field;import sun.misc.Unsafe;public class DirectMemoryOOM {private static final int _1MB = 1024*1024;public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {Field unsafeField= Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe = (Unsafe)unsafeField.get(null);while(true) {unsafe.allocateMemory(_1MB);}}}

需要注意的是DirectMemory 导致的内存溢出 没有Heap Dump文件不会看见明显的异常 如果OOM

之后Dump文件很小就要考虑是不是使用了NIO .

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。