java内存模型是jvm的基础,而jvm是java的基础,所以弄明白java的内存模型是至关重要的!本篇设计到了Java内存模型(堆、栈等)、常见的内存溢出(OOM、StackOutflowError等)。
目录
一、Java内存模型相关问题
jvm对于操作系统来讲,本质上还是一个进程,下图大致的将内存分成了三块儿:堆(存数据)、方法区(线程执行)、本地内存(不是jvm规范的一部分,由操作系统来直接管理)。
本地内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。
在 JDK 1. 4 中 新 加入 了 NIO( New IO) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java堆的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在 Java堆和 Native堆中来回复制数据。NIO 是一种同步非阻塞的 IO 模型。
JDK1.6、1.7、1.8的jvm(逻辑上的)内存结构
JDK1.6的JVM内存结构(逻辑上):
关于内存溢出:
程序计数器:较小的内存空间(不会出现OOM即内存溢出),当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。
Java栈:线程私有,生命周期和线程,每个方法在执行的同时都会创建一个栈帧由于存储局部变量表,操作数栈,动态链表,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用。
本地方法栈:与java栈不同的是,本地方法栈保存的是native(本地)方法的信息,当一个JVM创建的线程调用native方法后,JVM不在为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法。
堆:涉及到内存的分配(new关键字、反射等)与回收(回收算法、收集器等)。
方法区:也叫永久区,用于存储已被虚拟机加载的类信息、常亮(“abc”,“1234”等),静态变量(static)等数据。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量(“abc”,“1234”等)和符号引用。
JDK1.7、JDK1.8的JVM内存结构(逻辑上有哪些):
栈和堆的区别是什么?
▶功能:
▷栈:以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(short、byte、int、long、float、double、boolean、char)以及对象的引用变量,其内存分配在栈上,变量除了作用域就会自动释放;
▷堆:堆内存用来存储Java中的对象。无论是成员变量、局部变量,还是类变量,他们指向的对象都存储在堆内存中。
▶线程独享还是共享:
▷栈:栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
▷堆:堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
▶空间大小:
栈的内存要远远小于堆内存,栈的深度是有限制的,如果递归没有及时跳出,很可能发生StackOutflowError问题。可以通过-Xss选项设置栈内存大小;-Xms设置堆开始时的大小;-Xmx选项设置堆的最大值。
jvm常用内存参数设置:
注:java8去掉了-XX:PermSize和-XX:MaxPermSize,新增了-XX:MetaspaceSize和-XX:MaxMetaSpaceSize。
JDK1.8为什么要去除方法区(永久区)?
★ 永久区来存储类信息、常量、静态变量等数据不是个好主意,很容易遇到内存溢出的问题,JDK 8的实现中将类的元数据放入native memory,将字符串池和类的静态变量放入java堆中,可以使用MaxMetaspaceSize对元数据区大小进行调整;
★ 对永久区进行调优是很困难的,同时将元空间与堆得垃圾回收进行了隔离,避免永久区引发的Full GC和OOM等问题。
二、常见的内存溢出异常问题
java内存溢出异常主要有两个:
▶ OutOfMemeoryError:当堆、栈(多线程情况)、方法区、元数据区、直接内存中数据达到最大容量时产生;
▶ StackOverFlowError:如果线程请求的栈(单线程)深度大于虚拟机栈允许的最大深度,将抛出StackOverFlowError,其本质还是数据达到最大容量。
出现堆溢出的原因及解决办法
→ 堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收实例对象,就会在对象数量达到堆最大容量时产生OutOfMemoryError异常( java.lang.OutOfMemoryError: Java heap space) 。
▶ 使用-XX:+HeapDumpOnOutOfMemoryError可以让java虚拟机在出现内存溢出时产生当前堆内存快照以便进行异常分析,主要分析那些对象占用了内存;也可使用jmap(工具)将内存快照导出;一般检查哪些对象占用空间比较大,由此判断代码问题,没有问题的考虑调整堆参数。
出现栈溢出的原因及解决办法
→ 如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverFlowError;
→ 如果虚拟机在扩展栈时无法申请到足够的内存空间(栈中一般不会发生OOM,多线程/高并发时才会出现),抛出OutOfMemeoryError。
▶ StackOverFlowError 一般是函数调用层级过多导致,比如死递归、死循环;
▶ OutOfMemeoryError一般是在多线程环境才会产生,一般用“减少内存的方法”,即减少最大堆和减少栈容量来换取更多的线程支持;
出现方法区/元数据区内存溢出的原因及解决办法
→ jdk 1.6以前,运行时常量池还是方法区一部分,当常量池满了以后(主要是字符串变量),会抛出OOM异常;
→ 方法区和元数据区还会用于存放class的相关信息,如:类名、访问修饰符、常量池、方法、静态变量等;当工程中类比较多,而方法区或者元数据区太小,在启动的时候,也容易抛出OOM异常;
▶ jdk 1.7之前,通过-XX:PermSize,-XX:MaxPerSize,调整方法区的大小;
▶ jdk 1.8以后,通过-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,调整元数据区的大小;
出现本机直接内存溢出的原因解决办法
→ jdk本身很少操作直接内存,而直接内存(DirectMemory)导致溢出最大的特征是,Heap Dump文件不会看到明显异常,而程序中直接或者间接的用到了NIO;
▶ 直接内存不受java堆大小限制,但受本机总内存的限制,可通过MaxDirectMemorySize来设置(默认与堆内存最大值一样)。