阅读相关资料,自己先画了一个jvm内存模型草图
类装在子系统不过多解释,上一篇类加载机制说的就是这个。
字节码执行引擎(执行编译好后class文件指令码的程序),为C语音实现,不可见,不展开讲,下面主要来看内存模型中的5块。
芜湖起飞~
先从栈开始讲:
栈后面加了个括号,线程,栈就是线程在执行方法的时候存放的一些方法内部的局部变量。
当一个线程执行方法,会在栈里面开辟这个线程的栈空间。栈中有n个线程方法空间,我们看下面的程序
public class Stack { public int add() { int a =1; int b = 2; int c = (a+b) * 2; return c; } public static void main(String[] args) { Stack stack = new Stack(); stack.add(); } }
我们执行main函数,开启主线程,这个时候,栈会开辟一个main线程空间,如下图:
这里来解释一个重要的概念:先进后出。
我们看程序,先执行main方法:这时候,main方法栈帧入栈(栈帧:每个线程开启时对应的方法内存区域)
然后main方法调用add方法,这个时候add方法入栈:
然后add方法执行完了,add方法出栈:
最后是main方法执行完成,main方法出栈:
上面这个过程很好的解释了栈的先进后出的概念。
上图中每一个方法在对应线程的栈空间中都有与之相对应的栈帧,那么栈帧中又有啥呢?看下图:
里面有一些东西,为了更方便弄懂这些,我们来做以下操作:
1:打开cmd,进入到Stack类的字节码文件所在地;
2:执行javap -c Stack.class > Stack.txt;
3:打开Stack.txt文件;
4:打开java指令码操作手册(没有的百度);
这个时候我们可以来看Stack.txt里面都是啥:
Compiled from "Stack.java" public class com.ghsy.user.test.Stack { public com.ghsy.user.test.Stack(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int add(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: iconst_2 8: imul 9: istore_3 10: iload_3 11: ireturn public static void main(java.lang.String[]); Code: 0: new #2 // class com/ghsy/user/test/Stack 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method add:()I 12: pop 13: return }
我们来看add方法中有啥,看Code下面第一行:
0:iconst_1 - 将int类型值1压入栈(为啥是下标1不是0呢?0对应的是当前类对象this),这是个怎样的过程呢?看下图:
再看第二行:
1:istore_1 - 从栈中弹出int类型值,然后存到位置为1的局部变量中
第三行与第四行跟上面一样,不在赘述。执行第三行与第四行后结果为:
继续看第五行&第六行&第七行:
4: iload_1 - 从局部变量1中装载int类型值入栈(将下标为1的变量值压入操作数栈)
5: iload_2 - 从局部变量2中装载int类型值入栈(将下标为2的变量值压入操作数栈)
6: iadd - 将栈顶两int类型数相加,结果入栈(将操作数栈的两个int相加,然后重新压入操作数栈)
继续看第八行&第九行:
7: iconst_2 - 将int类型值2压入栈 (把下标为2的变量值压入操作数栈)
8: imul - 将栈顶两int类型数相乘,结果入栈(把操作数栈里面的两个数相乘所得结果压入操作数栈)
继续看第十行:
9: istore_3 - 将栈顶int类型值保存到局部变量3中(将操作数栈里面的6出栈到c的内存区域中)
第十一行:
10: iload_3 - 从局部变量3中装载int类型值入栈(把c的值压到操作数栈)
11: ireturn - 返回int类型值(把操作数栈的int类型值返回)
从上面对add方法的反编译指令码可以很好的看出一个方法执行过程,这个时候我们再来看下栈帧中的四个概念:
1-局部变量表:存放方法内部定义的局部变量内存空间
2-操作数栈:操作数的临时存放内存区域
3-动态链接:==================================这里先空着,等会解释
4-方法出口:方法的指令出口(记录上层方法执行指令码的位置)
这里插入程序计数器和本地方法栈:从上面一系列图可以看出,栈和程序计数器和本地方法栈的颜色是一致的,所以说,这两快空间也是跟线程挂钩的
程序计数器:记录jvm执行指令码的行数(Stack.txt的方法Code中行数),随着程序执行一直变动。
本地方法栈,记录当前线程调用本地方法(native修饰的方法)中的所产生内存的容器。
看下图:
到这里,栈基本结束了,顺便把程序计数器和本地方法栈带上了
问题1:栈会在什么情况下会溢出?
方法嵌套调用的时候占用内存超过栈的默认大小的时候会出现栈溢出的情况。
问题2:栈的默认大小是多大,如何修改?
栈的默认大小为1024KB(一个线程栈给的空间),修改命令为:-Xss128k
问题3:栈的修改会发生什么情况?
如果把栈默认大小调小,在栈总容量不变的情况下,可以执行更多的线程,同理,调大反之。
========================我是分割线===========================
下面我们来看方法区
方法区:主要存放常量&静态变量&类元信息(类编译好以后的组成部分,成员变量&方法&修饰符等等;即存放类中符号引用=>常量池中)---(可以看下面代码)
上面说到类元信息,我们可以再次反编译Stack.class看看,我们到clas目录下, 执行命令javap -v Stack.class > Stack.txt;我们打开txt:
Classfile /D:/work/mycode/cloud-parent/app-user/target/classes/com/ghsy/user/test/Stack.class Last modified 2020-7-27; size 588 bytes MD5 checksum 463b382878dd7e8e370be348be2980b6 Compiled from "Stack.java" public class com.ghsy.user.test.Stack minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#26 // java/lang/Object."<init>":()V #2 = Class #27 // com/ghsy/user/test/Stack #3 = Methodref #2.#26 // com/ghsy/user/test/Stack."<init>":()V #4 = Methodref #2.#28 // com/ghsy/user/test/Stack.add:()I #5 = Class #29 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 LocalVariableTable #11 = Utf8 this #12 = Utf8 Lcom/ghsy/user/test/Stack; #13 = Utf8 add #14 = Utf8 ()I #15 = Utf8 a #16 = Utf8 I #17 = Utf8 b #18 = Utf8 c #19 = Utf8 main #20 = Utf8 ([Ljava/lang/String;)V #21 = Utf8 args #22 = Utf8 [Ljava/lang/String; #23 = Utf8 stack #24 = Utf8 SourceFile #25 = Utf8 Stack.java #26 = NameAndType #6:#7 // "<init>":()V #27 = Utf8 com/ghsy/user/test/Stack #28 = NameAndType #13:#14 // add:()I #29 = Utf8 java/lang/Object { public com.ghsy.user.test.Stack(); 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/ghsy/user/test/Stack; public int add(); 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: iconst_2 8: imul 9: istore_3 10: iload_3 11: ireturn LineNumberTable: line 7: 0 line 8: 2 line 9: 4 line 10: 10 LocalVariableTable: Start Length Slot Name Signature 0 12 0 this Lcom/ghsy/user/test/Stack; 2 10 1 a I 4 8 2 b I 10 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/ghsy/user/test/Stack 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method add:()I 12: pop 13: return LineNumberTable: line 16: 0 line 18: 8 line 19: 13 LocalVariableTable: Start Length Slot Name Signature 0 14 0 args [Ljava/lang/String; 8 6 1 stack Lcom/ghsy/user/test/Stack; } SourceFile: "Stack.java"
上面的存放的就是Stack的类元信息。
我们看main反法,调用add方法处,我们把这个 (4: invokespecial #4)指向了常量池中的是#4,我们再去常量池看#4,就是method add(),#4中又包含#2和#26,
继续去找#2和#26分别对应为stack对象和add ();其他代码同理
上述过程中,把一个个符号引用转为具体的引用过程(存放符号引用具体指令码的入口地址),就是上面缺失的动态链接,同理,上一篇类加载机制中的静态链接也一样,
只是两者区别在于静态链接是类加载的时候完成,动态链接是程序运行期间完成。如果用图来表示的话,那应该就是下图:
那么问题来了
问题4:类元信息是什么
存放的是xxx.class编译后的指令码
问题5:什么会被放入到方法区
常量:final修饰的变量;静态变量:static修饰的变量;类元信息:类编译过后的指令码
==================我是分割线====================
下面我们来看堆:
存放的都是对象内存地址,里面可以分为两大快,一个为老年代(默认站堆内存大小为2/3);一个为年轻代默认站堆内存大小为1/3)
年轻代又可以分为一个Eden区(默认占年轻代的8/10)和连个Survivor区(默认占年轻代的1/10)
年轻代:新生成的对象存放区域。
老年代:大对象;默认15次minor gc存活的对象;动态年龄判断可以直接进入老年代的对象;达不到15次minor gc,但是由于gc过后Survivor区存放不下的对象(这块下一篇具体展开来讲)
5个区域都差不多了,那么我们5个区域是什么关系呢?我们现在串起来看看:
以上都是比较粗略的内存模型,自己刚刚学习到这,做个简单记录。
生而为人,我很抱歉