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

版权声明:本文为博主原创文章,转载请表明出处。如果您觉得文章还行就点个赞,同时也可以关注一下我哈。 https://blog.csdn.net/ljk126wy/article/details/88356899

深入理解
最近想好好复习一下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() {
				
				@Override
				public 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() {

				@Override
				public 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 .

猜你喜欢

转载自blog.csdn.net/ljk126wy/article/details/88356899