内存是非常重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请,分配,管理的策略,保证了JVM高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
方法区和堆区是随着JVM启动而创建,随着JVM关闭而回收,生命周期与JVM一致,一个Java进程内只有一个堆区,一个方法区,线程之间是共享使用这些区域的(Metaspace和CodeCache也是线程共享的)。本地方法栈,程序计数器和虚拟机栈都是线程私有的,每个线程都有自己的一份。一般来讲95%的垃圾回收都发生在堆区,5%的垃圾回收发生在方法区。JDK8以后的Metaspace使用直接内存,直接内存一般比较大,但不代表不会发生垃圾回收。
线程是一个程序里的运行单元。JVM允许一个应用里面有多个线程并行执行。
在HotSpot JVM中,每个线程都与操作系统的本地线程映射。当一个Java线程准备好执行以后,此时一个操作系统本地线程也会创建。Java线程终止后,操作系统本地线程也会回收。
操作系统负责把所有的线程调度到某个可用的CPU上,一旦本地线程初始化成功它就会调用Java线程中的Run
方法。Run()
方法如果有抛出未捕获的异常,JVM的线程就终止了,操作系统此时会判断是否应该结束JVM进程。当此线程是最后一个非守护线程时,JVM就被停止了。
JVM的PC寄存器是对物理PC寄存器的抽象模拟,是软件层面的概念,主要存储指令相关的现场信息。CPU只有把数据加载到寄存器才能使用。PC寄存器是用来存储指向下一条指令的地址,也可以理解为即将要执行指令的代码。由执行引擎读取下一条指令。
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}
编译成字节码为:
0 iconst_1
1 istore_1
2 iconst_2
3 istore_2
4 iload_1
5 iload_2
6 iadd
7 istore_3
8 return
前面的那一串数字就是指令行号,就是PC寄存器中存储的数值。
为什么使用PC寄存器记录地址?(PC寄存器有什么作用)
因为CPU是不断的切换线程执行的,切换回来后CPU要知道接下来从哪一条指令继续执行。JVM使用PC寄存器来明确接下来应该执行什么指令。
PC寄存器为什么设置为线程私有
CPU能调度的最小单元是线程,为了能够准确的记录各个线程正在执行的指令,最好的办法就是每个线程都分配一个PC寄存器。
CPU时间片即CPU分配给各个线程的执行时间,每个线程执行时被分配一个时间段,称为CPU时间片。
从宏观上看:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
从微观上看:由于只有一个CPU,一次只能处理一个应用程序要求的一部分,如何处理比较公平?引入时间片,每个程序轮流执行。
栈是运行时的单位,堆事存储时的单位。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的方法调用,是线程私有的。生命周期与线程一致。它保存方法的局部变量(8中基本变量类型,引用类型的地址),部分结果,并参与方法的调用和返回。
Java虚拟机规范规定了虚拟机栈可以是固定不变的,也可以是动态的。虚拟机栈大小设置参数 -Xss
。
随便写一些代码如下:
public static void main(String[] args) {
int a = 1;
String str = "我是main函数";
test1(678);
}
public static void test1(int a){
int b = 2;
int c = a + b;
Date now = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(simpleDateFormat.format(now));
short paramShort = 126;
byte paramByte = 1;
int re =test2(paramShort,1500,123456789L,false,paramByte,2.34f,
1.174938493,'x',"我是test1,我调用test2");
System.out.println(re);
}
public static int test2(short paramShor,int paramInt,long paramLong,boolean paramBoolean,
byte paramByte,float paramFloat,double paramDouble,char paramChar,
String paramStr){
short localShot = 0;
int localInt = 1;
long localLong = 2147483647L;
boolean localBoolean = true;
byte localByte = 8;
float localFloat = 1.23f;
double localDouble = 3.14;
char localChar = 'A';
Integer integer = Integer.valueOf("1000");
return localInt;
}
使用javap -v
反编译后如下(省略了部分不关注的信息):
//... 省略类信息 常量池 其他方法信息等
public static int test2(short, int, long, boolean, byte, float, double, char, java.lang.String);
descriptor: (SIJZBFDCLjava/lang/String;)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=22, args_size=9
0: iconst_0
1: istore 11
3: iconst_1
4: istore 12
6: ldc2_w #20 // long 2147483647l
9: lstore 13
11: iconst_1
12: istore 15
14: bipush 8
16: istore 16
18: ldc #22 // float 1.23f
20: fstore 17
22: ldc2_w #23 // double 3.14d
25: dstore 18
27: bipush 65
29: istore 20
31: ldc #25 // String 1000
33: invokestatic #26 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
36: astore 21
38: iload 12
40: ireturn
LineNumberTable:
line 29: 0
line 30: 3
line 31: 6
line 32: 11
line 33: 14
line 34: 18
line 35: 22
line 36: 27
line 37: 31
line 38: 38
LocalVariableTable:
Start Length Slot Name Signature
0 41 0 paramShort S
0 41 1 paramInt I
0 41 2 paramLong J
0 41 4 paramBoolean Z
0 41 5 paramByte B
0 41 6 paramFloat F
0 41 7 paramDouble D
0 41 9 paramChar C
0 41 10 paramStr Ljava/lang/String;
3 38 11 localShort S
6 35 12 localInt I
11 30 13 localLong J
14 27 15 localBoolean Z
18 23 16 localByte B
22 19 17 localFloat F
27 14 18 localDouble D
31 10 20 localChar C
38 3 21 integer Ljava/lang/Integer;
//...省略代码
}
SourceFile: "Main.java"
我们那典型的test2
这个方法来分析,可以看到Code
下面的locals=22
说明局部变量有22个,args_size=9
代表参数有9个。接下来LocalVariableTable
就是局部变量表。Start
和Length
规定了当前变量作用域,Start
为指令开始位置,Length
代表从Start
开始在Length
内可以用,Slot
可以理解为下标
Start Length Slot Name Signature
0 41 0 paramShort S //参数paramShort 类型为short
0 41 1 paramInt I
0 41 2 paramLong J
0 41 4 paramBoolean Z
0 41 5 paramByte B
0 41 6 paramFloat F
0 41 7 paramDouble D
0 41 9 paramChar C
0 41 10 paramStr Ljava/lang/String; //参数paramStr 类型为java/lang/String 最前面的L代表是引用类型
3 38 11 localShort S
6 35 12 localInt I
11 30 13 localLong J
14 27 15 localBoolean Z
18 23 16 localByte B
22 19 17 localFloat F
27 14 18 localDouble D
31 10 20 localChar C
38 3 21 integer Ljava/lang/Integer; //局部变量integer 类型为java/lang/Integer 是引用类型
另外,LineNumberTable
这个记录了字节码行号和代码行号的对应关系,line
后面是指令行号,:
后面是代码行号。
index 0
开始,长度-1结束slot
(槽)reference
)和retureAddress
类型slot
byte
,short
,char
在存储前被转换为int
,boolean
也被转换成int
,0代表false
,非0代表true
long
和doubule
占两个slot
reference
类型和retureAddress
类型都按32位算,也占用一个slot
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.testInnerNoReturnValue();
int re = main.testInnerHasReturnValue(2);
System.out.println("testInnerHasReturnValue返回值:" + re);
testStatic(1L,"test");
}
public void testInnerNoReturnValue(){
System.out.println("我是实例方法-无返回值");
}
public int testInnerHasReturnValue(int paramInt){
System.out.println("我是实例方法-无返回值 参数:" + paramInt);
return 1;
}
public static void testStatic(long paramLong,String str){
System.out.println("我是静态方法");
}
}
使用javap -v
反编译一下:
//...省略类信息和main方法信息
public void testInnerNoReturnValue();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #16 // String 我是实例方法-无返回值
5: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/example/demo/Main;
public int testInnerHasReturnValue(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."":()V
10: ldc #17 // String 我是实例方法-无返回值 参数:
12: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: iload_1
16: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
19: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: iconst_1
26: ireturn
LineNumberTable:
line 19: 0
line 20: 25
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 this Lcom/example/demo/Main;
0 27 1 paramInt I
MethodParameters:
Name Flags
paramInt
public static void testStatic(long, java.lang.String);
descriptor: (JLjava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #18 // String 我是静态方法
5: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 24: 0
line 25: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 paramLong J
0 9 2 str Ljava/lang/String;
MethodParameters:
Name Flags
paramLong
str
可以看到实例方法testInnerNoReturnValue
和testInnerHasReturnValue
本地变量表中第一个就是this
,而静态方法testStatic
就不是,在testStatic
中可以看到long
占了两个slot
,若想访问,直接取index
为0的slot
即可,无需关心index
为1的slot
栈帧中的局部变量是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的局部变量就很有可能复用过期局部变量的槽位,从而达到节省资源的目的。
public class Main {
public static int COUNT = 1;
public static void main(String[] args) {
Main main = new Main();
main.testSlot();
}
public int testSlot(){
int a = 1;
int c = 0;
if(COUNT > 0){
//无任何特殊逻辑,只是用来框定变量b的作用域
int b = 1;
c = a + b;
}
int d = c + COUNT;
return d;
}
}
使用javap -v
反编译一下:
//...省略类信息和main方法信息
public int testSlot();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
//...省略指令信息
//...省略指令行号与代码行号对应信息
LocalVariableTable:
Start Length Slot Name Signature
12 4 3 b I
0 24 0 this Lcom/example/demo/Main;
2 22 1 a I
4 20 2 c I
22 2 3 d I
//...省略其他信息
可以看到变量b
的槽位号和变量d
的是一样的,变量d
在变量b
的作用域之外,便可复用slot
public void test(){
int i;
System.out.println(i);
}
这会导致编译错误,提示未初始化变量i
,所以局部变量没有赋值不能使用。
在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈和出栈。Java虚拟机的解释引擎就是基于栈的引擎。
操作数栈主要用于保存计算过程的中间结果,同时作为计算过程的中变量临时存储空间。
如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新程序计数器。
操作数栈的数据类型要和字节码指令规定的数据类型严格一致。int32位占4个字节,long占8个字节。
JVM中的栈和局部变量表用的都是数组实现栈。
public void add(){
int a = 1;
a++;
int b = 1;
++b;
}
//编译出的字节码如下 看起来++i和i++没啥区别
0 iconst_1
1 istore_1
2 iinc 1 by 1
5 iconst_1
6 istore_2
7 iinc 2 by 1
10 return
// 看下赋值
public void add(){
int a = 1;
int c = a++;
int b = 1;
int d = ++b;
}
//编辑字节码为 看下来也没啥区别
0 iconst_1
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_2
7 iconst_1
8 istore_3
9 iinc 3 by 1
12 iload_3
13 istore 4
15 return
基于栈架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这也意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。由于操作数是存储在内存中的,因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们踢出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的效率。
每一个栈帧内部都包含一个执行运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能实现动态链接(Dynamic Linking)。比如 invokedynamic指令
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池中,比如:描述一个方法调用了另外的方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
public static void main(String[] args) {
System.out.println(add(1,2));
}
public static int add(int a,int b){
return a + b;
}
//上面代码main方法编译后指令为
0 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 iconst_1
4 iconst_2
5 invokestatic #3 <com/example/demo/Main.add : (II)I>
8 invokevirtual #4 <java/io/PrintStream.println : (I)V>
11 return
//第5行字节码中的 #3 就是引用,后面注释中的com/example/demo/Main.add就是符号链接
常量池如下
在JVM中,将符号引用转化为调用方法的直接引用于方法的绑定机制相关。
静态链接:
当一个字节码文件被装在进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。绑定类型为早期绑定,即编译期绑定,后面不再变化。上面的字节码中 #2 #3 #4
都是静态链接。子类调用父类中的方法或类调用当前类中的方法,编译期可确定方法,表现为静态链接,早期绑定。
动态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行过程中将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。绑定类型为晚期绑定,即运行时绑定。多态中参数是父类,调用时传入子类都是动态链接,只有在被调用时才知道方法具体所属类是哪个。
虚方法:多态中的概念,一般指父类中可被子类重写(覆盖)的方法,Java中除了构造方法,私有方法,final修饰的方法,其它的都可以认为是虚方法。
相关联的字节指令:
普通指令:
invokestatic
:调用静态方法,解析阶段确定唯一方法版本invokespecial
: 调用
方法、私有及父类方法,解析阶段确定唯一方法版本invokevirtual
: 调用所有虚方法,如果父类中的final方法在子类中调用,用此指令,如果添加了super.
,就会使用invokespecial
invokeinterface
:调用接口方法动态调用指令:
invokedynamic
: 动态解析出需要调用的方法,然后执行普通指令固化在虚拟机内部,方法的调用执行人为不可干预,而invokedynamic
指令支持由用户确定方法版本。其中invokestatic
执行令和invokespecial
指令调用的方法为非虚方法,其余的(除final修饰的)称为虚方法。
Java为了实现动态类型语言支持添加了invokedynamic
指令,在JDK7添加,最初没有什么代码能编译出这个指令,主要靠ASM这类工具使用此指令,JDK8添加了lambda表达式,会编译成此指令。
动态类型语言和静态类型语言:对类型进行检查是在编译期还是运行期,在编译期就是静态类型语言,在运行期就是动态类型,Java是静态类型。
栈帧当中的一个内存结构,存储该方法的PC寄存器的值(PC寄存器日常存储程序的下一条指令地址)。
一个方法的结束有两种:
不管是哪种,都会返回调用处。正常退出,PC寄存器存储方法的下一条指令地址,异常退出根据异常表确定,此时栈帧中不存储任何信息。
如果发生了异常,在异常表中未找到,走异常出口,找到了走异常表处理逻辑。
返回指令包含ireturen(当返回类型是boolean,byte,char,short和int类型使用),lreturn(long),freturn(float),dreturn(double)以及areturn(引用类型),void时使用return
无特定标准,具体情况依据虚拟机实现,一般会有调试虚拟机的信息。
一个NativeMethod就是一个Java调用非Java代码的接口。该方法的实现由非Java语言实现。在定义一个Native 方法时,只定义签名,不提供方法体,类似一个接口。本地方法的作用是融合不同的编程语言为Java所用,最初用来融C/C++程序。
为什么要使用Native方法
native关键字和abstract不可同时使用。
本地方法栈线程私有,可以固定也可以动态扩容。固定时超出或者动态扩容时整体内容不够时,会抛出StackOverFlowException。
并不是所有虚拟机都支持本地方法。JVM规范没有明确规定本地方法栈的要求。
一个JVM只有一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动时即被创建,其空间大小也会随之确定。
堆是JVM管理的最大的一块内存空间。
堆的大小是可以调整的。
《JVM虚拟机规范》规定,堆在物理上可以不连续,但在逻辑上必须是连续的。
所有线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
《JVM虚拟机规范》规定所有的对象实例以及数组应当分配在堆上,实际上来说:几乎所有的对象实例以及数组应当分配在堆上。
数组和对象可能永远不会存储在栈上,因为栈帧保存引用,这个引用指向对象或者数组在堆中的位置。例外情况就是逃逸分析和标量替换。
在方法结束后,堆中的对象不会马上被移除。仅仅在垃圾收集的时候才会被移除。
堆是GC执行垃圾回收的重点区域。
public class Main {
public static void main(String[] args) {
Main main1 = new Main();
Main main2 = new Main();
String[] stringArrays = new String[5];
}
}
//字节码为
0 new #2 //这里new了一个类
3 dup
4 invokespecial #3 : ()V>
7 astore_1
8 new #2 //这里new了另一个类
11 dup
12 invokespecial #3 : ()V>
15 astore_2
16 iconst_5
17 anewarray #4 //这里是new了一个数组
20 astore_3
21 return
第0行,第8行,第17行的操作都在堆上进行。
现代垃圾收集器大部分基于分代收集理论设计,细分为
- Java7 及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
- Young Generation Space 新生区 Young/New 这个区又被划分为Eden区和Survivor区