• JVM之运行时数据区、内存结构、内存模型


    运行时数据区

    JVM运行时数据区、JVM内存结构、JVM内存模型都是指Java虚拟机的内存空间划分,主要分为5个部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区

    注意:JVM内存模型与Java内存模型有严格不同,Java内存模型是与多线程编程相关。

    在这里插入图片描述

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,统称运行时数据区域。

    在类加载过程中的装载阶段:将Calss文件对应字节流所代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

    也就是说:类文件被类装载器装载进来之后,类中的内容,如变量,常量,方法,对象等数据得需要存储在JVM中对应的空间中,该空间就是运行时数据区。

    JDK8同JDK7比,最大差别是:元数据区取代了永久代。元空间Metaspace本质和永久代Perm Space类似,都是对JVM 规范中方法区的实现。不过元空间与永久代之间最大区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

    运行时数据区分为2类:

    线程私有的:

    程序计数器
    
    虚拟机栈
    
    本地方法栈
    
    • 1
    • 2
    • 3
    • 4
    • 5

    线程共享的:

    堆
    
    方法区
    
    直接内存 (非运行时数据区的一部分)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    常量池

    常量池分为静态常量池,运行时常量池,还有字符串常量池。

    静态常量池

    储存的就是字面量以及符号引用

    字符串常量池

    字符串常量池是JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

    JDK7之前,字符串常量池存放在永久代。JDK7字符串常量池和静态变量从永久代移动到Java堆中。

    这是因为永久代的GC回收效率太低,只有在Full GC的时候才会被执行回收。Java程序中通常会有大量被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

    在堆中创建字符串对象A,同时会将字符串对象A的引用保存在字符串常量池中
    
    String a = "A";
    
    再次创建一个字符串A时,会直接返回字符串常量池中字符串对象A的引用
    
    String b= "A";
    
    System.out.println(a == b)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行时常量池

    Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的常量池表

    字面量是源代码中的固定值的表示法,通过字面就能知道其值的含义。字面量包括整数、浮点数和字符串字面量
    
    符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用
    
    常量池表会在类加载后存放到方法区的运行时常量池中
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行时常量池的功能类似编程语言的符号表,也是方法区的一部分,常量池无法再申请到内存时会抛出OutOfMemoryError错误。

    方法区(Method Area)

    方法区属于是JVM运行时数据区域的一块逻辑区域,是各个线程共享的内存区域,在虚拟机启动时创建

    Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来

    当虚拟机要使用一个类时,它需要读取并解析Class文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    永久代与元空间

    永久代以及元空间是HotSpot虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是JDK8之前的方法区实现,JDK8及以后方法区的实现变成了元空间。最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

    永久代有一个JVM本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

    元空间里面存放的是类的元数据,加载多少类的元数据就不由MaxPermSize控制, 而由系统的实际可用空间来控制,这样能加载的类就更多。

    方法区参数设置

    JDK8之前

    -XX:PermSize:设置永久代初始分配空间,默认值是20.75M
    
    -XX:MaxPermSize:设定永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M
    
    当永久代溢出时会得到错误:java.lang.OutOfMemoryError: PermGen space
    
    • 1
    • 2
    • 3
    • 4
    • 5

    JDK8

    -XXMaxMetaspaceSize:设置最大元空间大小,默认值为unlimited,只受系统内存的限制
    
    -XXMetaspaceSize:定义元空间的初始大小,如果未指定,则Metaspace将根据运行时的应用程序需求动态地重新调整大小
    
    当元空间溢出时会得到错误: java.lang.OutOfMemoryError: MetaSpace
    
    • 1
    • 2
    • 3
    • 4
    • 5

    堆(Heap)

    Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享

    堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

    JDK7与JDK8

    JDK7的堆内存模型

    JDK7的堆内存由:Young、Tenured、Perm、Virtual等区构成

    Young年轻区(代)

    Young区被划分为三部分: Eden区、两个大小严格相同的Survivor区(S0+S1),S0和S1一样大,也叫From和To

    S0和S1某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

    Tenured/Old年老区

    Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

    Perm永久区

    Perm代主要保存class、method、filed对象,这部份空间一般不会溢出,除非一次性加载很多的类

    Virtual区

    最大内存和初始内存的差值,就是Virtual区。

    JDK8的堆内存模型

    JDK8的内存模型组成:

    新生代内存(Young Generation): Young区
    
    老生代(Old Generation): Old区
    
    永久代(Permanent Generation)
    
    Young区分为:Survivor区(S0+S1)、EdenS0S1一样大,也叫FromTo
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    JDK7与JDK8的区别:

    最大的Perm区,用Metaspace元数据空间进行替换。Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间。

    移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

    由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen,基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。

    Survivor区

    Survivor区是为了:减少老年代对象的产生,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC(回收新生代)还能在新生代中存活的对象,才会被移动到老年代。

    如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会移动到老年代

    当老年代满了后,触发Major GC(回收老年代),注意:Major GC一般伴随着Minor GC,也可以看做触发了Full GC

    老年代内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,会影响大型程序的执行和响应速度。

    假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长

    假如减少老年代空间,虽然GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。

    S0与S1区的出现是为了解决碎片化

    如果只有一个Survivor区,新建对象在Eden中,一旦Eden满了,触发Minor GC,Eden中存活对象移动到Survivor区。如此循环,当下一次Eden区进行Minor GC时,Eden和Survivor各有一些存活对象,此时把Eden区的存活对象放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化的产生。

    新生代中Eden:S1:S2的比例默认是8:1:1

    新生代中的可用内存:复制算法用来担保的内存为9:1
    
    可用内存中EdenS1区为8:1
    
    即新生代中Eden:S1:S2 = 8:1:1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    堆内存中都是线程共享的区域吗?

    JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
    
    对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
    
    • 1
    • 2
    • 3

    逃逸分析

    随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对。从JDK7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(未逃逸出去),那么对象可以直接在栈上分配内存。

    对象的创建

    新创建的对象都会被分配到Eden区,一些特殊的大对象会直接分配到Old区。当Eden区空间不足GC后进入Survivor区的From区,或者进入Survivor区的To区,来回切换。通常直到GC回收18次后进入Old区。
    在这里插入图片描述

    大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入S0或者S1,并且对象的年龄还会加1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold设置。

    堆内存溢出

    当创建大量对象时,堆空间不足,则会堆内存溢出

    设置堆大小

    -Xmx20M -Xms20M
    
    • 1
        public static void main(String[] args) throws InterruptedException {
            Thread.sleep(5000);
            List<Object> list = new ArrayList<>();
            while (true) {
                list.add(new Object());
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    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.example.demo.DemoApplicationTests.main(DemoApplicationTests.java:44)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    方法区内存溢出

    向方法区中大量添加Class的信息,当方法区内存不足,则方法区内存溢出

    设置Metaspace的大小

    -XX:MetaspaceSize=30M -XX:MaxMetaspaceSize=30M
    
    • 1

    可以借助ASM模拟方法区内存溢出。ASM是一个 Java字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。

    虚拟机栈溢出

    Stack Space用来做方法的递归调用时压入栈帧(Stack Frame),当递归调用深度太深,就可能耗尽Stack Space,爆出StackOverflow的错误。

    JDK5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。

    Stack Space是根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,在3000~5000左右较为合理。

    线程栈设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

    设置每个线程的堆栈大小

    -Xss128k
    
    • 1
        public static long count = 0;
    
        public static void method() {
            System.out.println(count++);
            method();
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread.sleep(5000);
            method();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1067
    1068
    1069
    1070
    1071
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    Exception in thread "main" java.lang.StackOverflowError
    	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
    	at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
    	at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
    	at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
    	at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
    	at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
    	at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
    	at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
    	at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
    	at java.io.PrintStream.write(PrintStream.java:526)
    	at java.io.PrintStream.print(PrintStream.java:611)
    	at java.io.PrintStream.println(PrintStream.java:750)
    	at com.example.demo.DemoApplicationTests.method(DemoApplicationTests.java:35)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    虚拟机栈(Java Virtual Machine Stacks)

    虚拟机栈是在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址等信息。使用先进后出(FILO)的数据结构

    虚拟机栈是基于线程的,是以线程方式运行,同时也是一个线程执行的区域,保存着一个线程中方法的调用状态。

    一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈是线程私有的,独有的,随着线程的创建而创建。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。

    每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧,一个方法调用完成,就会把该栈帧从栈中弹出。

    大小限制:缺省为1M,可用参数 –Xss 调整大小,如-Xss256k

    官方文档(JDK1.8):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

    压栈入栈与出栈过程

    static  void a(){
    	b();
    }
    
    static  void b(){
    	c();
    }
    
    static  void c(){
    
    }
    
     public static void main(String[] args) {
             a();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如上a、b、c3个方法,a栈帧入栈,接着b栈帧入栈,接着c栈帧入栈;然后c栈帧出栈,b栈帧出栈,直到a栈帧出栈

    在这里插入图片描述

    栈帧

    在每个Java方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。

    栈由一个个栈帧组成,每个栈帧大体都包含四个区域:局部变量表、操作数栈、动态连接、返回地址

    数据结构上与栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
    在这里插入图片描述

    局部变量表

    方法中定义的局部变量以及方法的参数存放在局部变量表中

    它是一个32位的长度,主要存放基础数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型

    它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。如果是局部的一些对象,如Object对象,则只需要存放它的一个引用地址即可。

    局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数

    操作数栈

    以压栈和出栈的方式存储操作数。主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

    动态链接

    动态链接主要服务一个方法需要调用其他方法的场景。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

    在Java源文件被编译成字节码文件时,所有变量和方法引用都作为符号引用(Symbolic Reference)保存在Class文件的常量池里。

    当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

    方法返回地址

    当一个方法开始执行后,只有两种方式可以退出:一种是遇到方法返回的字节码指令,如return语句。一种是遇见异常,并且这个异常没有在方法体内得到处理。

    不管哪种返回方式,都会导致栈帧被弹出。也就是说:栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

    程序运行中栈可能会出现两种错误:

    StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出StackOverFlowError 错误
    
    OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
    
    • 1
    • 2
    • 3

    程序计数器 (The pc Register)

    一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。

    假如线程A正在执行到某个地方,突然失去CPU的执行权,切换到线程B,然后当线程A再获得CPU执行权的时候,怎么能继续执行?这时候就需要在线程中维护一个变量,记录线程执行到的位置,它就是程序计数器。

    程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

    为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。

    程序计数器主要有两个作用:

    字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    
    多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿。
    
    • 1
    • 2
    • 3

    注意:

    如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则这个计数器为空
    
    程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
    
    • 1
    • 2
    • 3

    本地方法栈(Native Method Stacks)

    本地方法栈是为JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以它通常又叫C栈

    与虚拟机栈的区别:

    如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。如果在Java方法执行的时候调用native的方法,则会动态链接到本地方法栈执行
    
    虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。 
    
    本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
    
    方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowErrorOutOfMemoryError两种错误。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    直接内存(Direct Memory)

    直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

    在JDK1.4 中新加入了NIO类,引入了一种基于通道Channel与缓冲区Buffer的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。

    因为避免了在Java堆和Native堆中来回复制数据,在一些场景中能显著提高性能。

    本机直接内存的分配不会受到Java堆大小的限制,但是还是会受到本机总内存的大小及处理器寻址空间的限制。

    直接内存与堆内存比较

    直接内存申请空间耗费更高的性能
    
    直接内存读取IO的性能要优于普通的堆内存
    
    直接内存作用链: 本地IO -> 直接内存 -> 本地IO
    
    堆内存作用链:本地IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地IO
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其他内存

    Code Cache:JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,如JIT编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面

    GC功能需要运行在本地线程中,类似部分都需要占用内存空间。

  • 相关阅读:
    Map的clear踩坑
    JVM类加载器(详解)
    1.2.C++项目:仿mudou库实现并发服务器之时间轮的设计
    Xilinx 高速AD 设计参考(在网上找到的总结)
    jupyter notebook
    设计模式之美 - 如何理解单例模式中的唯一性?
    已解决(Python最新xlrd库读取xlsx报错)SyntaxError: invalid syntax
    ve-plus:基于 vue3.x 桌面端UI组件库|vue3组件库
    机器人操作系统ROS(22)ROS安装opencv
    在JS中使用精灵图的原理
  • 原文地址:https://blog.csdn.net/qq_38628046/article/details/127138029