什么是栈?什么是堆? 虚拟机中的对象的一生是如何在堆栈中度过

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

官网 :https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to
the run-time constant pool)、方法返回地址(Return Address)和附加信息。

局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。

操作数栈:以压栈和出栈的方式存储操作数的

动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态 连接(Dynamic Linking)。

方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且 这个异常没有在方法体内得到处理。

在这里插入图片描述
java类这个类用就是我们分析的对象

public class Person {
    
    
    private String name = "YongLi";
    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();
    }


}

上面的.java文件经过编译会变成下面这样

**
javac Person.java
javap -c Person >Person.txt
**

Compiled from "Person.java"
public class Person {
    
    
  public Person();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #2                  // String YongLi
       7: putfield      #3                  // Field name:Ljava/lang/String;
      10: aload_0
      11: ldc2_w        #4                  // double 100.0d
      14: putfield      #6                  // Field salary:D
      17: return

  public void say();
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #8                  // String person say ...
       5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static int calc(int, int);
    Code:
       0: iconst_3   //将int类型常量3压入[操作数栈]   
       1: istore_0   //将int类型值存入[局部变量0]      op1=3;
       2: iload_0    //从[局部变量0]中装载int类型值入栈
       3: iload_1    //从[局部变量1]中装载int类型值入栈
       4: iadd       //将栈顶元素弹出栈,执行int类型的加法,结果入栈
       5: istore_2  //将栈顶int类型值保存到[局部变量2]6: iload_2   //从[局部变量2]中装载int类型值入栈
       7: ireturn    //从方法中返回int类型的数据

  public static void order();
    Code:
       0: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1         
       1: iconst_2
       2: invokestatic  #10                 // Method calc:(II)I
       5: pop
       6: invokestatic  #11                 // Method order:()V
       9: return
}

此时你需要一个能够看懂反编译指令的宝典
比如官网的:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
网上也有很多
在这里插入图片描述

栈指向堆

如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象。
在这里插入图片描述

方法区指向堆

private static Object obj=new Object();

在这里插入图片描述

堆指向方法区

方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
在这里插入图片描述

思考 :一个对象怎么知道它是由哪个类创建出来的?怎么记录?这就需要了解一个Java对象的具体信息咯。

Java对象内存布局

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

在这里插入图片描述
在这里插入图片描述
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,而在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身,这点将在2.3.3节讨论。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

对象访问的定位(对于我们使用的虚拟机使用的是第二个方法 第一个方法只是额外的知识点)

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种
第一种方式
那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信

在这里插入图片描述
第二种方式
直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
在这里插入图片描述
两种方式的优缺点
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。对于Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

2.内存模型

图解

一块是非堆区,一块是堆区。
堆区分为两大块,一个是Old区,一个是Young区。
Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区。 Eden:S0:S1=8:1:1
S0和S1一样大,也可以叫From和To。

在这里插入图片描述
根据之前对于Heap的介绍可以知道,一般对象和数组的创建会在堆中分配内存空间,关键是堆中有这么多区
域,那一个对象的创建到底在哪个区域呢?

对象创建所在区域

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。

比如有对象A,B,C等创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了
100M或者达到一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect),
这样的GC我们称之为Minor GC,Minor GC指得是Young区的GC。
经过GC之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor
区,然后再清空Eden区中的这些对象。

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁

-XX:HandlePromotionFailure=false
-XX:HandlePromotionFailure=true

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/Major/Full 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区?

最大的好处就是解决了碎片化。也就是说为什么一个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
若上述链接找不到合适的,大家也可以自己在网上下载对应的版本

垃圾回收的丛林法则

垃圾回收机制(Garbage Collect) jvm的丛林法则

猜你喜欢

转载自blog.csdn.net/weixin_43979902/article/details/121597926