一、概述

1 运行时数据区概述

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8 和之前的版本略有不同,下面会介绍到。

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 堆外内存(永久代或元空间、代码缓存也叫方法区,jdk8以后叫元空间)

2 JVM线程

二、运行时数据区详解

1 程序计数器(PC寄存器)

  • 它是一块很小的内存空间,小到几乎可以忽略不记。也是运行速度最快的存储区域
  • 在JVM规范中,每个线程都有它自己的程序计数器,是 线程私有 的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域没有GC,没有OOM

作用:PC寄存器用来存储指向下一条指令的地址`,也就是即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。

  • 使用指令:javap -v xxx.class反编译即可查看

为什么使用 PC寄存器 存储字节码指令地址有什么用呢?

  • 因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
  • JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

PC寄存器为什么被设定为私有的?重要

  • 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
  • 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

CPU时间片:简单一句话:宏观并行,微观并发

2 虚拟机栈

  • Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,每次方法调用的数据都是通过栈传递的。
  • Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈。 (Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息
    • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
    • 堆解决的是数据存储的问题,即数据怎么放,放哪里
  • Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:
    • 局部变量表(Local Variables)
    • 操作数栈(Operand Stack)(或表达式栈)
    • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
    • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
    • 一些附加信息
  • 局部变量表、操作数栈的最大深度在编译器就确定
  • Java 方法有两种返回方式:
    • return 语句。
    • 抛出异常。
    • 不管哪种返回方式都会导致栈帧被弹出。

举例栈溢出的情况?(StackOverflowError)

  • 通过 -Xss 设置栈的大小

    • 当整个内存空间不足再扩容就会出现OOM
  • 递归很容易出现栈溢出

Java 虚拟机栈会出现两种错误StackOverFlowErrorOutOfMemoryError

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

调整栈大小,就能保证不出现溢出么?

  • 不能保证不出现溢出,只能让栈溢出出现的时间晚一点,不可能不出现

分配的栈内存越大越好么?

  • 不是,一定时间内降低了栈溢出的概率,但是会挤占其它的线程空间,因为整个虚拟机的内存空间是有限的

垃圾回收是否涉及到虚拟机栈?

  • 不涉及
  • 出栈操作就相当于垃圾回收

方法中定义的局部变量是否线程安全?

  • 何为线程安全?

    • 如果只有一个线程才可以操作此数据,则必是线程安全的。
    • 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
  • 具体问题具体分析:

    • 如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

2.1 局部变量表

总体介绍
  • 局部变量表,最基本的存储单元是Slot(变量槽)

  • 局部变量表中存放编译期可知的 各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

  • 在局部变量表里,32位以内的类型只占用一个slot(包括 引用类型、returnAddress类型),64位的类型(long和double)占用两个slot。

    • byte、short、char 在存储前被转换为int,boolean 也被转换为int,0 表示false,非0 表示true
    • long 和 double 则占据两个Slot
为什么静态方法中不能用this?

如果当前帧是 构造方法或者实例方法(非静态方法) 创建的,那么该 对象引用this 将会 存放在index为0 的slot处 ,其余的参数按照参数表顺序继续排列。

  • 静态方法的局部变量表中没有this, 因为this变量不存在于当前方法的局部变量表中
slot能重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量出了其作用域,那么在其作用域之后声明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

补充说明
  • 在栈帧中,与性能调优最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

  • 局部变量表的变量也是重要的垃圾回收根节点,只要被局部变量表直接或间接引用的对象都不会被回收

局部变量线程安全吗?

那必须的

2.2 操作数栈

什么是操作数栈?
  • 每一个独立的栈帧除了包含局部变量表以外,还包含一个 后进先出(Last - In - First -Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈作用
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈就是JVM执行引擎的一个工作区

2.3 动态链接(Dynamic Linking)

什么是动态链接?
  • 每一个栈帧内部都包含一个指向运行时常量池该栈帧所属方法的引用
  • 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令。
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里
  • 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
补充 方法的调用:解析和委派

2.4 方法返回地址(return address) 不重要

  • 存放 调用该方法的pc寄存器的值

  • 一个方法的结束,有两种方式:

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

  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

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

2.5 一些附加信息 略

3 本地方法栈

  • 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

4 堆

  • 堆是Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存的大小是可以调节的
  • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
    • 从实际使用角度看的:“几乎”所有的对象实例都在这里分配内存。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的

4.1 堆内存介绍

  • JDK1.7 及其之前堆内存被分为:新生代(Young Generation)+ 养老区(Old Generation) +永久代(Permanent Generation)
  • JDK1.8及其之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间

现在的堆空间只包括新生代和老年代,永久代或者元空间是在方法区进行具体的落地实现

  • 通过jvisual查看

4.2 堆内存细分

  • 存储在JVM中的Java对象可以被划分为两类:

    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
  • Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)

新生代与老年代介绍

  • 几乎所有的Java对象都是在Eden区被new出来的。

  • 绝大部分的Java对象的销毁都在新生代进行了(有些大的对象在Eden区无法存储时候,将直接进入老年代

    • IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
  • 新生区的对象默认生命周期超过 15 ,将进入老年代

4.3 堆中的报错有哪些?

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  • java.lang.OutOfMemoryError:GC Overhead Limit Exceeded: 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError:Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。它和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap size (opens new window)。

4.4 对象分配过程是怎样的?进行垃圾回收的时机?(重要 重要)

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

总结

  • 针对s0,s1区总结:复制之后有交换,谁空谁是to。

    • 永远有一个幸存者区是空的。
  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。

4.5 Minor GC(或Young GC)、Major GC、Full GC

我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(Stop the World)的问题,r2而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上**

JVM在进行GC时,并非每次都对上面三个内存( 新生代、老年代、方法区 )区域一起回收的,大部分时候回收的都是指新生代

  • 新生代收集( Minor GC/Young GC ):只是新生代( Eden、S0/S1 )的垃圾收集
  • 老年代收集( Major GC/Old GC ):只是老年代的垃圾收集。
    • 目前,只有CMS GC会有单独收集老年代的行为 。也就是说其它GC执行Major GC的时候可不会只收集老年代的垃圾。
    • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    • 目前,只有G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

补充说明

  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor区满不会触发GC。会有STW
  • 出现了MajorGc,经常会伴随至少一次的Minor GC
    • 但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程
    • 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC

4.6 触发Full GC执行的情况有哪些?(重要)

  • 调用System.gc( )时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小 大于 老年代的可用内存
  • 由Eden区、survivor space0(From Space)区 向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存 小于 该对象大小

说明:Full GC 是开发或调优中尽量要避免的。这样STW时间会短一些

4.7 堆空间都是共享的么?什么是TLAB?

堆空间都是共享的么

不一定,因为还有TLAB这个概念,在堆中划分出一块区域,为每个线程所独占

什么是TLAB

  • 对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量;
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%
  • 每个线程分配对象到堆空间的时候会优先分配到线程自己所属的那一块堆空间中
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

4.8 空间分配担保机制(Minor GC之前的操作,重要)

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间 是否大于 新生代所有对象的总空间。

  • 如果大于,则此次Minor GC是安全的

  • 如果小于,则虚拟机会查看 -XX:HandlePromotionFailure 设置值是否允许担保失败。

    • 如果HandlePromotionFailure=true,那么会继续 检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小

      • 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的,比如晋升对象大于平均值;
      • 如果小于,则进行一次Full GC
    • 如果HandlePromotionFailure=false,则进行一次Full GC

为什么要进行空间担保?

是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。

老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。

4.9 致命面试题 - 所有对象都在堆中分配内存吗?(重要)

4.9.1 堆是分配对象存储的唯一选择吗?

在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:

  • 随着JIT编译期的发展与 逃逸分析技术 逐渐成熟,栈上分配、标量替换优化技术 将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
  • 在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是 如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配 。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
  • 此外,前面提到的基于OpenJDK深度定制的TaoBao VM( 淘宝虚拟机 ),其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
4.9.2 什么是逃逸分析?
  • 逃逸分析的基本行为就是分析对象动态作用域:

    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
    • 当一个对象在方法中被定义后,它 被外部方法所引用,则认为发生逃逸 。例如作为调用参数传递到其他地方中。
  • 在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
  • 如果使用的是较早的版本,开发人员则可以通过:
    • 选项 -XX:+DoEscapeAnalysis 显式开启逃逸分析
    • 通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
4.9.3 栈上分配、同步省略、分离对象(也称 标量替换)

使用逃逸分析,编译器可以对代码做如下优化:

  • 栈上分配将堆分配转化为栈分配。经过逃逸分析法相,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配
  • 同步省略(锁消除)如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步
    • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象只能够被一个线程访问而没有被发布到其他线程。那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除
  • 标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
    • 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会 把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换
4.9.4 在HotSpot虚拟机中所有的对象都创建在堆上
  • Oracle Hotspot JVM是通过 标量替换 实现逃逸分析的,没有使用栈上分配

5 方法区

5.1 方法区存储的是什么?

  • 方法区存着类的信息,常量和静态变量,即类被编译后的数据。这个说法其实是没问题的,只是太笼统了。

  • 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。更加详细一点的说法是方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用

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

5.2 栈、堆、方法区的交互关系?

  • Person 类的 .class信息存放在方法区中;
  • person 变量 存放在 Java 栈的局部变量表中;
  • 真正的 person 对象存放在 Java 中。

5.3 方法区和永久代、元空间之间的关系?

方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代(jdk7以前)、元空间(jdk8以后)就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。

5.4 字符串常量池、Class常量池和运行时常量池

参考:https://blog.csdn.net/qq_26222859/article/details/73135660

字符串常量(string pool也有叫做string literal pool)

字符串常量池里的内容是类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到字符串常量池(String Pool)中

字符串常量池中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。

在HotSpot VM里实现的String Pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了驻留字符串的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

Class常量池(class constant pool)

当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池。

.class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量( 文本字符串、被声明为final的常量、基本数据类型的值 )和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。

常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。

运行时常量池(runtime constant pool)

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将Class常量池中的内容存放到运行时常量池中

由此可知,运行时常量池也是每个类都有一个。在上面我也说了,Class常量池中存的是字面量符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

5.5 字符串常量池的位置(重要)

  • JDK1.7之前运⾏时常量池逻辑包含让字符串常量池存放在⽅法区发, 此时hotspot虚拟机对⽅法区的实现为永久代
  • JDK1.7把 字符串常量池、静态变量拿出来放在了堆中,但是其他的仍在方法区(永久代)
  • JDK1.8 hotspot移除了永久代⽤元空间(Metaspace)取⽽代之,这时候字符串常量池、静态变量还在堆,运⾏时常量池还在⽅法区

5.6 JVM 常量池中存储的是对象还是引用呢?

  • 只要是对象都在在堆中(针对HotSpot虚拟机)

5.7 方法区中的垃圾回收?(常量池中废弃的常量和不再使用的类型)

  • 方法区内常量池之中主要存放的两大类常量:字面量和符号引用
  • 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

5.8 如何判定一个类是否不被使用?

方法区的回收相当困难判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例(没有继承关系)。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

6 直接内存

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError 错误出现。
  • JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
  • 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

三、补充知识

1 方法的调用:解析和分派

1.1 静态链接与动态链接

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接:

    • 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法 在编译期确定,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
  • 动态链接:
    • 如果 被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。

1.2 方法的绑定机制

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

  • 早期绑定

    • 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定

    • 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

1.3 虚方法和非虚方法

非虚方法

  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为 非虚方法
  • 静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法
    • 这里的父类方法可能是指子类super.fun()调用父类方法
  • 其他方法称为虚方法

子类对象的多态性的使用前提

  • 类的继承关系
  • 方法的重写

虚拟机中调用方法的指令

  • 普通调用指令:

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

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

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

1.4 虚方法表

  • 为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,非虚方法不会出现在表中。使用索引表来代替查找。
  • 虚方法表是什么时候被创建的呢? 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。
  • 如果类中重写了方法,那么调用的时候,就会直接在该类的虚方法表中查找

2 对象什么情况会进入老年代?(重要 重要 重要)

  • 大对象:所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。

    • 参考:https://blog.csdn.net/w605283073/article/details/94363110
  • 长期存活的对象:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认是15岁), 就将会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
  • 动态对象年龄判定:为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
  • 空间分配担保:在一次安全Minor GC 中,存活的对象不能在另一个Survivor 完全容纳,则会通过担保机制进入老年代。

2 内存溢出(Out Of Memory)

2.1 什么是内存溢出(Out Of Memory)

  • IavaDoc中对OutOfMemoryError的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存
  • 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。

内存没有空闲的情况分析

  • Java虚拟机的堆内存设置不够;
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
  • 内存泄漏的堆积最终会导致内存溢出

在抛出0utOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间

  • 在抛出0utOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间。
  • 但也不是在任何情况下垃圾收集器都会被触发的。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接拋出OutOfMemoryError

2.2 内存溢出发生的地方

JVM 运行时数据区主要包括:

  • 程序计数器
  • 虚拟机栈;
  • 本地方法栈;
  • Java堆;
  • 方法区(运行时常量池是方法区的一部分);
  • 直接内存;(直接内存并不是运行时数据区的一部分,但这部分内存也会被频繁的使用)

在JVM中,除了程序计数器之外,其他几个运行时数据区的区域都有发生OOM(内存溢出)异常的可能,如下:

  • 堆溢出;
  • 虚拟机栈和本地方法栈溢出;
  • 方法区和运行时常量池溢出;
  • 直接内存溢出;

JVM总结之内存区域详解相关推荐

  1. jvm之java内存区域详解篇guide哥yyds

    jvm 一.java内存区域详解 1.运行时数据区域 线程私有的: 虚拟机栈 本地方法栈 程序计数器 线程共享的: 堆 方法区 直接内存(非程序运行时数据区的一部分) 1.1什么是程序计数器 程序计数 ...

  2. 【JVM】Java内存区域详解(通俗易懂系列)

    Java虚拟机拥有管理内存的权利. 一.运行时数据区 在Java程序执行的过程中,Java虚拟机会将它管理的内存分为若干个不同数据区域(JDK1.8与之前版本不同) 线程私有: 虚拟机栈 本地方法栈 ...

  3. JVM内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)

    参考文章: https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html https://www.cnblogs.com/duanx ...

  4. [jvm]运行时数据区域详解

    了解虚拟机是怎么使用内存的,有助于我们解决和排查内存泄漏和溢出方面的问题.详解java虚拟机内存的各个区域,分析这些区域的作用服务对象以及可能发生的问题. 一.运行时数据区域 java虚拟机在执行ja ...

  5. java eden space_JVM虚拟机20:内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)...

    1.内存区域划分 根据我们之前介绍的垃圾收集算法,限定商用虚拟机基本都采用分代收集算法进行垃圾回收.根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法.大批对象死去.少量 ...

  6. java eden space_JVM内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)...

    JVM区域总体分两类,heap区和非heap区. heap区又分为: Eden Space(伊甸园). Survivor Space(幸存者区). Old Gen(老年代). 非heap区又分: Co ...

  7. java old区域_JVM内存区域详解(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)...

    JVM区域总体分两类,heap区和非heap区. heap区又分为: Eden Space(伊甸园). Survivor Space(幸存者区). Old Gen(老年代). 非heap区又分: Co ...

  8. jvm堆外内存排查详解

    文章目录 前言 一.堆外内存排查 1.背景 2.内存对比 3.堆外内存检查 4.排查堆外内存 5.glibc内存泄露 结尾 前言 内存泄漏想必大家并不陌生,对于jvm的内存泄漏,有很多排查手段和方便的 ...

  9. JVM 运行时内存空间详解——元空间

    通过上一篇文章,我们大体了解了JVM的整体架构,其分为:元数据(JDK7是方法区).堆.虚拟机栈.本地方法栈.程序计数器几个部分. 本篇文章,咱们对元空间进行剖析,一探究竟. 1. 元空间介绍 在JD ...

最新文章

  1. 一、pytorch搭建实战以及sequential的使用
  2. SAP 电商云 Spartacus UI added-to-cart 的端到端测试源代码解析
  3. python接口测试非json的断言_荐在接口自动化测试中,如何利用Pytest + JSONPath 进行接口响应断言...
  4. vuefullcalendar怎么判断切换上下月_房间太多、楼上楼下,终极解决家里wifi信号无缝切换问题...
  5. Uncaught RangeError: Maximum call stack size exceeded解决思路
  6. [C#][控件]列表控件listbox(一)
  7. CG之菲涅尔效果简单实现
  8. c++ opencv mat_【CV实战】OpenCV—Hello world代码示例
  9. C语言判断素数的几种方法
  10. 网络抓包工具之Wireshark
  11. IM“扫一扫”功能很好做?看看微信“扫一扫识物”的完整技术实现
  12. 计算机登录界面没有用户显示不出来,win7让administrator账户不出现在登陆界面方法...
  13. 快速文本分类(FastText)
  14. python--pandas长宽数据转换
  15. 操作系统C语言模拟内存分配算法的模拟实现
  16. 导出Excel时出现80080005错误的解决办法
  17. 计算机一个字节是几位,电脑的一个字节等于多少位??
  18. document.getElementsByTagName()返回值
  19. php disconf,未主/disconf
  20. 电信主机计费系统_数据采集子模块

热门文章

  1. 继承extends,inherit
  2. MySQL之存储引擎及SQL优化
  3. 网易云音乐 HTML5 随机播放器
  4. 凤凰财经专访爱加密:打造完整安全服务体系,做移动金融“卫道夫”
  5. 秒拍联合高德地图发起“交警探路vlog”活动 上线“温暖回家路”专区
  6. python 生日快乐歌_祝你生日快乐!
  7. 吞吐量测试手机软件,WIFI吞吐量测试.ppt
  8. 基于Blinker物联网+Esp8266的燃气灶温度与燃气残留浓度监控装置的设计与实现之二
  9. android ndk standalone,Android NDK make-standalone-toolchain因mips而失败
  10. qss 更改图标_qss 界面大全