结合字节码指令理解虚拟机栈和栈帧

官网 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

 每个栈帧中包括:

局部变量表,操作数栈,动态链接,方法返回地址

局部变量表 : 方法中定义的局部变量以及方法的参数存放在这张表中
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。 (局部变量以0,1,2这样的顺序存在)
操作数栈 : 以压栈和出栈的方式存储操作数的
动态链接 : 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态
连接 (Dynamic Linking)
方法返回地址 : 当一个方法开始执行后 , 只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且
这个异常没有在方法体内得到处理。

 

class Person {
    private String name = "Jack" ;
    private int age ;
    private final double salary = 100 ;
    private static String address ;
    private final static String hobby = "Programming" ;
    public void say (){
        System . out . println ( "person say..." );
    }
    public static int calc ( int op1 , int op2 ){
        op1 = 3 ;
        int result = op1 + op2 ;
        return result ;
    }
    public static void order (){
    }
    public static void main ( String [] args ){
        calc ( 1 , 2 );
        order ();
    }
}

 javap 反编译获得指令

此时你需要一个能够看懂反编译指令的宝典
比如官网的: https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
当然网上也有很多,大家可以自己查找

 

class Person {
...
public static int calc ( int , int );
    Code :
        0 : iconst_3 // int 类型常量 3 压入 [ 操作数栈 ]
        1 : istore_0 // int 类型值存入 [ 局部变量 0]
        2 : iload_0 // [ 局部变量 0] 中装载 int 类型值入栈
        3 : iload_1 // [ 局部变量 1] 中装载 int 类型值入栈
        4 : iadd // 将栈顶元素弹出栈,执行 int 类型的加法,结果入栈
For example , the iadd instruction ( §iadd ) adds two int values together . It
requires that the int values to be added be the top two values of the operand stack , pushed
there by previous instructions . Both of the int values are popped from the operand stack .
They are added , and their sum is pushed back onto the operand stack . Subcomputations may be
nested on the operand stack , resulting in values that can be used by the encompassing
computation .
        5 : istore_2 // 将栈顶 int 类型值保存到 [ 局部变量 2]
        6 : iload_2 // [ 局部变量 2] 中装载 int 类型值入栈
        7 : ireturn // 从方法中返回 int 类型的数据
    ...
}
 

 思考一下

1.栈可以指向堆 ,like Object a=new Object();在方法中new

2.方法区指向堆, 静态变量static Object aa=new Object(();

3.堆指向方法区,Class对象的类信息就是存储在方法区中

 

4 Java对象内存布局

 一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充

对齐填充->如何扩容

 

内存模型

一块是非堆区,一块是堆区。
堆区分为两大块,一个是 Old(对象会存在比较长时间,标记清除,标记-整理算法) 区,一个是 Young区(大多朝生夕死,只有少数会存活,复制算法)。
Young 区分为两大块,一个是 Survivor 区( S0+S1 ),一块是 Eden区。 Eden:S0:S1=8:1:1
S0 S1 一样大,也可以叫 From To
 
 

 

对象创建所在区域
一般情况下,新创建的对象都会被分配到 Eden 区,一些特殊的大的对象会直接分配到 Old 区。
 
比如有对象 A B C 等创建在 Eden 区,但是 Eden 区的内存空间肯定有限,比如有 100M ,假如已经使用了
100M 或者达到一个设定的临界值,这时候就需要对 Eden 内存空间进行清理,即垃圾收集 (Garbage Collect)
这样的 GC 我们称之为 Minor GC Minor GC 指得是 Young 区的 GC
经过 GC 之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到 Survivor
区,然后再清空 Eden 区中的这些对象。
Survivor 区详解
 
由图解可以看出, Survivor 区分为两块 S0 S1 ,也可以叫做 From To
在同一个时间点上, S0 S1 只能有一个区有数据,另外一个是空的。(复制算法,解决空间碎片问题,但浪费了空间)
 
接着上面的 GC 来说,比如一开始只有 Eden 区和 From 中有对象, To 中是空的。
此时进行一次 GC 操作, From 区中对象的年龄就会 +1 ,我们知道 Eden 区中所有存活的对象会被复制到 To 区,
From 区中还能存活的对象会有两个去处。
若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到 Old 区, 如果Eden区和From区 没有达到阈值的
对象会被复制到 To 区。 此时 Eden 区和 From 区已经被清空 ( GC 的对象肯定没了,没有被 GC 的对象都有了各
自的去处 )
这时候 From To 交换角色,之前的 From 变成了 To ,之前的 To 变成了 From
也就是说无论如何都要保证名为 To Survivor 区域是空的。
Minor GC 会一直重复这样的过程,知道 To 区被填满,然后会将所有对象复制到老年代中。
Old 区详解
 
从上面的分析可以看出,一般 Old 区都是年龄比较大的对象,或者相对超过了某个阈值的对象。
Old 区也会有 GC 的操作, Old 区的 GC 我们称作为 Major GC
对象的一辈子理解
一个普通的 Java 对象 , 出生在 Eden , Eden 区我还看到和我长的很像的小兄弟 , 我们在 Eden 区中玩了挺长时间。有
一天 Eden 区中的人实在是太多了 , 我就被迫去了 Survivor 区的 “From” , 自从去了 Survivor , 我就开始漂了 , 有时候在
Survivor “From” , 有时候在 Survivor “To” , 居无定所。直到我 18 岁的时候 , 爸爸说我成人了 , 该去社会上闯闯
了。
于是我就去了年老代那边 , 年老代里 , 人很多 , 并且年龄都挺大的 , 我在这里也认识了很多人。在年老代里 , 我生活了 20 ( 每次
GC 加一岁 ), 然后被回收。
常见问题
 
Minor GC: 新生代
Major GC: 老年代
Full GC: 新生代 + 老年代
 
为什么需要 Survivor ? 只有 Eden 不行吗?
 
如果没有 Survivor,Eden 区每进行一次 Minor GC , 并且没有年龄限制的话 存活的对象就会被送到老年代。
这样一来,老年代很快被填满 , 触发 Major GC( 因为 Major GC 一般伴随着 Minor GC, 也可以看做触发了 Full GC)
老年代的内存空间远大于新生代 , 进行一次 Full GC 消耗的时间比 Minor GC 长得多。
执行时间长有什么坏处 ? 频发的 Full GC 消耗的时间很长 , 会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低 Full GC 频率,但是随着老年代空间加大 , 一旦发生 Full
GC, 执行所需要的时间更长。
假如减少老年代空间,虽然 Full GC 所需时间减少,但是老年代很快被存活对象填满 ,Full GC 频率增加。
所以 Survivor 的存在意义 , 就是减少被送到老年代的对象 , 进而减少 Full GC 的发生 ,Survivor 的预筛选保证 , 只有经历 16
Minor GC 还能在新生代中存活的对象 , 才会被送到老年代。
为什么需要两个 Survivor 区?(s0,s1)
最大的好处就是解决了碎片化。也就是说为什么一个 Survivor 区不行 ? 第一部分中 , 我们知道了必须设置 Survivor 区。假设
现在只有一个 Survivor , 我们来模拟一下流程 :
刚刚新建的对象在 Eden , 一旦 Eden 满了 , 触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循
环下去 , 下一次 Eden 满了的时候 , 问题来了 , 此时进行 Minor GC,Eden Survivor 各有一些存活对象 , 如果此时把 Eden 区的
存活对象硬放到 Survivor , 很明显这两部分对象所占有的内存是不连续的 , 也就导致了内存碎片化。
永远有一个 Survivor space 是空的 , 另一个非空的 Survivor space 无碎片。
新生代中 Eden:S1:S2 为什么是 8:1:1
新生代中的可用内存:复制算法用来担保的内存为 9 1
可用内存中 Eden S1 区为 8 1
即新生代中 Eden:S1:S2 = 8 1 1
体验与验证
 
使用 jvisualvm 查看
 
visualgc 插件下载链接
https://visualvm.github.io/pluginscenters.html ---> 选择对应版本链接 ---> Tools --->Visual GC
 
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值