JVM-Java虚拟机自动内存管理机制

    技术2025-12-03  18

        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异常。

     

    方法区

          用于已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,和java堆一样,是线程共享的内存区域。在HotSpot®虚拟机中,HotSpot®虚拟机将分代收集扩展到了方法去,此区域被人们称为“永久代”。此区域和java堆一样不需要连续的物理内存,可选择固定的大小或扩展外,还可以不实现垃圾收集。垃圾收集在方法区比较少见,此区域的主要收集目标是常量池回收,和类型卸载。

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

     

    Processed: 0.026, SQL: 9