JVM垃圾收集器与内存分配策略一(JVM堆、栈、方法区内存溢出案例)


前言

java与C++直接有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙内的人想出来,作为一个java工程师,接下来将带大家一探墙内秘密。
这篇文章是JVM垃圾收集器与内存分配策略模块学习的第一篇文章,主要是作为真正学习前的铺垫,主要会介绍一下JVM运行时数据区以及各个区域在什么情景下回出现内存溢出,下文会通过代码实战模拟JVM的各个区域的内存溢出情景,并加以分析。


JVM运行时数据区

在这里插入图片描述

程序计数器

程序计数器是很小的一块内存区域,用于记录当前线程正在执行的虚拟机字节码指令的地址。在多线程情况下,为了当前线程在获取到CPU资源后能够迅速恢复到上次执行的代码位置,所以每个线程都要有自己单独的程序计数器,且各个线程之间的程序计数器互不影响,独立储存,所以程序计数器属于线程隔离数据区。此区域也是唯一一个在《java虚拟机规范》中提到不会出现OutOfMemoryError异常的地方。

虚拟机栈

虚拟机栈也是线程私有的每个线程都会有一个虚拟机栈,生命周期与线程相同,当前线程没调用一个方法就会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用执行至执行完毕的过程就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。
在《java虚拟机规范》中为这块区域定义了两种异常情况:1.如果线程请求的栈的深度大于虚拟机所允许的最大深度就回抛出Stack
OverflowError异常;2.在支持栈动态扩展的虚拟机中如果在栈自动扩展时无法申请到足够内存就回抛出OutOfMemoryError(如果当前虚拟机不支持栈动态扩展那当栈里剩余空间无法创建新的栈帧时会抛出StackOverflowError)

本地方法栈

本地方法栈和虚拟机栈类似,虚拟机栈是针对java方法的,本地方法栈是针对Native方法的。

堆是虚拟机管理的内存中最大的一块,是线程共享的内存区域,在虚拟机启动时创建,用来储存虚拟机运行过程中创建的对象实例,也是垃圾回收和内存分片的重点区域。如上图提到的新生代、老年代、Eden区等并不是《java虚拟机规范》中对堆的进一步细致划分,只是基于分代设计的垃圾回收器的对堆的逻辑划分,在不采用分代设计的垃圾回收器角度上图中对堆的细致划分就有待商榷了,这块在后面会真对不同的垃圾回收器进行详细分析。如果从内存分配的角度看,所有线程共享的堆中也可以划分出多个线程私有的分配缓冲区(TLAB)以提升对象分配效率,将java堆细致划分的目的只是为了更好的回收内存和更快的分配内存。

方法区

方法区和堆一样也是线程共享的内存区域,主要用来存储被JVM加载的类型信息、常量、静态变量等数据。说道方法区就必须提一下“永久代”这个概念,在JDK8之前HotSpot虚拟机中使用永久代来实现方法区,即将方法区放在堆中,可以省去为方法区单独编写垃圾回收和内存管理的代码,但是这样的设计反而让虚拟机更容易出现内存溢出,引用永久代有-XX:MaxPerSize的上限,就算不配置也会有默认的上限,而随着常量池中常量越来越多和加载的类型信息越来越多,且由于类型的卸载回收条件比较苛刻,会更大几率的导致内存溢出。在JDK8以后改用本地内存里的元空间来实现方法区,这样方法区只要不触及操作系统内存的上限就可以无限动态扩展。在《java虚拟机规范》中规定方法区无法满足内存分配需求时也会抛出OutOfMemoryError异常。

直接内存

严格的说直接内存并不是jvm运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域。但是这部分内存区域也被频繁使用且也会导致OutOfMemoryError异常出现。这块区域主要用来储存Socket,在java程序需要与数据库等其他服务进行通信时就需要创建Socket用于信息的发出和接收,socket是操作系统层面的,jvm在接受到java程序的创建连接指令后调用操作系统的函数直接分配堆外内存,然后通过一个储存在java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。虽然直接内存不受虚拟机的限制,但如同方法区一样会受到操作系统的内存上限的限制,同样当内存不满足分配时也会抛出OutOfMemoryError。

虚拟机栈OutOfMemoryError、StackOverflowError

关于栈上的这两种异常一定要明确在什么情景下会导致那种异常:

  1. 栈不支持动态扩展、线程栈请求栈深度超过了虚拟机栈的最大深度 StackOverflowError
  2. 栈支持动态扩展 (多线程情景和单线程情景导致OutOfMemoryError有不同处理思路)OutOfMemoryError
    PS:栈支持动态扩展虚拟机本人没有,而且这个验证最好在限制单个进程最大内存的服务器上验证如32G的Windows操作系统,不然如果栈支持动态扩展,那栈的大小就受限于操作系统内,我电脑16G要等很久才能出结果…主要还是懒得下载Classic等支持动态扩展的虚拟机。当然关于第二种情景下提供一种调优手段:如果在单线程情景下导致了OutOfMemoryError说明在内层压榨到极致的情况下栈的最大深度也不满足线程要求的深度,此时就要想办法增加栈深度,可以尝试通过减少堆内存或者减小单个栈帧的大小来增加栈的深度上线;如果在多线程的情景下那必然是内存上限了导致无法为新的线程创建栈导致的OOM,此时可以减少堆内存或者减少单个栈深度来换取更多的线程数。
    接下来验证一下第1中情景栈不支持动态扩展、线程栈请求栈深度超过了虚拟机栈的最大深度 导致StackOverflowError。
public class StackSpace {
    
    
    int i=0;
    public void demo(){
    
    
        System.out.println(i);
        i++;
        demo();
    }

    public static void main(String[] args) {
    
    
        StackSpace stackSpace=new StackSpace();
        stackSpace.demo();
    }

}

打印结果:

0
...
9866
9867
9868
9869
9870
9871
Exception in thread "main" java.lang.StackOverflowError
	at java.base/java.nio.Buffer.<init>(Buffer.java:222)
	at java.base/java.nio.CharBuffer.<init>(CharBuffer.java:281)
	at java.base/java.nio.HeapCharBuffer.<init>(HeapCharBuffer.java:75)
	at java.base/java.nio.CharBuffer.wrap(CharBuffer.java:393)
	at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:280)
	at java.base/sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
	at java.base/java.io.OutputStreamWriter.write(OutputStreamWriter.java:211)
	at java.base/java.io.BufferedWriter.flushBuffer(BufferedWriter.java:120)
	at java.base/java.io.PrintStream.write(PrintStream.java:605)
	at java.base/java.io.PrintStream.print(PrintStream.java:676)
	at java.base/java.io.PrintStream.println(PrintStream.java:812)
	at com.oom.StackSpace.demo(StackSpace.java:9)
	at com.oom.StackSpace.demo(StackSpace.java:11)

堆OutOfMemoryError

为了更快的让堆内存溢出,需要我们配置一下JVM的一些参数,让堆的内存更小一些

**
 * -Xmx20m -Xms20m -Xmn10m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
 * HeapSpace outOfMemoryError JVM堆内存溢出Demo
 * JVM参数:
 * -Xmx20m 最大堆内存
 * -Xms20m 最小堆内存
 * -Xmn10m 新生代内存
 * -XX:SurvivorRatio=8 新生代中Eden区与Survivor区比值
 * -XX:+HeapDumpOnOutOfMemoryError 堆内存溢出快照
 * -XX:+PrintGCDetails GC日志打印
 */
public class HeapSpace {
    
    
    static class Demo{
    
    

    }

    public static void main(String[] args) {
    
    

        List<Demo> demos=new ArrayList<>();

        while (true){
    
    
            demos.add(new Demo());

        }
    }

}

打印结果:

[GC (Allocation Failure) [PSYoungGen: 8192K->1008K(9216K)] 8192K->5121K(19456K), 0.0090997 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 9200K->9200K(9216K)] 13313K->19432K(19456K), 0.0120218 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 9200K->0K(9216K)] [ParOldGen: 10232K->9836K(10240K)] 19432K->9836K(19456K), [Metaspace: 3190K->3190K(1056768K)], 0.1015416 secs] [Times: user=0.38 sys=0.01, real=0.10 secs] 
[Full GC (Ergonomics) [PSYoungGen: 7742K->8020K(9216K)] [ParOldGen: 9836K->7725K(10240K)] 17578K->15746K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.1425264 secs] [Times: user=0.77 sys=0.01, real=0.14 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 8020K->8006K(9216K)] [ParOldGen: 7725K->7725K(10240K)] 15746K->15731K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.1086842 secs] [Times: user=0.65 sys=0.00, real=0.11 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7963.hprof ...
Heap dump file created [27802776 bytes in 0.091 secs]
Heap
 PSYoungGen      total 9216K, used 8192K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 100% used [0x00000007bf600000,0x00000007bfe00000,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 46% used [0x00000007bff00000,0x00000007bff76e30,0x00000007c0000000)
 ParOldGen       total 10240K, used 7725K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 75% used [0x00000007bec00000,0x00000007bf38b770,0x00000007bf600000)
 Metaspace       used 3227K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at com.oom.HeapSpace.main(HeapSpace.java:27)

方法区OutOfMemoryError

方法区是在JDK6-JDK7-JDK8升级过程中变化比较大的一块区域,随着永久代的移除元空间的出现,关于方法区出现了很多争议,比如运行时常量池究竟在堆里还是在元空间里?还有字符串intern方法带来的一系列面试题?这两块可以参考大佬文章我选了两个讲的比较好的文章

  • 运行时常量池究竟在堆里还是在元空间里?https://blog.csdn.net/weixin_44556968/article/details/109468386
  • 字符串intern方法带来的一系列面试题?https://blog.csdn.net/qq_36426468/article/details/110150453

直接内存OutOfMemoryError

在这里插入图片描述

/**
 * -XX:MaxDirectMemorySize=10m 指定直接内存大小
 */
public class DirectMemory {
    
    
    private static final int _1mb=1024*1024;


    public static void main(String[] args)throws Exception {
    
    

        Field declaredField = Unsafe.class.getDeclaredFields()[0];
        //设置允许暴力访问
        declaredField.setAccessible(true);
        // 传入启动类加载器(启动类加载器是c++实现的,在java代码中为null)
        Unsafe unsafe = (Unsafe) declaredField.get(null);
        while (true){
    
    
            unsafe.allocateMemory(_1mb);
        }
    }
}

由直接内存导致的内存溢出,有一个很明显的特征是在HeapDump文件不会有什么明显的异常,如果读者发现内存溢出后产生的Dump文件很小而程序中又直接或者间接使用了DirectMemory(典型的间接使用就是NIO)就可以重点考虑一下直接内存方面的原因了。

猜你喜欢

转载自blog.csdn.net/yangxiaofei_java/article/details/115272721