- 引言
- 一、JVM组成
- 1. JVM在JDK中的位置
- 2. JVM的组成
- 三、Runtime Data Area
- 1. 栈(线程私有)
- 1. 栈的特点
- 2. 基本内容
- 3. 栈的异常
- 4. 设置大小
- 5. 运行原理
- 6. 栈的内部结构
- 7. 栈的代码追踪
- 2. 堆(线程共享,GC最频繁)
- 1. 堆的细分
- 2. 基本内容
- 3. 设置大小
- 3. PC寄存器(线程私有)
- 4. 方法区:(Non-heap,所谓的永久代,线程共享)
- 1. 栈、堆、方法区交互关系
- 2. 基本介绍
- 3. JDK7和JDK8中的变化
- 5. 本地方法栈
- 三、JVM运行原理
- 1. 执行流程
- 2. 栈的管理
- 3. 堆的管理
- 1. 堆的基本介绍
- 2. 对象分配过程
- 3. 堆的异常
- 4. MinorGC、MajorGC和FullGC
- 5. 年轻代GC(MinorGC)触发机制
- 6. 老年代GC(MajorGC/FullGC)触发机制
- 7. FullGC触发机制
- 8. 堆空间参数设置
JVM
是 Java
虚拟机,是一种规范,它遵循着冯诺依曼体系结构的设计原理。在冯诺依曼体系结构中指出,计算机处理的数据和指令都是二进制数,采用存储程序方式,不加区分的存储在同一个存储器里,并且顺序执行。
指令由操作码和地址码组成,操作码决定了操作类型和所操作数的数字类型,地址码则指出地址码和操作数。不同的操作系统指令集以及数据结构都有着差异,而 JVM
通过在操作系统上建立虚拟机,自己定义出来的一套统一的数据结构和操作指令,把同一套语言翻译给各大主流的操作系统,实现了跨平台运行,可以说 JVM
是 Java
的核心,是 Java
可以一次编译到处运行的本质所在。
JVM
有多种实现,例如 Oracle
的JVM
,HP
的 JVM
和 IBM
的 JVM
等,本文使用广泛的HotSpot JVM
。
众所周知,JDK
是 Java
开发的必备工具箱,JDK
其中有一部分是 JRE
,JRE
是 JAVA
运行环境,JVM
则是 JRE
最核心的部分,下面是一张关于JDK Standard Edtion
的组成图。
从最底层的位置可以看出JVM
有多重要,而实际项目中JAVA应用的性能优化,OOM
等异常处理最终都得从JVM
这儿来解决。在命令行,我们可以通过java -version
命令可以查看关于当前机器JVM
的信息,下面是在本人Win7
系统上执行命令的截图。
JVM
由4大部分组成:ClassLoader
,Runtime Data Area
,Execution Engine
,Native Interface
。JVM
组成结构图如下:
-
ClassLoader
是类加载器,是负责加载class
文件,class
文件在文件开头有特定的文件标识,ClassLoader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
决定 -
Native Interface
是负责调用本地接口。它的作用是调用不同语言的接口给JAVA
程序用,它会在Native Method Stack
中记录对应的本地方法,然后调用该方法时就通过Execution Engine
加载对应的本地lib
。现在很少使用,因为即使要和底层C或C++接口进行交互,可以用WebService进行,完全没必要用JNI
-
Execution Engine
是执行引擎,Class
文件被加载后,会把指令和数据信息放入内存中,Execution Engine
则负责把这些命令解释给操作系统。 -
Runtime Data Area
是运行时数据区,用于存放数据,分为五部分:Stack(栈),Heap(堆),Method Area(方法区),PC Register(寄存器),Native Method Stack。几乎所有的关于Java
内存方面的问题,都是集中在这块。下图是关于运行时数据区的描述:
上半部分的框表示线程私有、下半部分的框表示线程共享
三、Runtime Data Area内存中的栈与堆:
栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据的存储问题,即数据怎么放,放在哪儿。
1. 栈(线程私有) 1. 栈的特点-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
-
JVM直接对Java栈的操作只有两个:
-
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出战操作
-
对于栈来说不存在垃圾回收问题:GC、OOM
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wpNxgaAp-1640077389752)
-
Stack
表示Java
栈内存,在Java
中,每个线程都拥有自己的栈,即栈为线程私有,其生命周期与线程相同 -
栈里面存储的是栈帧
StackFrame
,结构见下图 -
Stack
描述的是Java
方法执行的内存模型:每个方法被执行的时候,都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程 -
局部变量表存放了编译期可知的各种基本数据类型(
boolean
、byte
、char
、short
、int
、float
、long
、double
)、对象引用(reference
类型) -
局部变量表所需的空间在编译期间完成分配。当进入一个方法时,其需要在帧中分配多大的局部变量空间是确定的,方法运行期间不会改变局部变量表的大小。局部变量用来存储一个类的方法中所用到的局部变量。
Java
虚拟机规范允许Java
栈的大小是动态的或者是固定不变的- 如果采用固定大小的
Java
虚拟机栈,那每一个线程的Java
虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java
虚拟机栈允许的最大容量,Java
虚拟机将会抛出一个StackOverflowError
异常 - 如果
Java
虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机核那Java
虚拟机将会抛出一个OutofMemoryError
异常
可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
5. 运行原理- 不同线程中所包含的栈帧是不允许存在相互可用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java
方法有两种返回函数的方式,一种是正常的函数返回,使用return
指令,另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出
栈里面存储的是栈帧StackFrame
,每个栈帧中存储着:
- 局部变量表
Local variables
- 操作数栈
operand stack
- 动态链接
DynamicLinking
或指向运行时常量池的方法引用 - 方法返回地址
ReturnAddress
或方法正常退出或者异常退出的定义 - 一些附加信息
-
在Java7及以前,堆内存逻辑上分为三个部分:新生区+养老区+永久代
新生区:
Young Generation Space
Young/New
新生区又被划分为Eden区和Survivor区
养老区:
Tenure Generation Space
Old/Tenure
永久代:
Permanent Space
Perm
-
在Java8及之后,堆内存逻辑上分为三个部分:新生区+养老区+元空间
新生区:
Young Generation Space
Young/New
新生区又被划分为Eden区和Survivor区
养老区:
Tenure Generation Space
Old/Tenure
元空间:
Meta Space
Meta
无论在什么地方看到,下面的命名是等价的:
新生区=新生代=年轻代
养老区=老年区=老年代
永久区=永久代
-
Java
堆是Java
虚拟机管理内存中最大的一块,是所有线程共享的内存区域,随虚拟机的启动而创建。该区域唯一目的是存放对象实例,几乎所有对象的实例都在堆里面分配。Java
堆是垃圾收集器管理的主要区域,被称GC堆
-
Java
虚拟机规范规定,Java
堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。如同磁盘空间一样,既可以实现成固定大小,也可以是扩展的,当前主流虚拟机都是按照扩展来实现的,通过 -Xms 和 -Xmx 控制 -
Java
虚拟机规范中对该区域规定了OutOfMemoryError
异常:如果堆中没有内存完成实例分配,并且堆无法再扩展则抛出OutOfMemoryError
异常
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行
新生区介绍
-
新生区是类的诞生、成长、消亡的区域,一个类在这里产生、应用,最后被垃圾回收器收集,结束生命
-
新生区又分为两部分:伊甸区(
Eden space
)和幸存者区(Survivor space
),所有的类都是在伊甸区被new
出来的 -
幸存区有两个:0区(
Survivor 0 space
)和1区(Survivor 1 space
) -
Eden
和Survivor0
、Survivor1
的比例是:8:1:1 -
新生区的大小站堆内存的三分之一
解释:
当Eden区的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用的对象进行销毁。
然后将伊甸园中的剩余对象移动到幸存0区(也叫From区),若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区(也叫To区)。那如果1区也满了呢?
再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老区的内存清理, 若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”
3. 设置大小-
堆的大小在
JVM
启动时就已经设定好了,可以通过选项-Xms
和-Xmx
来进行设置-
-Xms
用于表示堆区的起始内存,等价-XX:InitialHeapSize
-
-Xmx
则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
-
-
一旦堆区中的内存大小超过
-Xmx
所指定的最大内存时,将会抛出OutOfMemoryError
异常 -
通常会将
-Xms
和-Xmx
两个参数配置相同的值,其目的是为了能够在Java
垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能 -
默认情况下,初始内存大小:物理电脑内存大小
1/64
最大内存大小:物理电脑内存大小1/4
PC Register
是程序计数寄存器,每个JAVA
线程都有一个单独的PC Register
,它表示一个指针,由Execution Engine
读取下一条指令。如果该线程正在执行Java
方法,则PC Register
存储的是正在被执行的指令的地址,如果是本地方法,PC Register
的值没有定义。PC寄存器
非常小,只占用一个字宽,可以持有一个returnAdress
或者特定平台的一个指针。
关于PC寄存器
,有两个问题,其实就是一个问题:
- 使用
PC寄存器
存储字节码指令地址有什么用呢?为什么使用PC寄存器
记录当前线程的执行地址呢?
答:因为CPU
需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM
的字节码解释器就需要通过改变PC寄存器
的值来明确下一条应该执行什么样的字节码指令。
PC寄存器
为什么会被设定为线程私有?
答:我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU
会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线都分配一个PC寄存器
,这样一来各个线程之间便可以进行独立计算,从而不会出现相干扰的情况。
由于CPU
时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
4. 方法区:(Non-heap,所谓的永久代,线程共享) 1. 栈、堆、方法区交互关系-
方法区看作是一块独立于
Java
堆的内存空间 -
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机对这个区域的限制非常宽松,处理和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集在这个区域较少出现
-
Java虚拟机规范中对该区域规定了
OutOfMemoryError
异常: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError
异常 -
运行时常量池是方法区的一部分
-
方法区保存装载的类信息:类型的常量池、字段、方法、方法字节码、通常和永久区(Perm)关联在一起
-
运行时常量池
- Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面常量和符号引用,这部分内容在类加载后存放到方法区的常量池中
- Java虚拟机规范中对该区域规定了OutOfMemoryError异常:当常量池无法申请到内存时抛出OutOfMemoryError异常
-
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存
-
永久代、元空间二者并不只是名字变了,内部结构做了方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。
-
设置方法区内存大小,
Jdk7
及以前: -
-XX:PermSize
来设置永久代初始分配空间,默认值是20.75M
-
-XX:MaxPermSize
来设定永久代最大可分配空间。32
位机器默认是64M
,64
位机器模式是82M
-
Jdk8
及以后,元数据区大小设置:-
-XX:MetaspaceSize
-
-XX:MaxMetaspaceSize
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,- XX:MaxMetaspaceSize 的值是-1,即没有限制。
-
Native Method Stack是供本地方法(非java)使用的栈,每个线程持有一个Native Method Stack。
三、JVM运行原理 1. 执行流程Java 程序被 javac 工具编译为 .class 字节码文件之后,我们执行 java 命令,该 class 文件便被 JVM 的 Class Loader 加载,可以看出JVM的启动是通过JAVA Path下的 java.exe 进行的。
JVM的初始化、运行到结束大概包括这么几步:
- 调用操作系统API判断系统的CPU架构,根据对应CPU类型寻找位于JRE目录下的/lib/jvm.cfg文件
- 然后通过该配置文件找到对应的 jvm.dll 文件(如果我们参数中有-server或者-client, 则加载对应参数所指定的 jvm.dll,启动指定类型的JVM),初始化jvm.dll并且挂接到 JNIENV 结构的实例上,之后就可以通过JNIENV实例装载并且处理class文件了
- class文件是字节码文件,它按照JVM的规范,定义了变量,方法等的详细信息,JVM管理并且分配对应的内存来执行程序,同时管理垃圾回收,直到程序结束,一种情况是JVM的所有非守护线程停止,一种情况是程序调用 System.exit(),JVM的生命周期也结束。
JVM允许栈的大小是固定的或者是动态变化的。在Oracle的关于参数设置的官方文档中有关于Stack的设置,是通过 -Xss 来设置其大小。关于Stack的默认大小对于不同机器有不同的大小,并且不同厂商或者版本号的 jvm 的实现其大小也不同,如下表是HotSpot的默认大小:
TablesAreWindows IA3264 KBLinux IA32128 KBWindows x86_64128 KBLinux x86_64256 KBWindows IA64320 KBLinux IA641024 KBSolaris Sparc512 KB一般通过减少常量,参数的个数来减少栈的增长。在程序设计时,我们把一些常量定义到一个对象中,然后来引用它们可以体现这一点,另外,少用递归调用也可以减少栈的占用。 栈是不需要垃圾回收的(线程私有),尽管说垃圾回收是Java内存管理的一个很热的话题,栈中的对象如果用垃圾回收的观点来看,它永远是live状态,是可以reachable的,所以也不需要回收,它占有的空间随着Thread的结束而释放。
关于栈一般会发生以下两种异常:
- 当线程中的计算所需要的栈超过所允许大小时,会抛出StackOverflowError
- 当Java栈试图扩展时,没有足够的存储器来实现扩展,JVM会报OutOfMemoryError
堆的管理要比栈管理复杂的多
上图是 Heap
堆和 PermanentSapce
的组合图。其中 Eden
区里面存着是新生的对象,S0
和 S1
中存放着是每次垃圾回收后存活下来的对象 。所以每次垃圾回收后,Eden
区会被清空。存活下来的对象先是放到 S0
,当 S0
满了之后移动到 S1
。当S1
满了之后移动到 Old Space
。
Survivor
的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden
复制过来对象,和从前一个Survivor
复制过来的对象,而复制到老年区的只有从第一个Survivor
复制过来的对象。而且,Survivor
区总有一个是空的。同时,根据程序需要,Survivor
区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到老年代的可能。
Old Space
中则存放生命周期比较长的对象,而且有些比较大的新生对象也放在Old Space中。
新生代与老年代:
- 通过
-Xmn
来指定Young Generation
的大小,一些老版本也用-XX:NewSize
指定,即上图中的Eden
加S0
和S1
的总大小 - 通过
-XX:NewRatio
来指定Eden
区的大小,在Xms
和Xmx
相等的情况下,该参数不需要设置。通过-XX:SurvivorRatio
来设置Eden
和 一个Survivor
区的比值 - 配置新生代与老年代在堆结构的占比:
- 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
- 在
HotSpot
中,Eden
空间和另外两个Survivor
的空间缺省默认所占的比列是8:1:1
,可以通过手动配置选项-XX:SurvivorRatio
调整整个空间比列
new
的对象先放Eden
区,此区有大小限制- 当
Eden
的空间填满时,程序又需要创建对象,JVM
的垃圾回收器将对Eden
区进行垃圾回收MinorGC
,将Eden
区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到Eden
区 - 然后将
Eden
中的剩余对象移动到S0
区 - 如果再次触发垃圾回收,此时上次幸存下来的放到
S0
区的,如果没有回收,就会放到S1
区 - 如果再次经历垃圾回收,此时会重新放回
S0
区,接着再去S1
区。 - 啥时候能去养老区呢?可以设置次数,默认是
15
次。可以设置参数:-XX:MaxTenuringThreshold=10
进行设置 - 在养老区,相对悠闲。当养老区内存不足时,再次触发
GC:Major GC
,进行养老区的内存清理 - 若养老区执行了
Major GC
,GC
之后发现依然无法进行对象的保存,就会产生OOM
异常
堆异常分为两种,
- 一种是
Out of Memory(OOM)
- 一种是
Memory Leak(ML)
。Memory Leak
最终将导致OOM
实际应用中表现为:从Console
看,内存监控曲线一直在顶部,程序响应慢;从线程看,大部分的线程在进行GC
,占用比较多的CPU
,最终程序异常终止,报OOM
。OOM
发生的时间不定,有短的一个小时,有长的10天一个月的。关于异常的处理,确定OOM/ML
异常后,一定要注意保护现场,可以dump heap
,如果没有现场则开启GCFlag
收集垃圾回收日志,然后进行分析,确定问题所在。如果问题不是ML
的话,一般通过增加Heap
,增加物理内存来解决问题,是的话,就修改程序逻辑。
JVM
在进行GC
时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是指新生代。 针对HotSpotVM
的实现,它里面的GC
按照回收区域又分为两大种类型:一种是部分收集PartialGC
,一种是整堆收集FullGC
-
部分收集:不是完整收集整个
Java
堆的垃圾收集。其中又分为:-
新生代收集(
MinorGC/YoungGC
):只是新生代(Eden\S0,S1
)的垃圾收集 -
老年代收集(
MajorGC/OldGC
):只是老年代的垃圾收集,目前,只有CMSGC
会有单独收集老年代行为注意,很多时候Maior GC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收。
-
混合收集(
MixedGC
):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1GC
会有这种行为
-
-
整堆收集(
FullGC
):收集整个Java
堆和方法区的垃圾收集
- 当年轻代空间不足时,就会触发
MinorGC
,这里的年轻代满指的Eden
代满,Survivor
满不会引发GC
。每次MinorGC
会清理年轻代的内存 - 因为
Java
对象大多都具备朝生夕灭的特性,所以MinorGC
非常频繁,一般回收速度也比较快,这一定义既清晰又易于理解 MinorGC
会引发STW
,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
-
指发生在老年代的
GC
,对象从老年代消失时,我们说MajorGC
或Fu11 GC
发生了 -
出现了
MajorGC
,经常会伴随至少一次的MinorGC
,但非绝对的,在Parallel Scavenge
收集器的收集策略里就有直接进行MajorGC
的策略选择过程也就是在老年代空间不足时,会先尝试触发MinorGC。如果之后空间 还不足, 则触发MajorGC
-
MajorGC
的速度一般会比MinorGC
慢10
倍以上,STW
的时间更长 -
如果
MajorGC
后,内存还不足,就报OOM
了
触发FullGC
执行的情况有如下五种:
-
调用
System.gc()
时,系统建议执行FullGC
,但是不必然执行 -
老年代空间不足
-
方法区空间不足
-
通过
MinorGC
后进入老年代的平均大小大于老年代的可用内存 -
由
Eden
区、survivor space0(From Space)
区向survivor space1( To Space)
区复制时,对象大小大于ToSpace
可用内存,则把该对象转存到老年代老年代的可用内存小于该对象大小说明:Full gc是开发或调优中尽量要避免的
-
-XX:+PrintFlagsInitial
:查看所有的参数的默认初始值 -
-XX:+PrintFlagsFinal
:查看所有的参数的最终值(可能会存在修改,不再是初始值) I -
-Xms
:初始堆空间内存(默认为物理内存的1/64) -
-Xmx
:最大堆空间内存(默认为物理内存的1/4) -
-Xmn
:设置新生代的大小。(初始值及最大值) -
-XX:NewRatio
:配置新生代与老年代在堆结构的占比 -
xx:SurvivorRatio
:设置新生代中Eden
和S0/S1
空间的比例 -
-XX:MaxTenuringThreshold
:设置新生代垃圾的最大年龄 -
XX:+PrintGCDetails
:输出详细的GC
处理日志打印gc简要信息:-xx:+PrintGC -verbose:gc
-
XX:HandlePromotionFailure
:是否设置空间分配担保