一个方法对应一块栈帧内存区域。且这个区域是在虚拟机栈(线程栈)上分配的
这个线程栈就是用的数据结构中的栈(FILO:先进后出)。
为什么用栈的数据结构存放栈帧?因为先进后出的顺序和我们嵌套调用程序的执行顺序是一致的!
通过javap命令,对.class代码反汇编,将其存入一个txt文档,便于阅读
打开文件可以看到:
然后查JVM指令手册可以知道具体含义:
先在局部变量表中存入变量名,然后操作数栈中的2出栈,并赋值给局部变量表中的b。
然后到了上面的第4行:iload_1 ,查手册意思是:从局部变量1中装载int类型值:(操作数栈中又有了值)
然后是iadd:意思是执行int类型的加法,它会从操作数栈中弹出两个元素,做加法,并把计算的结果重新 压入 操作数栈
然后是bi push 10 :在操作数栈中 入栈 10
然后是imul:执行Int类型的乘法:操作数栈先后出栈10, 3,执行乘法运算=30,重新入操作数栈
然后来到第10行 istore_3,当前线程的 程序计数器的值 为 10(行号)
问:是谁改变的程序计数器的值?是它自己变化的吗?怎么被修改的?
答:程序计数器 只是一块内存空间,它不会自己改变值!
加载的Math.class文件需要字节码执行引擎去 执行, 所以它会知道当前执行到了哪里。所以是字节码执行引擎 改变的程序计数器的值。
为什么JVM中要设计程序计数器?
答:用于线程切换!当更高优先级的时间片执行完毕后,再回到原线程时,需要通过程序计数器记录的位置,继续执行
然后是10:istore_3,在局部变量表中先分配一块局部变量3(c对应的内存空间),然后11:iload_3:把30出栈赋值给c
然后12:ireturn 返回c
以上为内存分配的流转模型,分析方法都是一样的,javap命令和命令手册。
要学并发就得这么研究。
所以操作数栈就是 在程序运行过程中,开辟的一块 临时的 中转的 内存空间。
什么是动态链接?
把符号引用转换为直接引用的,底层涉及大量C++实现
例如:
当在主线程中执行到compute()方法时,这个括号 属于一种符号引用,
compute()方法加载到方法区的内存地址放到动态链接中
通过动态链接可以找到compute()方法对应的一些代码。
方法出口
方法返回的出口一些信息,存在这个区域
每个栈帧都有这些区域(局部变量表,操作数栈、动态链接)
回看前面的程序,在main()中的math(局部变量内存空间)指向(存放了)一个堆的内存地址(math的内存地址)
以下就是栈和堆的联系
线程栈中,局部变量是引用堆的地址
方法区:
在JDK1.8以前,叫永久代(持久代),在之后就叫元空间,开始使用操作系统的物理内存,不是堆内部的内存。
其中存放常量+静态变量+类信息
这些就是class加载到方法区之后的形成的一些信息
看最上面的代码:public static User user = new User();
静态变量user存放在方法区,其会引用堆区的user
所以方法区也可以画很多的箭头 引用堆区对象
最后讲一下本地方法栈:(现在用得不多了)
首先需要知道什么是本地方法
例如在调用一个线程时:
这个start0就是本地方法,他用native修饰,本地方法栈就是存放本地方法的区域。
因此,蓝紫色的是每个线程都具备的,因此是线程私有的。而堆和方法区,是所有线程共享的。
下面说堆区:
堆内部 无非就是由年轻代,老年代组成。
年轻代有Eden区和Survivor区组成
当Eden区中的内存快满了,就会由字节码执行引擎,去执行minor GC做垃圾回收
垃圾如何收集?——可达性分析算法
上面说到,局部变量,静态变量,都是GC Roots,可达性分析算法会吧全部的GC Roots找出来,然后由此出发,找到引用的对象,一直往下,直到找到所有对象,最后一个成员变量不再引用任何其他对象为止。
凡是在这个引用链上的所有对象,GC过程中都会将其标记为“非垃圾”
标记为非垃圾的,会将其复制到 空着的 Survivor区
那么当GC结束了,剩下的还在伊甸园区的无引用对象会被回收。
经历过一次GC后存活下来的对象(复制到Survivor区域的对象)其年龄会+1
当下一次,伊甸园区又满了, 那么触发GC,minor GC会对整个年轻代进行回收,过程相同。在剩下的区域中未引用对象会被清理。
每经历一次GC。存活下来的对象的年龄都会+1,并在Survivor中两个小区对调位置(复制)
当年龄达到了15,那么这个对象会直接进入老年代
下面通过一个程序不断创建对象,来可视化GC过程:
在heapTests.add(new HeapTest())中创建的对象,始终不能回收,
因为heapTests引用了HeapTest(), 而heapTests又被外部的局部变量(GC Roots)引用。这些对象被判定为都不是垃圾对象
从上图可看到,当Eden区满了,会触发GC,并且Survivor0和Survivor1来回复制,随着GC次数增加,老年代区域内存增加。
那么当老年代内存满了怎么办?
字节码执行引擎会再开一条Full GC线程对整个堆区域做垃圾回收(包括年轻代和老年代)
当最后Full GC以后,还是没有内存空间,那么就会报OOM了。
JVM调优真正的目的?
主要减少FULL GC的次数,以及执行的时间。
减少STW:Stop the World,尽量减少STW的时间和次数
在垃圾收集线程执行时,会停掉用户线程。GC会影响性能。
蚂蚁金服问:为什么JVM要设计一个STW的机制?GC能否不设计STW,这样网站就不会卡顿了呀?
答:假设不STW,那么可能在GC过程中,如果线程提前结束了(方法结束),那么这部分内存都会出栈,整个线程栈对应的内存都会销毁掉。那么这个math这些局部变量引用的对象还在堆区,不会随线程结束而结束(堆区对象要GC过程才能回收),堆区对象可能会由非垃圾变为垃圾。也就是说如果没有STW,线程边执行,边执行GC回收,当堆区的对象的状态发生变化(非垃圾-变为垃圾),那么回收的程序不好设计,每次都要再去检查GC ROOTS。
所以不如STW快速做完垃圾回收工作。
垃圾收集器
每一种垃圾收集器都有STW机制,(过程、时机可能不一样)
分配JVM的参数:
线程运行时,每秒产生60MB的对象,会存放到Eden区
对象动态年龄判断机制:
如果eden区中一批对象的总大小大于这块 Survivor区域 (S0)内存大小的50%,那么这个大对象会直接进入老年代。所以,每隔4-5分钟这个老年代区域就会被放满,触发FULL GC(问题所在)
阿里面试题:能否对JVM调优,让其几乎不发生FULL GC?(允许可能一两次)
调优:
把年轻代Xmn调大一点,为2G,(老年代没必要太大为2G,调为1G),Eden区调为1.6G,那么S0,S1都为200M。
此时,产生的对象先到Eden区,约25s占满Eden区,之后触发Minor GC,存活下来的对象到S0/S1区时内存没有达到Survivor区域的50%.经过25S线程肯定早就结束了,那么在S0、S1中的对象相应也能够回收。就能够做到,垃圾对象能够一定在年轻代被回收,不会拖到老年代占用空间,所以性能有提升。
调优:
尽可能地让那些朝生夕死的垃圾对象,通过合理的JAVA虚拟机的各个内存区域之间的关系、比例,让垃圾对象在年轻代被干掉。
G1垃圾收集分类
G1垃圾收集器参数设::
网易2016年笔试题目:
已知虚拟机的一些参数设置如下:
-Xms:1G;
-Xmx:2G;
-Xmn:500M;
-XX:MaxPermSize:64M;
-XX:+UseConcMarkSweepGC;
-XX:SurvivorRatio=3;
求Eden区域的大小?
分析:
这是网易2016年在线笔试题中的一道选择题。 先分析一下里面各个参数的含义:
-Xms:1G , 就是说初始堆大小为1G
-Xmx:2G , 就是说最大堆大小为2G
-Xmn:500M ,就是说年轻代大小是500M(包括一个Eden和两个Survivor)
-XX:MaxPermSize:64M , 就是说设置持久代最大值为64M
-XX:+UseConcMarkSweepGC , 就是说使用使用CMS内存收集算法
-XX:SurvivorRatio=3 , 就是说Eden区与Survivor区的大小比值为3:1:1
题目中所问的Eden区的大小是指年轻代的大小,直接根据-Xmn:500M和-XX:SurvivorRatio=3可以直接计算得出
解:
500M*(3/(3+1+1))
=500M*(3/5)
=500M*0.6
=300M
所以Eden区域的大小为300M。
小结
1,整个堆包括年轻代,老年代和持久代。其中年轻代又包括一个Eden区和两个Survivor区。
2,年轻代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
3,持久代:
-XX:PermSize
-XX:MaxPermSize
4,年轻代和老年代的比例:
-XX:NewRatio(年轻代和老年代的比值,年轻代多,除去持久代)
当设置了-XX:+UseConcMarkSweepGC后,会使-XX:NewRatio=4失效,此时需要使用-Xmn设置年轻代大小
5,Eden与Survivor的比例
-XX:SurvivorRatio(Eden区与两个Survivor区的比值,Eden区多)