一、类装载子系统(C实现)
二、运行时数据区(内存模型)
三、字节码执行引擎(C实现)
运行时数据区内存模型:
(划分5块内存区域)
1、栈(线程私有)
概念:当我们运行一个线程时,线程栈内分配一块独立(专属)内存空间,放局部变量的内存空间。
栈帧:每一个方法都有专属的栈帧内存空间,放这个方法的局部变量,
栈的数据结构:数据结构里面的栈,从下往上分配新的栈帧内存空间,当要再内存空间取出时,从栈顶取出数据,先进的后出,后进的先出,吻合我们程序运行的规律;例如我们有个computer()方法,这个computer方法里面还有keyboard()方法;当我们运行computer()方法时肯定是先给他分配内存空间,再执行他里面的方法,也就后一步分配空间。但是我们keyboard()肯定先一步执行完,computer()后执行完。所以就刚好吻合,先进的后出,后进的先出这个道理。
栈帧内存空间(主要部分,分配的内存空间):
① 局部变量表: 局部变量放在这里(下面有解释)
②操作数栈 : 操作数具体的值放在这里(下面有解释)
③动态链接 :在程序运行的过程中把符号引用转为直接引用
compute()就相当我们程序中的符号,当我们程序加载的时候并不会解析这些非静态方法。加载的时候只会解析静态方法。只有在运行的时候我们才去解析这个符号,这些指令符号是存放在方法区的,并且有存放的位置,动态链接的意思就是把这些符号变成在代码中的直接地址。(符号地址的直接引用)
④方法出口
调用到compute()方法的时候,我们需要知道原代码在哪一行位置调用的他,方法出口存的就是这些信息。我们把方法执行完毕后,根据方法出口的信息,回到原来的代码中继续执行代码。
(局部变量,操作数栈,帮助理解)
public int keyboard(){
int a=1;
int b=2;
int c=(a+b)*10;
return c;
}
执行javap编译上面这段代码
public int compute();
Code:
stack=2, locals=4, args_size=1
0: iconst_1
将int类型的常量1压入操作数栈
1: istore_1
把上面那个值存入局部变量1里(类似索引这边代表了a,像是a这个变量在局部变量表的下标,int类型的值会出栈)
2: iconst_2
同上
3: istore_2
同上给b赋值
4: iload_1
局部变量1的值装载出来(把值放到操作数栈里)
5: iload_2
局部变量2的值装载出来(把值放到操作数栈里)
6: iadd
执行int类型的加法(从操作数栈把值拿出来做操作,把结果重新压回操作数栈)
7: bipush 10
(将8为带符号数压入操作数栈)把10压入操作数栈
9: imul
执行int类型的乘法 把3和10拿出操作数栈,做乘法运算,得到30压入操作数栈(这里跳过了8,因为10也占有一个内存空间)
10: istore_3
11: iload_3
12: ireturn
2、堆(线程共享)
new出来的对象一般放在堆里面
堆与栈的关系,一般new出来的新对象会放在堆里,而栈里面会放这个对象在堆中的所存放位置的地址(指向堆里面的对象)。
①年轻代(占用1/3):
Eden区(8/10),Survicor 1区(1/10),Survicor 2区(1/10)
新生成的对象一般放在这个区(不一定),当这个区被放满的时候会做minor gc。
minor gc:字节码执行引擎会开启一个垃圾回收线程,
gc大体过程:从方法区,栈找很多的gc roots根节点,从这些gc roots找所有被引用的对象,从这个gc roots根节点出发的都认为是非垃圾对象。标记为非垃圾对象,复制到Survicor 1区中,其他的对象直接删掉,并且把那些经历过gc的非垃圾对象分代年龄加1。当Eden区再被放满的时候会对Eden区和Survicor 1区一起进行minor gc,然后把非垃圾对象放到Survicor 2区(分代年龄继续加1).当Eden区再被放满的时候在1区和2区反复做这个动作。
gc roots根节点:线程栈的本地变量,静态变量,本地方法栈的变量等等。
②老年代(占用2/3):
当一个对象分代年龄到了15(默认值,最大也是这个)的时候会被放到老年代里面。像静态变量,spring的对象一般会被放到老年代里面。如果在做minor gc过程中Survicor 1区(或者Survicor 2区)放满了不能继续放东西,就会把这些东西放到老年代里。
full gc:当老年代被放满的时候,就会做full gc;当做完full gc老年代还是满的我们又要继续添加新的对象的时候就会,触发OOM,内存溢出报错。
STW(stop the world):当我们做gc(不管是minor gc还是full gc)操作的时候,停止用户线程。用户会体验到卡顿的感觉。full时间比较长,如果我们做调优主要是对这块。什么要这么做呢,在进行标记的时候,如果工作线程不停止的话,那么肯定会有新对象生成。这些对象是没有被标记的,里面可能有存活的对象。再者在这期间也可能会有新的引用,但是在遍历的时候没有被标记而被清除了。
3、方法区(线程共享)
常量,静态变量,类元信息(类信息),new 出来的对象是放在堆里面的,方法区指针引用堆里面堆里面的(存堆里面对象的内存地址)
4、程序计数器(线程私有)
上面(线程栈下面的那个字节码前面那个数字)每一行都有一个数字,在程序计数器中存,在内存中的地址。这里我们可以把他认为是前面的那个数字,我们每执行一行代码,相应的就会把这个数字(这行代码在内存中的位置)存入程序计数器中。(就像我们cpu的执行是以时间片段的执行的,我们在执行一行代码的时候,要执行下一行有可能就被其他线程抢占过去了,如果没有程序计数器我们再回头运行这段代码的时候,就找不到该从哪里继续运行代码了,程序计数器帮我们记录这个,回过头来就能知道在哪里继续执行了)
5、本地方法栈(线程私有)
现在的程序员用的是很少的,比如Thread.start()方法,底层实现是C语言实现的。早些年java整个jdk很多核心代码都是C语言实现的。这个区域就是存放一些本地方法(由C语言实现)。如果一个线程使用到本地方法,就会在开辟一个独有的本地方法栈。
JVM参数设置:
方法区:-XX:MaxMetaspaceSize:设置元空间最大值,默认-1(不限制),受内存大小限制
-XX:MetaspaceSize:元空间的初始值,默认值为21M。当被填满的时候,触发full gc;如果释放很多空间,那么其初始值会变小,如果没怎么释放空间,那么会初始值会变大。、
XX:PermSize代表永久代的初始容量。
栈:-Xss128k 设置栈的最大空间为128K(默认1M)