JVM系列3:JVM运行时数据区(详解)

1.运行时数据区组成概述

JVM的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从Java虚拟机规范,Java8虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区:
1.程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
2.Java虚拟机栈(Java Virtual Machine Stacks)
描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
3.本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务java方法的,而本地方法栈是为虚拟机调用Native方法服务的。
4.Java堆(Java Heap)
是java虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
5.方法区(Method Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即是编译后的代码等数据。
JVM系列3:JVM运行时数据区(详解)_第1张图片

线程共享:堆,堆外内存;
每个线程独立:程序计数器、栈、本地方法栈。

2.程序计数器(Progarm Counter Register)

1.概述
JVM中的程序计数寄存器中的Register命名源于CPU的寄存器,寄存器存储指令相关的线程信息,CPU只有把数据装载到寄存器才能运行。
这里的寄存器并非广义的物理寄存器,应该称其为PC计数器(或指令计数器)也称程序钩子,JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
2.作用
程序计数器用来存储下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。
JVM系列3:JVM运行时数据区(详解)_第2张图片

  • 它是一块很小的内存空间,几乎可以省略不计,也是运行速度最快的存储区域。
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的java方法的JVM指令地址,如果是在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
    3.面试题
    (1)使用程序计数器存储字节码指令地址有什么用?/为什么使用程序计数器记录当前线程的执行地址?
    因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿开始继续执行。
    JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
    (2)程序计数器为什么被设定为线程私有的?
    所谓多线程在一个特定的时间内会执行其中某一个线的方法,CPU会不停地做任务切换,这样必然会导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法是为每个线程都分配一个程序计数器,这样各个线程之间便可以独立计算,互不干扰。

3.Java虚拟机栈(Java Virtual Machine Stacks)

1.虚拟机栈出现的背景
由于跨平台的设计,java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更多的指令集。
2.分清栈和堆
栈是运行时单位,堆是存储单位。
即:栈解决程序的运行问题,即程序如何执行或者如何处理数据;堆解决的是数据存储问题,即数据怎么放,放在哪儿。
JVM系列3:JVM运行时数据区(详解)_第3张图片
3.Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈,每个线程在创建时都会创建一个虚拟机,其内部保存的是一个个栈帧,对应着一次方法的调用。
Java虚拟机是线程私有的。
生命周期和线程保持一致。

4.作用
主管Java程序的运行,它保存方法的局部变量(八种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
eg:
JVM系列3:JVM运行时数据区(详解)_第4张图片
5.栈的特点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器;

  • JVM直接对java栈的操作只有两个:调用方法,进栈;执行结束,出栈;

  • 对于栈来说不存在垃圾回收问题。
    JVM系列3:JVM运行时数据区(详解)_第5张图片
    6.栈中出现的异常

  • StackOverflowError:线程请求的栈的深度大于虚拟机所允许的深度;

  • OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
    7.栈中存储的是什么?
    每个线程都有自己的栈,栈中的数据都以栈帧为单位存储。在这个线程上执行的每个方法都各自对应一个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
    8.栈的运行原理

  • JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/后进先出的原则;

  • 在一条活动的线程中,一个时间点上,只会有一个活动栈。即只有当前执行的方法的栈帧(栈顶)是有效的,这个栈帧被称为当前栈(Current Method),定义这个方法的类称为当前类(Current Class)。

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作;

  • 如果在该方法中调用了其他方法,对应的新的线帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。
    JVM系列3:JVM运行时数据区(详解)_第6张图片
    不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。
    如果当前方法调用了其他方法,方法返回之际,当前线帧会传回此方法的执行结果给前一个线帧,接着虚拟机会丢弃当前线帧,使得前一个线帧重新成为当前线帧。
    Java方法有两种返回的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管哪种方式,都会导致线帧被弹出。
    9.线帧的内部结构
    每个线帧中存储着:

  • 局部变量表(Local Variables):局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。

  • **操作数栈(表达式栈Operand Stack):**栈最经典的一个应用就是用来对表达式求值,在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根结底就是进行计算的过程。因此可以说:程序中的所有计算过程都是在借助于操作数栈来完成的。

  • **动态链接(Dynamic Linking指向运行时常量池的方法引用):**因为在方法执行的过程中有可能需要用到类中的常量,所以必须有一个引用指向运行时常量。

  • **方法返回地址(Return Address 方法正常退出或异常退出定义):**当一个方法执行完毕之后,要返回之前它调用的地方,因此在栈帧中必须保存一个方法返回地址。

  • 一些附加信息

JVM系列3:JVM运行时数据区(详解)_第7张图片
10.面试题
(1)什么情况下会出现栈溢出(StackOverflowError)?
栈溢出就是方法执行时创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
(2)通过调整栈大小,就能保证不出现溢出吗?
不能。
(3)分配的栈内存越大越好吗?
并不是,只能延缓这种现象的出现,可能会影响其他内存空间。
(4)垃圾回收机制是否会涉及到虚拟机栈?
不会。

4.本地方法栈(Native Method Stack)

1.Java虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法的调用。
2.本地方法栈也是线程私有的。
3.允许被实现成固有或者是可动态扩展的内存大小,内存溢出方面也是相同的。
如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError;
如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存就会抛出OutOfMemoryError。
4.本地方法是用C语言写的。
5.它的具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

5.Java堆内存

1.堆内存概述
JVM系列3:JVM运行时数据区(详解)_第8张图片

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域;
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间;
  • 堆内存大小是可以调节的。eg:-Xms10m(堆起始大小) -Xmx30m(堆最大内存大小)。一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率;
  • 《Java虚拟机规范》规定,堆可以处于物理不连续的内存空间,但逻辑上它应该被视为连续的;
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区;
  • 《Java虚拟机规范》中对java堆的描述是:所有的对象实例都应该在运行时分配在堆上;
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除;
  • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

2.堆内存区域划分
Java8及以后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为Eden(伊甸园)区和Survivor(幸存者)区
JVM系列3:JVM运行时数据区(详解)_第9张图片

3.为什么分区?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
JVM系列3:JVM运行时数据区(详解)_第10张图片
4.对象创建内存分配过程
为对象分配内存是一件非常严谨和复杂的任务,JVM设计者们不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完成内存回收后是否会在内存空间中产生内存碎片。
(1)new的新对象先放到伊甸园区,此区大小有限制;
(2)当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不在被其他对象所引用的对象进行销毁,再加载新的对象放入伊甸园区;
(3)然后将伊甸园区中剩余的对象移动到幸存者0区;
(4)如果再次触发垃圾回收,此时上次幸存下来放入幸存者0区的对象,如果没有回收,就会被放入幸存者1区,每次会保证有一个幸存者区是空的;
(5)如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区;
(6)什么时候去老年区,默认是15次,也可以设置参数:-XX:MaxTenuringThreshold=< N>;
(7)在老年区,当老年区内存不足时再次触发GC:Major GC,进行老年区的内存清理;
(8)如果老年区执行了Major GC之后依然无法进行对象保存,就会产生OOM异常:Java.lang.OutOfMemoryError:Java heap space。
5.新生区与老年区配置比例
配置新生区与老年区在堆结构中的占比(一般不会调):

  • 默认**-XX:NewRatio **=2,表示新生代占1,老年代占2,新生代占整个堆的三分之一;
  • 可以修改**-XX:NewRatio**=4,表示新生区占1,老年区占4,新生区占整个堆的五分之一;
  • 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年区的大小,来进行调优。
    JVM系列3:JVM运行时数据区(详解)_第11张图片
    在HotSpot中,Eden空间和另外两个survivor空间缺省所占比例是8:1:1,开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。
    JVM系列3:JVM运行时数据区(详解)_第12张图片
    6.分代收集思想Minor GC、Major GC、Full GC
    JVM在进行GC时,并非每次都新生区和老年区一起回收,大部分时候回收的都是指新生区,针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
    (1)部分收集:不是完整收集整个java堆的垃圾收集,其中又分为:
    新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集;
    老年区(Major GC/Old GC):只是老年区的垃圾收集;
    混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾。
    (2)整堆收集(Full GC):集整个java堆和方法区的垃圾收集。
    整堆收集出现的情况:System.gc();时、老年区空间不足、方法区空间不足。
    开发期间尽量避免整堆收集。
    7.TLAB机制
    (1)什么是TLAB?
    TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
    如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
    JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样就可以避免线程同步,提高了对象分配的效率。
    TLAB空间内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
    JVM系列3:JVM运行时数据区(详解)_第13张图片
    (2)为什么会有TLAB(Thread Local Allocation Buffer)
    堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
    由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
    为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
    8.堆空间的参数设置
    官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
    -XX:+PrintFlagsInitial 查看所有参数的默认初始值
    -XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
    -Xms:初始堆空间内存(默认为物理内存的 1/64) -Xmx:最大堆空间内存(默认为物理内存的 1/4)
    -Xmn:设置新生代的大小(初始值及最大值)
    -XX:NewRatio:配置新生代与老年代在堆结构的占比
    -XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例
    -XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄
    -XX:+PrintGCDetails 输出详细的 GC 处理日志
    9.字符串常量池
    字符串常量池为什么要调整位置?
    JDK8中将字符串常量池放到了堆空间中,因为永久代(方法区)的回收效率很低,在Full GC的时候才会执行永久代(方法区)的垃圾回收,而Full GC是老年代空间不足、永久代(方法区)不足时才会触发。这就导致StringTable回收率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代(方法区)内存不足。放在堆里,能及时回收内存。

6.方法区

1.方法区的基本理解
方法区,是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据、static final常量、static变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。
Java虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开,所以,方法区看做是一块独立于java堆的内存空间
JVM系列3:JVM运行时数据区(详解)_第14张图片
方法区在JVM启动时被创建,并且它的实际的物理内存空间中和java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误:java.lang.OutOfMemoryError:Metaspace。
方法区、栈、堆的交互关系
JVM系列3:JVM运行时数据区(详解)_第15张图片
2.方法区大小设置
java方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

  • 元 数 据 区 大 小 可 以 使 用 参 数 -XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的两个参数.
  • 默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB,
  • -XX:MaxMetaspaceSize 的值是-1,级没有限制.
  • 这个-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦 触及 就会触发 Full GC
  • 因此为了减少 FullGC 那么这个-XX:MetaspaceSize 可以设置一 个较高的值。

3.方法区的内部结构
JVM系列3:JVM运行时数据区(详解)_第16张图片
方法区它用于存储已被虚拟机加载的类型信息、常量、静态常量、即时编译器编译后的代码缓存,运行常量池等。
通过反编译字节码文件查看
反编译字节码文件,并输出至文本文件中,便于查看,参数-p确保能查看private权限类型的字段或方法:java -v -p Demo.class>test.txt
JVM系列3:JVM运行时数据区(详解)_第17张图片
4.方法区的垃圾回收
(1)有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
(2)一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相对苛刻,但是这部分区域的回收有时又确实是必要的。
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
回收废弃常量与回收Java堆中的对象非常类似。(关于常量的回收比较简单,重点是类的回收)
下面也称做类卸载
判断一个常量是否“废弃”相对简单,而要判断一个类型是否属于“不再被使用的类”的条件相对苛刻。需要同时满足三个条件:

  • 该类所有的实例都已被回收,也就是Java堆中不存在该类及其任何派生子类的实例;
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,否则通常是很难达成的;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过放射访问该类的方法。
    5.常见面试题
    说一下 JVM 内存模型吧,有哪些区?分别干什么的? JVM 内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个 survivor 区?Eden 和 survior 的比例分配 jvm 内存分区,为什么要有新生代和老年代 讲讲 vm 运行时数据库区 什么时候对象会进入老年代? jvm 的方法区中会发生垃圾回收吗?

你可能感兴趣的:(笔记,jvm,java)