JAVA与C/C++的区别之一,JAVA的内存交给JVM(Java Virtual Machine)来管理。也就是说,JAVA中我们只需要创建一个对象(new),此时该对象已在内存中申请了一块空间,而这个空间何时被回收可分配,是由JVM来管理的,程序员不需要关心内存回收。
那么JAVA中把内存管理完全交给了虚拟机管理,我们还有必要学习JVM吗?答案是要的。学习JVM有利于我们编程时内存优化和上线后出现内存溢出/内存泄漏问题排查。
JVM内存模型
JAVA运行时数据区分为五大区域:堆、方法区、本地方法栈、虚拟机栈、程序计数器。我们通常所说的java内存中,栈指虚拟机栈。
程序计数器
分支、循环、跳转、异常处理、线程恢复等功能都要依赖程序计数器。
JAVA虚拟机栈
虚拟机栈描述java中方法的执行过程,方法的执行到结束对应着一个栈帧的入栈到出栈。 局部变量表中存储了基本数据类型、引用类型(句柄)和returnAddress类型,在编译期间完成分配(long、double占用两个局部变量空间(slot),其他基本类型占一个slot),所以可以确定一个方法需要多大的slot;操作数栈中执行class文件在被解读时要如何在栈中进行P,V操作,oracle官方中有操作栈中执行具体指令的说明文档;动态链接将程序拆分成相对独立的模块,在程序执行的时候将各个模块链接起来形成一个完整的程序。
JVM规范中规定了此区域两种异常情况:
1.StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度抛出;
2.OutOfMemoryError:若虚拟机栈可扩展,扩展时无法申请到足够的内存抛出。
本地方法栈
本地方法栈也是用来描述方法的执行过程,区别是此区域仅用来描述native方法的执行过程。native方法在java代码中都只有声明,具体实现是与平台有关的,jdk中的native方法多是用来加载文件和动态链接库(IO、底层硬件设备等),因为JAVA语言无法访问操作系统底层信息。native修饰的方法可以被其他语言重写实现。
JVM规范中规定了此区域两种异常情况:
1.StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度抛出;
2.OutOfMemoryError:若虚拟机栈可扩展,扩展时无法申请到足够的内存抛出。
JAVA堆
Java堆的唯一目的就是用来存储对象实例和数组,在虚拟机启动时创建,被所有线程共享,是垃圾回收器的重点照顾对象。Java堆可以是物理上不连续,逻辑上连续的空间。
对于使用分代收集算法的内存回收策略,此区域又可从逻辑上划分为新生代(Eden、From Survivor、To Survivor)、老年代。
从内存分配的角度,线程共享的Java堆中可能被划分出多个线程私有的分配缓冲区(TLAB)。
JVM规范中规定了此区域异常情况:当堆中没有内存可被分配将抛出OutOfMemoryError异常。
方法区
垃圾收集在方法区比较少见,此区域的主要收集目标是常量池回收,和类型卸载。
JVM规范中规定了此区域异常情况:当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
直接内存
直接内存(Direct Memory) 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,而且可能导致OOM。
New IO引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接进行内存分配,然后通过一个存储在java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以避免在java堆和native堆中来回复制数据,可以显著提升性能。本机直接内存会受到本机总内存的限制。所以在配置虚拟机内存的时候,要给native内存留足空间,因为本机总内存是一定的。
Object obj = new Object()
当我们在执行👆这样的代码时,内存中会发生什么?
当上面的代码发生在方法体中,“Object obj”会反映在虚拟机栈的本地变量表中,作为一种引用类型。“new Object()”会直接在堆内存中申请存储Object类型所有实例数据。
内存区域异常发生demo
package com.mlgg.jdk;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import sun.misc.Unsafe;
/**
* <Description>
*
* @Program: jdkDemo
* @Author: zhang.yifei4 <br/>
* @TaskId: <br/>
* @Date: 2020/07/06 09:53
* @see: com.mlgg.jdk
*/
public class OomDemo {
@Test // -Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
public void heapOomDemo() {
ArrayList<Object> objects = new ArrayList<>();
while (true) {
objects.add(new Object());
}
}
//----------------------------------------------------------------------------------
/**
* 栈帧长度
*/
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
/**
* 栈帧超过虚拟机栈容量导致StackOverflowError
* -Xss 栈容量
* java.lang.StackOverflowError
*/
@Test // -Xss128k
public void vmStackOverflowDemo() {
try {
stackLeak();
}
catch (Throwable e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
//----------------------------------------------------------------------------------
private void dontStop() {
while (true) {
}
}
public void stackLeadByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
/**
* !此代码会导致windows系统计算机假死,请勿轻易尝试
* 每个线程都需要分配栈内存,通过不断创建线程,当栈内存不够分配时,抛出oom
*
* -Xss 栈容量,多线程时越大越容易导致OOM
*/
@Test // -Xss2m
public void vmStackOutOfMemoryDemo() {
stackLeadByThread();
}
//----------------------------------------------------------------------------------
/**
* 运行时常量池溢出
* 我们无法直接限制运行时常量池的大小,但是可以限制方法区的大小从而间接限制运行时常量池大小
* jdk1.7以后将字符串常量池移植到了堆内存中,字符串常量池溢出参数-Xms1m -Xmx1m
*
* -XX:PermSize 最小方法区内存
* -XX:MaxPermSize 最大方法区内存
*/
@Test // -XX:PermSize=10m -XX:MaxPermSize=10m
public void constantPoolOOMDemo() {
// 使用List保持常量池的引用,避免Full GC回收常量池
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
//----------------------------------------------------------------------------------
/**
* 借助cglib创建多个类模拟方法区溢出
*
* PermSize 方法区最小内存
* MaxPermSize 方法区最大内存
*
* 结果:并没有如书中所说:方法区抛出OOM
*/
@Test // -XX:PermSize=10m -XX:MaxPermSize=10m
public void methodAreaOutOfMemoryDemo() {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.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, new Object[1]);
}
});
enhancer.create();
}
}
static class OOMObject {
public OOMObject() {
}
public String name;
}
//----------------------------------------------------------------------------------
private static final int _1MB = 1024 * 1024;
/**
* 直接内存溢出
* -XX:MaxDirectMemorySize 直接内存容量
* -Xmx 最大堆内存容量
*
* @throws IllegalAccessException
* @throws java.lang.OutOfMemoryError
*/
@Test // -Xmx20m -XX:MaxDirectMemorySize=10m
public void directMemoryOOMDemo() throws IllegalAccessException {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}