JVM内存区域
了解java内存区域的划分,和每个区域存储的数据,可以帮助我们分析问题。
JVM内存区域分成堆 ,方法区,虚拟机栈,本地方法栈, 程序计数器
上图说明了 运行时数据区的划分,关注
方法区,堆是线程共享的
虚拟机栈,程序计数器,本地方法栈是线程私有的
方法区
存放的数据是JVM加载的类信息,常量,静态变量和编译器编译后的代码等,这里要注意的是JDK1.8之后已经将这个方法区删除了,使用元空间,metaspace代替了,理由有如下:
1.方法区存放的是常量,容易造成内存溢出,outofmemory:permGen space
2.编译后的代码,类和方法难确定大小,太小,容易造成永久代溢出,太大,容易造成堆溢出,使用元空间,不受JVM虚拟机内存限制,受本地内存的限制。
3.同时永久代的GC复杂,回收效率偏低。
元空间常用的配置参数
1.MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数
2.MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
3.MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
4.MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。
5.MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
6.MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。
堆
new出来的对象就放在堆,这部分区域目的就是为了存放对象实例。也是JVM管理的内存最大的最大的一块区域。
堆又分成 新生代(YoungGeneration)和 老年代(OldGeneration),新生代还可以分成Eden, from Survivor,to Survivor
程序计数器
非常小的一个内存空间,是当前线程执行字节码的行号指示器,每个线程都有自己的程序计数器,是线程私有的,程序计数器是唯一一个不会发生内存溢出的区域。
虚拟机栈
虚拟机栈,也是线程私有的一个空间
虚拟机会为每个线程提供一个虚拟机栈,每个虚拟机栈都有若干个栈帧,每个栈帧存储了局部变量表,操作数栈,动态链接,返回地址,当线程执行一个方法时,这个方法对应的栈帧,就处于虚拟机栈,栈帧的顶部,每一个java方法,从被调用,到结束,对应了一个栈帧入栈到出栈的过程。
本地方法栈
虚拟机栈上执行的是 Java方法,本地方法栈上执行的是本地方法(Native Methmod),HotSpot虚拟机将,虚拟机栈和本地方法栈合二为一。
JVM 内存溢出
1.堆溢出
public class HeapOOM {
static class OOMObject {
}
/**
* VM args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -XX:HeapDumpPath=D://java_pid.hprof -Xms:是初始化堆内存值 -Xmx:是堆内存最大值
* -XX:+HeapDumpOnOutofMemoryError 在堆溢出时保存快照
* -XX:HeapDumpPath=./java_pid.hprof来显示指定路径
*
* MAT 工具 http://download.eclipse.org/mat/1.6/update-site/
* https://blog.csdn.net/u010335298/article/details/52233689/
* https://blog.csdn.net/liu765023051/article/details/75127361
* @param args
*/
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行后会出现异常
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
新生代中,new 出来的对象会被放到Eden,第一次Mirror GC 之后,会被转移到 from survivor, 新生代采用的是复制算法进行MirrorGC ,当from survivor不足时,对象转移到to survivor,每进行一次MirrorGC ,年龄+1,当survivor对象年龄达到一定程度时,新生代对象,转移到老年代。
2.虚拟机栈溢出
当线程请求的栈深度大于虚拟机栈支持锁允许的最大深度,或抛出StackOverFlowError异常,即是虚拟机栈过多,导致了堆栈溢出
public class JavaVMStackSOF { private int stackLength = 1; private void stackLeak(){ stackLength++; stackLeak(); } /** * VM args: -Xss128k * -Xss 栈内存容量 * @param args */ public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); // Throwable ->Error, Exception } catch (Throwable e) { System.out.println("stack length:"+oom.stackLength); // e.printStackTrace(); } } }
以上代码会抛出
stack length:1003
java.lang.StackOverflowError 异常
虚拟机栈溢出还有一种 OutofMemoryError异常,这个异常,可以这样理解,一台机器的物理内存是4个G,其他系统应用占用2G,堆占用1G,永久代占用512M,栈可用的空间是512M,如果每个线程设置成1M,最大可创建512个线程
/** * 本地虚拟机栈溢出 * 设置每个线程的栈大小:-Xss2m * 运行时,不断创建新的线程(且每个线程持续执行),每个线程对一个一个栈,最终没有多余的空间来为新的线程分配,导致OutOfMemoryError */ public class StackOOM { private static int threadNum = 0; public void doSomething() { try { Thread.sleep(100000000); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { final StackOOM stackOOM = new StackOOM(); try { while (true) { threadNum++; Thread thread = new Thread(new Runnable() { @Override public void run() { stackOOM.doSomething(); } }); thread.start(); } } catch (Throwable e) { System.out.println("目前活动线程数量:" + threadNum); throw e; } } }
以上代码会报错
java.lang.OutOfMemoryError: unable to create new native thread
3.方法区溢出
说了堆,栈,还有个方法区(永久代)也会出现溢出,方法区什么时候溢出,主要看,方法区存放的是类信息,常量和静态变量,与编译器编译后的代码等。
溢出一般会报错 java.lang.OutOfMemoryError: PermGen space
JDK 1.8之后去除了PermGen space 使用Metaspace 代替了,如果方法区溢出,会有如下异常:
Caused by: java.lang.OutOfMemoryError: Metaspace
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 * 1.8 设置 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m * 运行时,通过cglib不断创建JavaMethodAreaOOM的子类,方法区中类信息越来越多,最终没有可以为新的类分配的内存导致内存溢出 */ public class JavaMethodAreaOOM { public static void main(final String[] args){ try { while (true){ Enhancer enhancer=new Enhancer(); enhancer.setSuperclass(JavaMethodAreaOOM.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o,objects); } }); enhancer.create(); } }catch (Throwable t){ t.printStackTrace(); } } }
4.本机直接内存溢出
本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但 Java 中用到 NIO 相关操作时(比如 ByteBuffer 的 allocteDirect 方法申请的是本机直接内存),也可能会出现内存溢出的异常。