JVM整体结构与内存模型之间的关系
JVM整体结构图如下:
先贴一个代码:
package com.jvm.jvmCourse2; public class Math { public static int INITDATA = 666; public Math() { } public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); System.out.println("test"); } }
栈(线程)
栈:java虚拟机只要开始运行一个程序的时候就会给这段程序分配一个栈内存区域,专属于这个线程,所以说栈(线程)主要存储的就是属于自己的局部变量的内存区域。以上图代码为例,当我们运行这段代码的时候,java虚拟机就给我分配了一个栈。栈跟数据结构类似,都是先进后出FILO.当我执行以上代码的时候我main方法先进栈,然后是compute方法,compute方法执行完成以后需要回到main方法,此时compute方法栈帧就会被移除。在jvm中有一个参数Xss,这个参数就是设置栈的大小,默认是1M.这1M是不是虚拟机中的所有的栈的大小,而是当前线程所占用的栈的大小。当一个线程里面调用的方法过多,可能会导致栈溢出。溢出代码如下:
package com.jvm.jvmCourse2; public class StackOverFlowError { static int count=0; static void redo(){ count++; redo(); } public static void main(String[] args) { redo(); } }
JVM内存分配是一定的,当XSS的值越小说明可以启动的线程越多,值越大说明启动的线程越少。
栈帧:栈里面又有很多复杂的结构,首先就是栈帧,每个线程在执行一个方法的时候就会给这个方法分配一个内存区域,这个内存区域就叫做栈帧。当运行main方法的时候就会有
在栈中给main方法分配一个栈帧,当使用方法compute()的时候就会给compute分配一个栈帧,如下图所示:
局部变量表&操作数栈
每一个方法分配的栈帧里面都会有自己的结构,如下:
对于以上数据结构,我们根据JVM的指令文档来看看:
注意:局部变量0(iconst_0)下标一般是留给this使用,其他的不能使用。
Compiled from "Math.java"
public class com.jvm.jvmCourse2.Math {
public static int INITDATA;
public com.jvm.jvmCourse2.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();//compute方法
Code:
0: iconst_1 //将int类型常量1压入操作数栈,即a=1的1压入操作数栈
1: istore_1 //将int类型值存入局部变量1,即将1存到变量a中,到这里compute中的a=1执行完成。
2: iconst_2 //将int类型常量2压入操作数栈,即b=2的1压入操作数栈
3: istore_2 //将int类型值存入局部变量2,即将1存到变量a中,到这里compute中的b=2执行完成。
4: iload_1 //从局部变量1中装载int类型值,即将a=1放入操作数栈中
5: iload_2 //从局部变量2中装载int类型值,即将b=2放入操作数栈中
6: iadd // 执行int类型的加法
7: bipush 10 //将加法的结果压入操作数栈中
9: imul //乘
10: istore_3 //乘以常量10,将30压入操作数栈
11: iload_3 //c=30
12: ireturn //从方法中返回int类型的数据
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/jvm/jvmCourse2/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
static {};
Code:
0: sipush 666
3: putstatic #8 // Field INITDATA:I
6: return
}
程序计数器:存放的是jvm当前线程正在执行的jvm指令码的行号(即内存地址),是每个线程私有的。每执行完一行,字节码执行引擎会去更新程序计器的值。
以上1-12的字节码执行过程图如下:
方法出口
根据代码,在main方法执行完成compute()方法以后是需要返回main方法的。compute方法执行完成以后就会去方法出口里面查看应该返回到哪个方法,即方法出口记录的是compute()方法执行完成以后应该返回的方法的对应位置。
main方法也有局部变量,math,但是math是一个对象,对象是存放在堆中的,所以这里的局部变量是对象的一个引用。对象的值是放在堆中的。有的对象的值也会放在栈中。
本地方法栈
本地方法栈主要是以前用来调用底层C语言的时候使用的,一般该方法用native来修饰,现在基本上不用了,本地方法栈和栈的存在是一样的。
堆
堆中存储的全部是对象,(并不是所有对象都存储在堆中,部分对象会存储在栈中)每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)。jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。main()方法中的局部变量math,math是一个对象,该对象存储在堆中。局部变量表里面只是对堆中对象的一个引用。根据对象的内存结构(如下图)
(对象内存结构图)
根据上图,对象头中有MetaData元数据指针,该指针指向的方法区中的类元信息。因此局部变量与堆的关系是引用,堆与方法区的关系是对象头中的元数据指针。如下图所示:
方法区
类装载系统主要就是将类装载到方法区里面去,会把类的信息装载成类元信息,即类的组成部分。又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。方法区中包含的都是在整个程序中永远唯一的元素。方法区中主要存储常量、静态变量、类元信息。当静态变量有对象类型的时候,该对象存储在堆中,因此方法区会有指针指向堆。
动态链接
类在加载的过程中有个一个步骤叫解析,在这个解析的过程中主要是将符号引用替换为直接引用,什么是符号引用,来看一下本文中的代码转换成符号引用的字节码文件如下:
Classfile /E:/IDEA_SPACE/jvm-full-gc/target/classes/com/jvm/jvmCourse2/Math.class
Last modified 2019-11-20; size 857 bytes
MD5 checksum cdee9660b546aa31c2473cba0a594765
Compiled from "Math.java"
public class com.jvm.jvmCourse2.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#33 // java/lang/Object."<init>":()V
#2 = Class #34 // com/jvm/jvmCourse2/Math
#3 = Methodref #2.#33 // com/jvm/jvmCourse2/Math."<init>":()V
#4 = Methodref #2.#35 // com/jvm/jvmCourse2/Math.compute:()I
#5 = Fieldref #36.#37 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #38 // test
#7 = Methodref #39.#40 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Fieldref #2.#41 // com/jvm/jvmCourse2/Math.INITDATA:I
#9 = Class #42 // java/lang/Object
#10 = Utf8 INITDATA
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/jvm/jvmCourse2/Math;
#19 = Utf8 compute
#20 = Utf8 ()I
#21 = Utf8 a
#22 = Utf8 b
#23 = Utf8 c
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 math
#29 = Utf8 MethodParameters
#30 = Utf8 <clinit>
#31 = Utf8 SourceFile
#32 = Utf8 Math.java
#33 = NameAndType #12:#13 // "<init>":()V
#34 = Utf8 com/jvm/jvmCourse2/Math
#35 = NameAndType #19:#20 // compute:()I
#36 = Class #43 // java/lang/System
#37 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#38 = Utf8 test
#39 = Class #46 // java/io/PrintStream
#40 = NameAndType #47:#48 // println:(Ljava/lang/String;)V
#41 = NameAndType #10:#11 // INITDATA:I
#42 = Utf8 java/lang/Object
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/io/PrintStream
#47 = Utf8 println
#48 = Utf8 (Ljava/lang/String;)V
{
public static int INITDATA;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public com.jvm.jvmCourse2.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jvm/jvmCourse2/Math;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
line 9: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/jvm/jvmCourse2/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/jvm/jvmCourse2/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 13
line 15: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 math Lcom/jvm/jvmCourse2/Math;
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: sipush 666
3: putstatic #8 // Field INITDATA:I
6: return
LineNumberTable:
line 4: 0
}
SourceFile: "Math.java" Const pool中的#8呀什么的就是我们符号引用。跟本文之前的那个字节码文件相比,这个字节码文件复杂得多。这些符号都放在常量池里面。以compute方法为例,Math通过类加载器将指令码都加载到了方法区
中的某一个地方,当java代码在执行compute方法,怎么找到方法区中的compute方法的指令码呢?主要就是通过堆中对象的对象头中的类型指针找到compute方法JVM指令码的入口地址,将入口地址放入到动态
链接这个位置来。根据以上的讲解,大家对JVM内存结构有了一个大致的印象了,接下来我们来重点说明一下JVM内存结构中比较重要的堆。
堆
堆的内部主要包含4个区域:Eden、From、To、老年代。其中前三个属于年轻代(Eden、From、To),其中From、To 合并称为Survivor区。具体的结构如下图所示:
其中(8/10,1/10,1/10,2/3)是默认的堆空间分配比例。
堆结构图
如果需要调整堆的空间大小,或者其他的大小,具体每个区域的调整参数如下:
假设我们的堆默认分配空间大小为600M,那么老年代就有400M,Eden区有160M,From,To两个区域各占20M。一般情况下我们new出来的对象都是存放在Eden区域,也有可能存储在老年代。随着程序的执行,Eden区域会存放很多很多对象,Eden区域会被放满。当Eden区域放满以后就会触发一次GC,这个GC叫minor GC.主要是对Eden+Survovor区的无引用对象进行回收。即当我的栈帧里面没有对堆里的对象进行引用的时候,堆里面的这个对象就是垃圾对象,这时候就应该被minor GC回收。对于存活的对象就会放到Survivor区域中,当我继续new对象,Eden区又满的时候,minorGC就会将Eden,From区域的对象清理,并将存活对象移动到To区域去,此时Eden区域以及From区域就清空了,程序继续运行,当Eden区域再次满了时候,MinorGC就去清理Eden和To区域,将存活对象移动到From区域,此时Eden,To区域就清空了。如此循环往复。存活的对象每经历一次minorGC,对象头中的分代年龄就会增加一次,当它的分代年龄达到15的时候,这个对象还存在的话,就会被移动到老年代。对于哪些对象会被挪到老年代呢,比如静态变量,线程池,@Controller等。
如此总有一天我的老年代也会放满,当老年代存满的时候就会触发fullGC,fullGC在进行回收的时候,回收的对象比较多,耗时比较长。无论是minorGC还是fullGC都会做同一件事情就是STW(Stop The World)
下面我们来看一段代码:
package com.example.seckill; import net.bytebuddy.implementation.bytecode.Throw; import java.util.ArrayList; public class HeapTest { byte[] bytes=new byte[1024*100]; public static void main(String[] args) { ArrayList<HeapTest> arrays=new ArrayList<>(); while (true){ arrays.add(new HeapTest()); try { Thread.sleep(10); }catch (Exception e){ e.printStackTrace(); } } } }
这段代码在执行的时候,在命令窗口(cmd打开),输入jvisualvm.然后会打开如下的窗口:
Visual GC窗口正常情况下是没有的,需要自己安装。
根据以上图形我们可以看到整个对象在堆中的流转情况Eden,Survivor 0 ,Survivor 1中的循环往复的进行回收。老年代(Old Gen)在不断的增长。因为在这个程序中每一个对象都是存在引用的,因此老年代满的时候,程序就会报错(OOM,堆溢出),如下图所示
对JVM参数的设置:
下面来看一个StackOverFlowError的测试案例。
将-XSS128k,即将栈设置为128k,这样的话就会导致堆栈异常,因为栈分配的空间越小说明栈帧分配的空间越小,但是对整个JVM来说可以可运行的线程就越多。
package com.jvm.jvmCourse2; public class StackOverFlowError { static int count=0; static void redo(){ count++; redo(); } public static void main(String[] args) { try{ redo(); }catch (Throwable e){ e.printStackTrace(); System.out.println(count); } } }
运行结果:
java.lang.StackOverflowError
at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:6)
at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7)
at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7)
at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7)