JVM组成
各个组件的说明
子系统称号 | 描绘 | 线程同享 |
---|---|---|
类加载子系统 | 担任将.class文件加载到内存区域中。 | |
堆内存(Heap) | Java虚拟机中最大的一片空间,存储new出来的目标,以及经过其它办法构建出来的目标。也是GC首要收回的一片区域。 | 同享 |
办法区(Method Area) | 存储一些类的界说信息,类的元信息,类的办法界说,类中的常量。(元数据) | 同享 |
虚拟机栈(VM Stack) | 存储线程运转过程中的栈信息,程序的调用与履行过程,就比如反常时打印出来的栈信息便是其中之一。 | 线程私有 |
程序计数器 | 保存程序运转到哪里了。 | 线程私有 |
本地办法栈 | 与虚拟机栈相似,可是这个是针对于本地办法native办法的栈信息。而虚拟机栈是针对于Java办法的 | 线程私有 |
履行引擎
担任翻译字节码指令到操作系统认知的指令,调用操作系统的指令
1.程序计数器
用于记载咱们的程序履行到哪个方位的一个组件。
咱们的Java
程序其实便是字节码,履行需求字节码指令,咱们知道Java是依据线程的,而且能够多个线程一起履行,由于CPU核心有限,想要履行多线程任务的话,就需求和谐好各个线程的履行次序,便是经过时刻片来进行和谐办理,那么就会存在线程的中止和发动,假设没有记载程序运转方位的话,程序中止后在运转就找不到该从哪开端履行,而程序计数器就恰恰处理了这个问题。
假设调用的是
navite
办法,那么程序计数器为空。由于
native
办法直接是调用的是c
程序,处于两个不同的内存空间。因而获取不到相关信息。
特性
- 程序计数器是记载着当时线程所即将履行的字节码指令行号
- 每一个线程都具有自己的计数器
- 履行
Java
办法时,程序计数器是有值的 - 履行native本地办法时,计数器值为空
- 程序计数器占用内存十分少,不会呈现
OutOfMemoryError
2.虚拟机栈
栈介绍:它是一种数据结构,特性如下
- 接连紧密存储
- 先进后出
- 压栈与出栈
- 集装箱的摆放办法便是栈结构,入栈便是堆叠集装箱,出栈便是挪走集装箱
虚拟机栈的生命周期是和线程一致的
线程运转时则虚拟机栈存在,线程毁掉时,虚拟机栈也毁掉。
栈巨细、空间
- Java1.5后默许每个栈巨细为1mb,在此之前为256kb
- Java在发动参数中装备
-Xss数值[k|m|g]
能够装备栈巨细,例:-Xss10m
- 不主张手动设置巨细,1mb能够满足运用,假设设置的太大会导致OS内存压力增大,影响高并发环境下的功能
- 栈分配的内存决议了栈的深度,超出栈的深度则会抛出栈溢出的反常
-
StackOverflowError
仓库溢出 -
OutOfMemoryError
内存溢出
-
栈帧的组成
一个栈帧就对应一个办法调用,也便是一个办法,除了办法具体的流程外,栈帧还包含其它内容:
- 局部变量表
- 办法内部的局部变量信息
- 操作数栈
- 保存中间计算的暂时成果
- 动态链接
- 将符号引证转化为直接引证
- 回来地址
- 寄存调用办法的程序计数器值
局部变量表
局部变量表存储以下两块的内容:
- 存储办法参数
- 存储办法内的局部变量
何为局部变量?
public class Test{ // 成员变量 private int name; public void sayHi(){ // 局部变量,由于作用域仅仅是办法内 String text = "h"; } }
局部变量的特性
- 线程私有,不允许跨线程访问,随办法调用创立,办法退出毁掉
- 编译期间局部变量表长度(变量个数)已确定,局部变量元数据会存在在字节码文件中。
- 局部变量表是栈帧中最首要的存储空间,巨细影响栈的深度。
字节码文件内界说的局部变量表元数据
运用Jclasslib插件检查
sayHi()实例办法的源代码
public void sayHi(int printTotal) {
for (int i = 0; i < printTotal; i++) {
int a = 0;
String text = "你好";
System.out.println(text + a);
}
}
sayHi()实例办法对应的本地变量表
开端PC代表该变量是在第几行字节码创立的
main()静态办法的源代码
public static void main(String[] args) {
new LocalVarSample().sayHi(3);
}
main()办法对应的局部变量表
对比这两个办法(静态办法,实例办法),能够发现以下不同,也是最首要的区别
- 静态办法的局部变量表没有
this
,由于是static
办法,而sayHi()
办法对应的0号槽位(0号序号)便是this
变量
剩下的便是相同的地方
-
局部变量表的次序由变量的书写次序决议,假设是结构办法或许实例办法,则0号槽位(序号为0)寄存this,静态办法则是第一个书写的变量。
-
局部变量表的长度固定(多少槽位),在字节码创立时就固定下来,称之为静态局部变量表
-
一个局部变量至少占用一个
Slot
槽位,对应LocalVariableTable
的Index
列,也便是jclasslib
的序号列。 -
StartPC
代表字节码行号(并非代码行号),Length
代表从StartPC
开端的后几行能够运用这个变量,
高级特性
-
上边说到局部变量表中,每个变量至少占用一个
Slot
,那什么状况下会占用多个Slot呢?答案如下:-
占用槽位多少是依据数据类型巨细来决议的,32位以内的类型(int\float\char\引证类型..)占用1个槽位,大于32位的数据类型(long/double)就需求运用到2个槽位
-
如下图所示,money字段(序号2)为
double
类型,id字段(序号4)为long
类型,他俩的序号能够发现是从2-3,4-5,一个变量占用了两个槽位,由于他们超过了32位。 -
double money = 32.1; long id = 1111111111111111111L;
-
-
Slot复用,Slot在字节码中被称为是静态的本地变量表,而JVM运转时会把静态变量表动态的加载到内存中去,因而槽位或许会发生改变,例如
Slot
复用的概念。示例代码如下:-
public void sayHi(){ int a = 0; int b = 1; if(a < b){ int sum = a+b; a = sum; } int d = 4; System.out.println(a+d); }
-
在上边的示例代码中,咱们创立了4个变量,
a
、b
、sum
、d
-
字节码文件的静态变量表如下:
-
Slot槽位序号 变量名 0 this 1 a 2 b 3 sum 4 d
-
-
那程序真正履行过程中,由于
if
内的局部变量sum
作用域仅仅只有if
内部,当程序履行到if结尾时,就会把3号槽位的sum
给铲除去,由于已经不或许会被访问到了,在将d
加载到3号槽位,实现复用。好处如下:- 能够节约栈帧的空间,空间大了栈的深度就会更深。
- 进步功能:当槽位被复用时,能够避免创立新的槽位,然后削减内存的分配和收回,进步功能。
- 优化废物搜集:未复用的局部变量槽位会误导废物搜集器,阻止内存收回。经过槽位复用,废物搜集器能更精确地收回内存。
-
操作数栈
字节码指令履行过程中的暂时成果数据寄存的地方就叫做操作数栈
假设咱们有一个办法,对应的字节码指令如下,由于办法还未运转,所以操作数栈为空:
当履行第一条指令bipush 10
时,10将被压入栈顶。此刻栈如下
履行第二条指令istore_1
时,栈顶元素将被放入局部变量表,此刻局部变量表和栈的状况如下:
10将被弹出栈,并被放入局部变量表,操作数栈清空
履行第三条指令bipush 18
时,18将被压入栈顶。此刻局部变量表和栈的状况如下:
履行第四条指令istore_2
时,栈又会被清空,并将18放入局部变量表。
履行第五条指令iload_1
和第六条指令iload_2
后,局部变量表的10
和18
将被压入栈顶:
履行第七条指令iadd
时,操作数栈的栈顶元素与第二位元素将相加。
履行第八条指令istore_3
时,栈顶寄存的相加后的元素28
将先被弹出栈顶,再被存入本地变量表。
履行第九条指令iload_3
时,28
将被压入栈顶。
履行最终一条指令ireturn
时将回来栈顶元素28
。
此刻办法完毕,局部变量表将被清空,操作数栈将被清空。办法回来成果。
动态链接
什么是动态链接?将字节码中的符号引证转化成内存的直接引证。
字节码文件中存储的引证信息都是cp_info #27
这种东西,能够理解为便是个字符串信息,也叫字面量,实际上便是一个引证的信息,假设字节码new
一个目标,在字节码中存储的是new #14
这种字符串,那在JVM运转时,想真正的找到这个目标的信息,就要去JVM办法区里去找,而这个#14
就比如是目标信息存储的方位,有了#14
就能找到LocalVarSample
这个目标。
在内存中履行的做个转化过程,就叫动态链接。
为什么要这么干呢?
- 由于栈帧的空间是有限的,不能直接存储目标的信息,假设只存储目标的指针,就能大大的节约栈帧的空间。而且方便
JVM
办理。
动态链接示意图
回来地址
它首要用来指示办法履行完毕后程序的履行流应该跳转到的方位。简略来说,便是告诉程序:我这个办法履行完了,你应该去哪里继续履行。
举个比如,假设你正在履行一个名为A的办法,然后在A中又调用了一个名为B的办法。那么在调用B的时分,虚拟机会在栈帧中保存一个回来地址,这个地址指向的是办法A中调用办法B那条指令的下一条指令。当办法B履行完毕后,程序就会跳转到这个回来地址,也便是回到办法A中继续履行。
爆栈
虚拟机栈,是个栈结构,既然是一个存储的结构那么必然是有巨细限制,不或许无限制的一向入栈,这样内存也扛不住,那么爆栈的意思其实也很好理解,便是办法运转的层级过深,就比如递归,没有中止条件,就会导致一向入栈,一向入栈,直到把栈给顶爆,就抛出,StackOverflowError
反常。
public static void main(String[] args) {
// 调用爆栈(递归)办法
System.out.print(method1());
}
public int method1(){
// 无限制的递归
return method1();
}
成果
Exception in thread "main" java.lang.StackOverflowError
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
at cn.yufire.aqs.ZhanZhen.method1(ZhanZhen.java:29)
... 省掉n行
3.本地办法栈
本地办法栈是专供于本地办法(navite
修饰)运用的,当JVM经过JNI调用本地办法时,本地办法栈便是首要记载JVM调用本地办法时的一些状态信息。
4.堆(Heap)
堆是JVM中最核心的内存区域,寄存运转时实例化的目标实例。堆在JVM创立的时分就被创立,空间也被分配。堆是线程同享的一片大区域。
-
堆内存在物理上是涣散的,在逻辑上是接连的,也便是说堆内存的数据能够涣散在不同的内存颗粒上,可是JVM在运用时会经过指针将数据接连起来,在咱们看来JVM堆便是接连的一片空间。
-
堆中包含线程私有的缓冲区(
TLAB
),能够有用的进步JVM的并发功率
TLAB(Thread-Local Allocation Buffer)是线程本地分配缓冲区的缩写。在Java中,为了进步内存分配的功率,JVM一般会为每个线程在堆中分配一个私有的缓冲区,这便是TLAB。
TLAB的首要作用是削减线程之间的竞赛。在多线程环境下,假设每个线程都直接在堆上分配内存,那么这些线程或许会竞赛同一块内存区域,然后导致功能下降。经过运用TLAB,每个线程都能够在自己的缓冲区中分配内存,然后避免了这种竞赛。
TLAB只用于分配小目标。当一个线程需求分配一个大目标时,它会直接在堆上进行分配。当TLAB用完时,线程会请求一个新的TLAB。
总的来说,TLAB的引入是为了进步内存分配的功率,特别是在多线程环境下。
引证类型与堆的联系
- 引证类型的本质便是一个指针,指向堆内存的行列实例地址。
- 堆是废物收回(GC)的要点区域,办法完毕后并不会立即进行废物收回,而是等JVM判别需求废物收回时才进行废物收回。
堆结构
- 新生代,首要寄存刚创立的目标,这些目标是不稳定的,或许会被频频GC收回。
- 老时代,寄存相对稳定的目标(GC屡次未被收回),不会进行频频的GC行为。
- 元空间,内存中永久保存区域,用于寄存类的描绘信息,几乎不会GC。
堆巨细
参数相关
-
-Xms数量[k|m|g]
设置堆空间(年轻代+老时代)的初始化内存巨细,例如-Xms2g
-
-Xmx数量[k|m|g]
设置堆空间(年轻代+老时代)的最大内存巨细,例如-Xmx2g
-
-XX:+PrintGCDetails
设置该参数能够检查GC相关的数据,在程序退出时打印
Heap
PSYoungGen total 1378816K, used 161309K [0x0000000716580000, 0x000000076e180000, 0x00000007c0000000)
eden space 1364480K, 11% used [0x0000000716580000,0x0000000720307790,0x0000000769a00000)
from space 14336K, 0% used [0x000000076d380000,0x000000076d380000,0x000000076e180000)
to space 33792K, 0% used [0x0000000769f80000,0x0000000769f80000,0x000000076c080000)
ParOldGen total 459264K, used 73890K [0x00000005c3000000, 0x00000005df080000, 0x0000000716580000)
object space 459264K, 16% used [0x00000005c3000000,0x00000005c7828ba0,0x00000005df080000)
Metaspace used 92836K, capacity 98120K, committed 98752K, reserved 1134592K
class space used 12143K, capacity 13082K, committed 13184K, reserved 1048576K
设置主张:主张整个堆巨细设置为
FullGC
后存活目标的3-4倍
默许值
- 堆默许巨细为操作系统内存的
64
分之1
- 堆最大内存为操作系统的
4
分之1
- 新生代与老时代份额
- 新生代占用总堆的
3
分之1
- 老时代占用总堆的
3
分之2
- 新生代占用总堆的
新生代
新生代分为三个小区域:Eden(伊甸园)、Survivor-0(From)、Survivor-1(To)
- 内存分配份额为:
8:1:1
- 假设新生代有
1G
内存,那分配成果便是:800M
、100M
、100M
- 绝大多数刚创立的目标都会放在
Eden
区,因而需求有足够的空间寄存这些目标
刚初始化的新生代,此刻还没有数据时是长这样的
假设新创立一个目标:User user = new User()
此刻User()
目标会被放入Eden
区。
假设Eden
区满了(默许)的时分,无法分配新目标的时分,就会触发一次Minor GC
,也叫Young GC
对新生代进行废物收回。
- 依据可达性算法扫描过一切目标后,会将目标分为两拨:无引证目标,持有引证目标
- 持有引证目标将会被经过
仿制拷贝
算法到S0
区,剩下的一切目标将被废物收回掉。 - 并将目标的头特点
age + 1
当Eden
区再次满了之后,再次触发Minor GC
,再次经过可达性算法剖析目标是否持有引证,再次将目标进行拷贝交互和铲除。
假设咱们的User
目标仍然持有引证,User目标将被仿制并拷贝到S1
区age + 1
,再将无引证目标进行废物收回
假设Eden
区再次满了,又会触发Minor GC
,此刻咱们的User
目标仍然持有引证,那么它将被移动到S0
区age + 1
假设一向Minor GC
,直到User
的age
大于15时,User
目标会被JVM
认定为是稳定的目标,会被放入Old Gen
老时代里。
问题一
为什么
User
会从S1
移动到S0
呢?
- JVM为了方便相关内存数据,处理内存碎片问题,所以会将
S0
区和S1
区的数据进行互换,每次Minor GC
时Eden
区的目标会被放入空的Survivor
区,并将该区的一切目标移动到另一个Survivor
区- 出目标的
Survivor
区被称为From
区,移动到的Survivor
区被称为To
区
问题二
为什么废物收回要分代(新生代、老时代)处理
- 为了履行功率的考量,由于大多数目标的存活时刻或许极低,或许办法履行完目标就没有引证了,假设触发大局
GC
(Full GC
),则会牵连无关紧要的目标,影响整体功率,添加程序响应时刻。
新生代废物收回的特殊状况
其实特殊状况简略概括便是,新创立的目标没有足够的内存进行分配了该怎样办?
Eden区内存不足无法分配,目标被移动到S0区
- 履行
Minor GC
,并将目标移动到S0
区,可是S0
区满了,目标放不进去。 - 测验直接将目标放入
Old Gen
老时代里。 - 假设老时代满了放不进去,则会触发
Full GC
后再次测验寄存- 假设还放不进去,则抛出
OOM
过错
- 假设还放不进去,则抛出
仿制交流算法
5.办法区
寄存类的信息,办法信息等字节码中的静态信息(元数据)。
- 办法区是线程同享的区域,是物理上涣散,逻辑上接连的一片区域
保存的内容
- 类型信息:这包含类的称号、父类、接口、访问修饰符等信息。
- 字段信息:类中的字段(包含静态字段和非静态字段)的称号、类型、访问修饰符等信息。
- 办法信息:类中的办法(包含静态办法和非静态办法)的称号、回来类型、参数类型、访问修饰符等信息。
- 常量池:包含字面量(如文本字符串)和符号引证(如类和接口的全名、字段的称号和描绘符、办法的称号和描绘符)。
- 静态变量:类的静态变量。
- 即时编译后的代码:假设启用了即时编译(JIT),办法区还会保存编译后的本地机器代码。
永久代与元空间
永久代的数据是寄存在JVM内存之内的,和JVM同享内存空间,而元空间的数据是直接寄存在操作系统内存上的,与JVM内存互不影响,不占用JVM内存。
- 永久代占用JVM内存,容易导致内存溢出
- 元空间与物理机内存保持一致,最大可用内存为物理机剩下可用内存。
元空间巨细
默许为(12mb~21mb),依据操作系统的不同在此区间内动态调整。假设慢了则会触发Full GC
,并进步元空间巨细或许打破21mb
能够经过-XX:MetaSpaceSize=xxx[M|G]
设置元空间最小值、运用-XX:MaxMetaSpaceSize=xxx[M|G]
设置最大值
- 主张设置较大的元空间巨细,削减因元空间内存不足导致的
Full GC
- 假设元空间内存超过了
MaxMetaSpaceSize
则会抛出OutOfMemoryError:MetaSpace
过错
不同版本JVM办法区的改变
办法区是个概念,JVM
规范要求有必要要有这个东西,至于怎样实现便是JVM厂商做的工作。例如HotSpot
虚拟机
- 1.7前,办法区叫做永久代
Permanent Generation
- 1.7开端逐步扔掉永久代
- 1.8彻底扔掉永久代并实现了元空间
Meta Space
比如公共场所的消防设施,消防队要求有必要有,可是怎样去装置是公共场所的工作。
- 某棋牌室放了16个灭火器 + 3个消防栓作为消防设施
- 某酒店采用了热感喷头 + 36个消防栓作为消防设施
运转时常量区
寄存来自字节码中常量池的数据,或许动态发生的新常量。
运转时常量区位与元空间内,属于内部的一片空间。
办法区历史改变
静态变量是存储在哪的?public static User user = new User();
不同版本JVM是不一样的,如下图所示
JDK版本 | 改变 |
---|---|
<=1.6 | 此刻有永久代的概念,静态变量寄存在永久代内 |
1.7 | 有永久代,可是逐步移除永久代,字符串常量池,静态变量从永久代移除,改为寄存在堆中 |
>= 1.8 | 已移除永久代,类型信息、字段、办法、常量保存在元空间内,字符串常量池,静态变量仍在堆中 |
静态变量是运用new
关键字创立的,如new User()
,那一定是存储在堆内存中的。
假设静态变量是根本类型如static int i = 1;
,那它会寄存在元空间内。