JAVA与C/C++的区别之一,JAVA的内存交给JVM(Java Virtual Machine)来管理。也就是说,JAVA中我们只需要创建一个对象(new),此时该对象已在内存中申请了一块空间,而这个空间何时被回收可分配,是由JVM来管理的,程序员不需要关心内存回收。

    那么JAVA中把内存管理完全交给了虚拟机管理,我们还有必要学习JVM吗?答案是要的。学习JVM有利于我们编程时内存优化和上线后出现内存溢出/内存泄漏问题排查。

JVM内存模型

    JAVA运行时数据区分为五大区域:堆、方法区、本地方法栈、虚拟机栈、程序计数器。我们通常所说的java内存中,栈指虚拟机栈。

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);
        }
    }
}