天天看点

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

运行时数据区组成概述

java虚拟机所管理的内存将会包括以下几个运行时数据区域:

  1. 程序计数器:

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

  2. java虚拟机栈:

    描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个 线帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

  3. 本地方法栈:

    与虚拟机栈的作用是一样的,只不过虚拟机栈是服务java方法的,而本地方法栈是为虚拟机调用Native方法服务的。

  4. java堆:

    是java虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时创建,java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么“绝对”了。

  5. 方法区:

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。内存区域是很重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了java在运行过程中内存申请,分配,管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的规划方式和管理机制存在着部分差异,我们现在以使用最为流行的HotSpot虚拟机为例讲解。

    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

java虚拟机定义了若干种程序运行期间会使用到的运行数据区,其中有一些随着虚拟机启动而创建,随着虚拟机推出而销毁。另一些则是与线程一一对应的。这些与线程对应的区域会随着线程开始和结束而创建销毁。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

如图,红色的为多个线程共享,灰色的为单个线程私有的。

线程间共享:堆,对外内存。

每个线程:独立包括程序计数器,栈,本地方法栈。

程序计数器

概述

PC计数器存储指令相关的线程信息。CPU只有把数据装载到PC计数器才能运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

作用

程序计数器用来存储下一条指令的地址(即将执行的指令代码)。由执行引擎读取下一条指令。

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

举例:程序计数器的作用位置:

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

面试题

  1. 使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?

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

  2. 程序计数器为什么被设定为线程私有的?

    我们都知道所谓的的多线程在一个特定的时间段内只会执行其中一个线程的方法,CPU会不停做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?

    为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个程序计数器,这样一来各个线程之间就可以独立计算,总而互不干扰。

    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

java虚拟机栈

出现的背景

由于跨平台的设计,java的指令都是根据栈来设计的。不同的平台CPU架构不同,所以不能设计为基于寄存器的。基于栈的指令设计有点事跨平台,指令集小,编译器容易实现。缺点是性能下降,实现同样的功能需要更多的指令集。

分清栈和堆

栈是运行时的单位,而堆是存储的单位。

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  • 堆解决的是数据存储的问题,即数据怎么放,往哪放。
JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

java虚拟机栈是什么?

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用。java虚拟机栈时线程私有的。生命周期和线程一致。

作用

主管java程序的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

栈的特点:

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对java栈的操作只有两个:
    • 调用方法,进栈
    • 执行结果,出栈

对栈来说不存在垃圾回收的问题

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

栈中出现的异常

  • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
  • OutOfMemoryError:虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

栈中存储声明

每个线程都有自己的栈,栈中的数据都以栈帧为单位存储。

在这个线程上正在执行的每个方法都各自对应一个栈帧。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行原理

  • JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈。遵循“先进后出(后进先出)”的原则。
  • 在一条活动的线程中,一个时间点上,只会有一个活动栈,即只有当前在执行的方法的栈帧(栈顶)是有效的,这个栈帧被称为 当前栈 ,与当前栈帧对应的方法称为 当前方法 ,定义这个方法的类称为 当前类。
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。
    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

不同的线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。

如果当前方法调用了其他方法。方法返回之前,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

java方法有两种返回方式,一种是正常的函数返回,使用return指令。另一种是抛出异常,不管是哪种方式都会导致栈帧被弹出。

栈帧的内部结构

每一个栈帧中存储着:

  • 局部变量表

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

  • 操作数栈

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

  • 动态链接

    因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

  • 方法返回地址

    放荡一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

  • 一些附加信息
    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

面试题

  1. 什么情况下会出现栈溢出?

    栈溢出就是方法执行时创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生这种结果。

  2. 通过调整栈大小,就能保证不出现溢出吗?

    不能。

  3. 分配的栈内存越大越好吗?

    并不是,只能延缓这种现象的出现,可能会影响其他内存空间。

  4. 垃圾回收机制是否会涉及到虚拟机栈?

    不会。

本地方法栈

  • java虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法的调用。
  • 本地方法栈也是线程私有的。
  • 允许被实现成固定或者是可动态扩展的内存大小。内存溢出方面也是相同的。
  • 本地方法是用c语言写的。
  • 它的具体做法是在Native Methond Stack中登记native方法,在Execution Engine执行时加载本地方法库。

java堆内存

概述

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区
  • 一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域。
  • java堆区在jvm启动时的时候即被创建,其空间大小也就确定了,时jvm管理的最大一块内存空间。
  • 堆内存的大小是可以调节的。

    例:-Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)

    一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。

  • 《java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。
  • 所有的线程共享java堆,在这里还可以划分线程私有的缓冲区。
  • 《java虚拟机规范》中对java堆的描述是:所有的对象实例都应当在运行时分配在堆上。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
  • 堆,是GC(垃圾收集器)执行垃圾回收的重点区域。

对内存区域划分

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

为什么区分(代)?

将对象根据存活概率进行分类,对存活时间长的对象,放在固定区,从而减少扫描垃圾时间及GC概率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

对象创建内存分配过程

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

  1. new的新对象先放到伊甸园区,此区大小有限制。
  2. 当伊甸园的空间填满时,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
  3. 然后将伊甸园区中的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来存放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区时空的。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 什么时候去养老区呢?默认是15次,也可以设置参数。

    -XX:MaxTenuringThreshold=

  7. 在老年区,相对悠闲,当老年区内存不足时,再次触发GC:Major GC,进行养老去的内存清理。
  8. 若养老区执行了Major GC之后发现依然无法进行对象保存,就会产生OOM异常。(Java.lang.OutOfMemoryError:Java heap space)

    如:

    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区
JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

新生区与老年区配置比例

配置新生代与老年代在堆结构的占比(一般不会调)

  1. 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  2. 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
  3. 当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优
    JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

在HptSpot中,Eden空间和另外两个survivor空间缺省所占的比例时8:1:1,当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间的比例。比如:-XX:NewRatio=8

新生区的对象默认生命周期超过15,就回去养老去养老

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

分代收集思想Minor GC、Major GC、Full GC

JVM在进行GC时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区。针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集.。

  • 部分收集:不是完整收集整个java堆的垃圾收集。

    其中又分为:

    • 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.。
    • 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.。
    • 混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾.。
  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

    整堆收集出现的情况:

    • System.gc()时
    • 老年区空间不足
    • 方法区空间不足

开发期间尽量避免整堆收集。

TLAB 机制

为什么有 TLAB?

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.。

由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程

不安全的.。

为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

什么是TLAB?

TLAB是线程本地分配缓存区,这是一个线 程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在 自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项 -XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

堆空间的参数设置

-XX:+PrintFlagsInitial

查看所有参数的默认初始值

-XX:+PrintFlagsFinal

查看所有参数的最终值(修改后的值)

-Xms:初始堆空间内存(默认为物理内存的 1/64)

-Xmx:最大堆空间内存(默认为物理内存的 1/4)

-Xmn:设置新生代的大小(初始值及最大值)

-XX:NewRatio:配置新生代与老年代在堆结构的占比

-XX:SurvivorRatio:设置新生代中 Eden 和 S0/S1 空间比例

-XX:MaxTenuringTreshold:设置新生代垃圾的最大年龄

-XX:+PrintGCDetails 输出详细的 GC 处理日志

字符串常量池

字符串常量池为什么要调整位置?

JDK7中将字符串常量池放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

方法区

方法区的基本理解

方法区是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据、static final常量、static变量、即时编译器编译后的代码等数据。另外,方法区包含了一个特殊的区域“运行时常量池”。 Java虚拟机规范中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

所以,方法区看做是一块独立于java堆的内存空间。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

方法区在JVM启动时被创建,并且它的实际的物理内存空间中和java堆区一样都可以是不连续的。

方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误:java.lang.OutOfMemoryError:Metaspace.。

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

方法区,栈,堆的交互关系

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

方法区大小的设置

Java方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

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

方法区的内部结构

JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

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

通过反编译字节码文件查看

反编译字节码文件,并输出值文本文件中,便于查看。参数-p确保能查看private权限类型的字段或方法。

javap -v -p Demo.class > test.txt
           
JVM--运行时数据区运行时数据区组成概述程序计数器java虚拟机栈本地方法栈java堆内存方法区

方法区的垃圾回收

  1. 有些人认为方法区(如Hotspot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。
  2. 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。

回收废弃常量与回收Java堆中的对象非常类似。(关于常量的回收比较简单,重点是类的回收)

下面也称作类卸载

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

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

常见的面试题

说一下 JVM 内存模型吧,有哪些区?分别干什么的?

JVM 内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个 survivor

区?Eden 和 survior 的比例分配

jvm 内存分区,为什么要有新生代和老年代

讲讲 vm 运行时数据库区

什么时候对象会进入老年代?

jvm 的方法区中会发生垃圾回收吗?