Java内存区域与内存溢出异常

Java内存区域与内存溢出异常


Java和C++之间有一堵由内存动态分配和垃圾回收技术所围成的高墙。

1、运行时数据区

请添加图片描述

1.1、程序计数器

  1. 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器

  2. 在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

  3. 为什么程序计数器是线程私有的?
    由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储
    请添加图片描述

  4. 如果线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,这个计数器值应该是空

  5. 这是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError情况的区域

1.2、Java虚拟机栈

  1. Java虚拟机栈也是线程私有的,它与线程的生命周期相同

  2. Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表操作数栈动态链接方法返回地址等信息。
    在这里插入图片描述

  3. 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  4. 对于栈来说不存在垃圾回收问题(栈存在溢出的情况)。

  5. 可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

1.2.1、局部变量表
  1. 局部变量表主要存放方法的参数和定义在方法体内的局部变量,包括编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)对象引用(reference类型,它并不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
    在这里插入图片描述

  2. 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
    在这里插入图片描述

  3. 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位的长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个
    在这里插入图片描述

  4. 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的

1.2.1.1、静态变量与局部变量的对比
  • 我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”(类加载器子系统中“链接阶段”(加载——>链接(验证、准备、解析)——>初始化)的“准备阶段”),执行系统初始化,对类变量设置零值;另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值
    在这里插入图片描述

  • 和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
    在这里插入图片描述

1.2.2、操作数栈(Operand Stack)
  1. 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)。
  2. 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
    • 比如:执行复制、交换、求和等操作
  3. 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  4. 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
  5. 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
1.2.3、动态链接(Dynamic Linking)
  1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
  2. 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
    在这里插入图片描述
1.2.3.1、方法的调用:解析与分配
  1. 静态链接
    当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

  2. 动态链接
    如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。

    静态链接和动态链接不是名词,而是动词,这是理解的关键。

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

  1. 早期绑定
    早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  2. 晚期绑定
    如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C语言中的虚函数(C中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

  1. 虚方法和非虚方法

    如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。

    静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法
    虚拟机中提供了以下几条方法调用指令:

    普通调用指令:

    • invokestatic:调用静态方法,解析阶段确定唯一方法版本
    • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
    • invokevirtual:调用所有虚方法
    • invokeinterface:调用接口方法

    动态调用指令:

    • invokedynamic动态解析出需要调用的方法,然后执行

    前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fina1修饰的除外)称为虚方法

1.2.3.2、方法重写的本质
  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.1ang.AbstractMethodsrror异常。
1.2.4、方法返回地址(return address)
  1. 存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:

    1. 正常执行完成
    2. 出现未处理的异常,非正常退出

    无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

  2. 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值


1.3、本地方法栈

  1. 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务

1.4、Java堆

  1. Java堆(Java Heap)是虚拟机所管理的内存中最大的一块Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  2. Java堆唯一目的就是存放对象实例
1.4.1、堆(Heap)的核心概述
1.4.1.1、堆内存细分
  1. Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区

    • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
    • Tenure generation space 养老区 Old/Tenure
    • Permanent Space 永久区 Perm

    在这里插入图片描述

  2. Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

    • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
    • Tenure generation space 养老区 Old/Tenure
    • Meta Space 元空间 Meta

    在这里插入图片描述

1.4.2、设置堆内存大小与OOM
  1. Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。

    • -Xms"用于表示堆区的起始内存,等价于-XX:InitialHeapSize
    • “-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize

    一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常

    默认情况下:

    • 初始内存大小:物理电脑内存大小 / 64
    • 最大内存大小:物理电脑内存大小 / 4
1.4.3、年轻代与老年代
  1. Java堆区进一步细分的话,可以划分为==年轻代(YoungGen)老年代(oldGen)==

    其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)

    在这里插入图片描述

  2. 配置新生代与老年代在堆结构的占比:

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
  3. 几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。

1.4.4、图解对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
  3. 然后将伊甸园中的剩余对象移动到幸存者0区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
  5. 如果再次经历垃圾回收此时会重新放回幸存者0区,接着再去幸存者1区
  6. 啥时候能去养老区呢?可以设置次数。默认是15次
    • 可以设置参数:进行设置-Xx:MaxTenuringThreshold= N
  7. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
  8. 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。

总结:

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集。
1.4.5、Minor GC,MajorGC、Full GC

针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)

部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC / Old GC):只是老年代的圾收集
  • 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集(目前,只有G1 GC会有这种行为)

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集

1.4.5.1、最简单的分代式GC策略的触发条件

年轻代GC(Minor GC)触发机制:

  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。)
  • 因为Java对象大多都具备朝生夕灭的特性.,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC(Major GC / Full GC)触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了
  • 出现了Major Gc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
    • 也就是在老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
  • 如果Major GC后,内存还不足,就报OOM了

Full GC触发机制(触发Full GC执行的情况有如下五种:):

  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
1.4.6、为对象分配内存:TLAB
1.4.6.1、为什么有TLAB(Thread Local Allocation Buffer)?
  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
1.4.6.2、什么是TLAB?
  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

在这里插入图片描述

  • 在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项 “-XX:TLABWasteTargetPercent” 设置TLAB空间所占用Eden空间的百分比大小。

1.5、方法区

栈、堆、方法区的交互关系:

在这里插入图片描述

1.5.1、方法区的理解
  1. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
  2. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace
  3. 在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代
  4. 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
1.5.2、设置方法区大小与OOM

设置方法区内存的大小:

jdk7及以前

  • 通过来设置永久代初始分配空间。默认值是20.75M-XX:Permsize
  • 通过来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M-XX:MaxPermsize
  • 当JVM加载的类信息容量超过了这个值,会报异常OutOfMemoryError:PermGen space

JDK8以后

  • 元数据区大小可以使用参数 -XX:MetaspaceSize-XX:MaxMetaspaceSize指定
  • 默认值依赖于平台。windows下,-XX:MetaspaceSize=21M -XX:MaxMetaspaceSize=-1//即没有限制
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
  • -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
1.5.3、方法区的内部结构

在这里插入图片描述

1.5.3.1、方法区(Method Area)存储什么?

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

在这里插入图片描述

  1. 类型信息

    对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

    • 这个类型的完整有效名称(全名=包名.类名)
    • 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
    • 这个类型的修饰符(public,abstract,final的某个子集)
    • 这个类型直接接口的一个有序列表
  2. 域(Field)信息

    域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

  3. 方法(Method)信息

    JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

    1. 方法名称

    2. 方法的返回类型(或void)

    3. 方法参数的数量和类型(按顺序)

    4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)

    5. 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)

    6. 异常表(abstract和native方法除外)

      每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

1.5.4、 运行时常量池 VS 常量池

在这里插入图片描述

  • 方法区,内部包含了运行时常量池

  • 字节码文件,内部包含了常量池

    一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用

为什么需要常量池?

一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。

  1. 运行时常量池(Runtime Constant Pool)是方法区的一部分
  2. 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  3. JVM为每个已加载的类型(类或接口)都维护一个常量池。
  4. 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址
  5. 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值