5K字带你彻底了解JVM运行时内存
1.程序计数器
概念
程序计数器也叫作PC寄存器,是一块很小的内存区域,可以看做是当前线程执行的字节码的行号指示器。字节码的解释工作就是通过改变程序计数器里面的值来获得下一条需要执行字节码的指令。
特点
- Pc寄存器表现为一块内存,功能是存放伪指令,确切的说是存放的将要执行指令的地址。
- 当虚拟机正在执行的是一个native方法时,JVM的PC寄存器存储的值是undefined。
- 程序计数器是线程私有的,它的生命周期和线程一样,每个线程只有一个。这也是为了保证多线程下,线程切换后能恢复到正确的执行位置,所以每个线程需要独立的程序计数器,相互隔离互不影响。
- 此区域是唯一一个没有OOM情况的区域。
图例
2.虚拟机栈
概念
JAVA虚拟机栈的生命周期和线程相同,他也是线程私有的,每一个线程有自己独立的虚拟机栈。他用来存储栈帧,程序运行时,每一个方法被调用执行时都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
图例演示
栈帧
栈帧是支持虚拟机方法调用和执行的数据结构。栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法的调用和执行完成都对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
设置虚拟机栈的大小
-Xss为JVM启动时的每个线程分配的内存大小,也就是可以设置线程栈的大小。
-Xss1m # 单位为MB -Xss1024k #单位为KB -Xss1048576 #字节大小
局部变量表
局部变量表是一组变量值存储空间,用于存放方法的参数和方法内定义的局部变量。
操作数栈
操作数栈是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或者对象实例的字段中复制常量或者变量写到操作数栈,再随着计算的进行会将栈中的元素出栈到局部变量表或者返回给方法调用者。
动态链接
java虚拟机中,每一个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的为了支持方法调用过程中的动态链接。 动态链接的作用:将符号引用转换为直接引用。
方法返回地址
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
3.本地方法栈
概念
本地方法栈则是为虚拟机使用到的本地(Native) 方法服务,而虚拟机栈是为使用到的java方法服务。
关于native方法
native关键字修饰的Java方法是一个原生态方法,方法对应的实现Java作用范围达不到,而是在用其他编程语言(如C和C++)文件中实现。Java语言本身不能直接对操作系统底层进行访问和操作,但可以通过JNI接口调用其他编程语言来实现对操作系统底层的访问。 native方法在异地实现,类似抽象方法,不能有方法体,要以分号结束。例如:
本地方法栈特点
- 本地方法栈加载nativef方法,是为了填补java不方便实现的场景产生的。
- 虚拟机栈为为虚拟机执行java服务,而本地方法栈为了执行虚拟机所使用到的native服务
- 本地方法栈也是线程私有的,和线程的生命周期是一致的,每个线程都有一个本地方法栈。
4.堆
4.1 堆的总括
4.1.1 概念
Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。
4.1.2 特点
- 堆是Java虚拟机所管理内存中最大的一块区域。
- 堆是线程共享的。
- 堆在虚拟机启动的时候创建。
- 堆存在的目的就是存放对象实例。
- 堆是垃圾回收管理的主要区域。因此堆又被称作为GC堆,JAVA堆还可以细分为新生代,老年代,永久代(jdk8以后就取消了),其中新生代又分为Eden空间、From survivor、To survivor。
- 堆在计算机物理上存储是不连续的,但是逻辑上是连续的,它的大小可以调节(-Xmx,-Xms控制)。
- 方法结束后,堆对象不会马上的移除,仅仅在垃圾回收的时候才会移除。
- 如果堆中没有足够的内存完成对实例的分配,且堆的空间无法再扩展时,那么将会报出OOM异常。
4.1.3 设置堆内存大小
我们可以通过-Xms来设置最小堆内存,通过-Xmx设置最大堆内存。
以上是设置了:-Xms5m -Xmx20m
这里可以看出打印出来的Xmx值18m和设置的值20m之间是有差异的,total Memory和最大的内存之间也还是存在比较明显的差异,就是说JVM一般会尽量保持内存在一个尽可能底的层面,而非贪婪做法按照最大的内存来进行分配。
另外,当我们申请分配内存10m时,我们会发现free Memory和total Memory都上升了,可以看出JVM在内存分配时是动态分配的。
4.1.4堆的分类
JAVA将虚拟机堆分为三个部分:
- 新生代 (又分为伊甸园区,幸存者区s0和幸存者区s1)
- 老年代
- 永久代(JDK1.8后没有了,被本地内存的元空间取代了)
图例如下:
4.2 新生代和老年代
4.2.1 对象存储
- 新生代存放刚创建的实例对象,内存比较小,垃圾回收比较频繁。新生代又分为Eden区,survivor To区S0和survivor From区S1,其中S0区和S1区并不是固定的from及to的区域,由对象转移的方向决定的,假设对象从S1转移到S0,那么S1便是survivor From,S0是survivor To。
- 老年代主要存放一些生命周期比较长的对象,经过在新生代几次的回收依旧没有清除掉,那这部分实例便会转移到老年代。老年代的垃圾回收相对来讲没有那么频繁。
4.2.2 配置新生代和老年代的堆中占比
默认情况下-XX:NewRatio=2,表示新生代:老年代 = 1:2,新生代占整个堆空间的1/3
案例:假设我们将-XX:NewRatio修改为等于4,那么则表示新生代:老年代 = 1:4,那么新生代占整个堆空间的1/5
除了我们可以配置新生代和老年代的比例之外,我们还可以配置eden和S0和S1在新生代中的占比情况,默认情况下-XX:SurvivorRatio = 8,表示Eden:S0:S1=8:1:1,这表示Eden占整个新生代的8/10,而两个survivor区域分别占了1/10,另外,需要补充一点,由于JVM在运行时,每次都只会使用Eden区和一块survivor区进行服务,因此总是会有一个survivor区域是空闲着的,所以新生代的最高使用也只能达到9/10。
4.3 对象分配过程
- new对象时首先会将对象放在eden区,该区大小有内存限制。
- 当eden区的数据满了之后,程序还需要创建对象,会触发垃圾回收,将那些不再被引用的对象给销毁掉。
- 剩余没被回收掉的对象会被转移到S0区,而程序新创建的对象又会继续写入Eden区。
- 当再次发生垃圾回收时,如果S0中还存在未被销毁的对象,那么这部分剩余的对象会被转移到S1中。
- 之后每次经历垃圾回收,存在S0或者S1中未被销毁的对象总会相互转移过去。
- 当这种转移达到15次上限后,那么这部分对象将会被转移到老年区。当然这个阈值并不是固定15,可以通过调节参数 -XX:MaxTenuringThreshold=N来控制阈值。
- 当养老区的内存也不足时,会触发GC进行养老区的垃圾回收。
- 如果养老区进行了GC垃圾回收后还是没有办法保存新创建的对象,那么将会报OOM异常。
4.4 堆GC
Java中的堆是虚拟机中GC收集垃圾的主要区域。GC分为两种,一种是部分收集(Partial GC),一种是整堆收集(Full GC).
部分收集
- 新生代收集(Minor GC/Yong GC):只是新生代的垃圾回收。
- 老年代收集(Major GC/Old GC):只是老年代的垃圾回收。
- 混合收集(Mixed) :收集整个新生代和老年的垃圾。(G1 GC会混合回收, region区域回收)
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器
年轻代GC触发条件
- 当年轻代内存不足时,会触发Minor GC,这里的内存不足指的是Eden区的内存不足,Survivor区不会。
- Minor GC 会暂停其他用户的线程,等到垃圾回收结束,用户的线程才恢复。
老年代GC触发条件
- 老年代空间不足时,会尝试触发Minor Gc,如果空间还是不足,则会触发Major GC
- 如果Major GC结束后,空间还是不足,会报OOM异常。
- Major GC的速度比Minor GC慢10倍以上。
Full GC触发条件
- 程序调用System.gc(),会触发Full GC,但不会立即去执行。
- 老年代空间不足。
- 方法区空间不足。
- 通过Minor GC后仍能进入老年代的对象所占空间大于老年代剩余可用空间。
5.元空间
JDK1.8后为什么废除永久代,引入元空间
- 在之前的永久代中,它是堆的一部分,主要是在存储类的元数据、静态变量、常量等,这些数据的大小也不太容易控制和计算,开发人员对永久代进行调优会有很多的难度。永久代会对GC带来不必要的复杂度,回收效率偏低。
- 而用元空间替代永久代,这样的话可以很好的解决这个问题,因为元空间是放在本地内存上的,简而言之,只要你服务器内存还有,元空间基本就不会发生内存溢出等问题。
废除永久代的好处
- 由于类的元数据分配在本地内存上,这样就说元空间的最大分配内存就是服务器系统剩余可用内存,不会遇到永久代时存在的内存溢出问题。
- 将运行时常量池从永久代中分离出来,与类的元数据分开,提高了元数据的独立性。
- 将元数据从永久代剥离出来放到元空间,可以提升对元数据的管理,同时也提升GC效率。
元空间相关参数
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。如果设置了该参数,当Metaspace剩余空间不足,会抛出:java.lang.OutOfMemoryError: Metaspace space。
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
6.方法区
6.1方法区的理解
概念:
元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的,方法区是线程共享的。
特点:
- 方法区与堆一样是各个线程共享的内存区域。
- 方法区在JVM启动的时候就会被创建,并且它实际的物理内存空间和Java堆一样可以是不连续的。
- 方法区的大小跟堆一样,可以选择固定的大小或者动态变化。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机仍然会报OOM异常。
- 关闭虚拟机就会释放方法区区域。
6.2 方法区结构
类加载器将Class文件加载到内存以后,将类的信息存储到方法区中。
方法区中存储的内容:
- 类型信息(域信息、方法信息)
- 运行时常量池
类型信息
- 这个类型的完整有效名称(全名 = 包名.类名)
- 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
- 这个类型的修饰符( public, abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
域信息
域信息,即为类的属性,成员变量JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)
方法信息
- 方法名称方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
- 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
6.3 方法区设置
方法区的大小不必是固定的,可以根据应用的需要动态调整
- jdk7及之前通过-xx:Permsize来设置永久代初始分配空间。-XX:MaxPermsize来设定永久代最大可分配空间。64位的机器默认是82M。当JVM加载的类信息容量超过了这个值,会报OOM异常:PermGen space。
- jdk8及以后元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。但是元数据区的 -XX:MaxMetaspaceSize默认是-1即没有限制,不设置可以使用系统剩余所有内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
7.运行时常量池
字节码文件中,内部包含了常量池。
方法区中,内部包含了运行时常量池。
常量池:存放了编译期间产生的各种字面量和符号引用。
运行时常量池:是常量池表在运行时的一种表现形式。
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
#Java面试题#