运行时数据区

程序计数器

程序计数器是当前线程所执行的字节码的行号指示器,字节码指示器工作时通过改变这个计数器的值来选取下一行需要执行的字节码指令。每个线程都有一个独立的程序计数器,各个线程之间不会相互影响。此区域也是唯一一块不会发生oom区域。
需要注意的是,它存储的并不是当前的指令地址,而是将要执行的下一行的指令地址。
为什么要使用程序计数器记录当前线程的字节码指令的地址?
因为cpu需要进行切换,切换回来之后需要知道从哪里开始执行,字节码指示器通过改变程序计数器的值来明确下一行该执行怎样的字节码指令。

java虚拟机栈

和程序计数器一样,java虚拟机栈也是线程私有的,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法执行完毕,都会对应一个栈帧在虚拟机栈入栈到出栈的过程。
局部变量表存放的是编译期间基本的数据类型(byte、short、long等八大基本类型)、对象引用。其中long、double会占用2个局部变量空间(slot),其余的数据类型占用1个,因为一个slot占用32位。局部变量表所需的内存空间在编译期间就已经分配完毕,所以,当方法执行时,该方法在栈帧中所分配的大小已经是完全确定的了,在运行期间不会改变该栈帧的大小。
虚拟机栈是线程级别的,栈帧是方法级别的,所以一个线程对应多个方法,一个虚拟机栈中有多个栈帧。

栈帧

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程中,都会对应一个栈帧在虚拟机栈里面从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大局部变量表,多深的操作数栈都已经完全确定了,并且写到了方法表中的Code属性中,因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体虚拟机的实现。一个线程中的方法调用链可能会很长,很多方法都处于执行状态,对于执行引擎来说,只有位于栈顶的栈帧才是最有效的,称为当前栈帧,与这个栈帧相关联的方法被称为当前方法。执行引擎运行的所有字节码指令都只对当前栈帧进行操作。
在这里插入图片描述

局部变量表

局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量。其容量以变量槽为最小单位—slot,一个slot可以放一个32位以内的数据类型,也就是说除了long和double之外,都会占用一个slot,long和double会占用两个。
为了节省栈帧的空间,局部变量表中的slot是可以复用的,如下,我们在jvm参数中加入 -verbose:gc参数来查看垃圾回收信息。
当我们执行下面的代码时,用代码块将内存占用的代码包裹起来,然后通知jvm进行gc。

public class OOMTest {

    public static void main(String[] args) {
        {
            byte[] placeHolder = new byte[64 * 1024 * 1024];
        }
        System.gc();
    }
}

打印结果如下:
[GC (System.gc()) 73431K->66176K(502784K), 0.0234945 secs]
[Full GC (System.gc()) 66176K->65965K(502784K), 0.0037857 secs]

第一行的GC表示进行了minor GC,发生在年轻代;73431K->66176K表示堆之前是73431K,进行了垃圾回收之后,变成了66176K,(502784K)表示堆的总大小
第二行的Full GC表示进行了 full GC,发生在整个堆中,66176K->65965K表示回收之前是66176K(和第一行的回收之后的大小一致),回收之后变成了65965K。
以上说明placeHolder 没有被回收掉。

当我们再加上一行代码之后:

public class OOMTest {

    public static void main(String[] args) {
        {
            byte[] placeHolder = new byte[64 * 1024 * 1024];
        }
        int a = 1;
        System.gc();
    }
}

打印结果:
[GC (System.gc()) 73431K->672K(502784K), 0.0058833 secs]
[Full GC (System.gc()) 672K->429K(502784K), 0.0034925 secs]
当我们加上int a =1之后,发现内存被回收了。
placeholder能否被回收的原因在于局部变量表中是否还存在placeholder的引用,第一种写法,虽然代码离开了代码块,但是没有对局部变量表进行写入操作,placeholder所在slot还没有被复用,所以gc root仍然保持着对它的关联,但如果遇到一个新的方法,前面的变量也已经不再使用,可以清除之前的引用,然后复用之前的局部变量表中slot。

局部变量表的查看

可以在idea下载该插件
在这里插入图片描述
然后在View中选择该选项
在这里插入图片描述
可以看到局部变量表中的信息
在这里插入图片描述
可以看到序号列是从0开始的,也就是说slot默认是从0开始的,但是 b 这个变量占用了2个slot,所以 oomTest 变量就从4开始。所以总共用了5个slot。
在这里插入图片描述
如果是构造方法的话,slot中的参数名字叫做this。
在这里插入图片描述
当我们在用这个插件来看上面placeholder问题时,可以看到只有两个slot,证明placeholder被复用了
在这里插入图片描述

操作数栈

操作数栈也被称为操作栈,它是一个先入后出的栈,同局部变量表一样,操作数栈的最大深度也是在编译期间就已经确定了,32位数据类型所占栈的容量为1,64位所占为2,在方法执行的时候,操作数栈的深度不会超过栈的最大深度。
它用来保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
当一个方法刚开始执行时,栈帧被创建,此时操作数栈是空的,操作数栈的访问和局部变量表不一样,并不能根据索引来访问,而是通过入栈和出栈来完成一次数据访问。

执行该方法查看操作数栈

    public  void cal(){
        int a = 1;
        int b = 2;
        int c = a + b;
    }

通过 javap -v 来查看
在这里插入图片描述
1、由 iconst_1 指令将数字1压入栈顶
在这里插入图片描述
2、成功入栈后,istore1 命令将1 存入到局部变量表中
在这里插入图片描述
3、接下来 执行 iconst_2,将数字2压入栈顶
在这里插入图片描述
4、通过 istore_2 命令将栈顶元素出栈,并将2 存储局部变量表中
在这里插入图片描述
5、iload_1 命令将将局部变量表中1 重新压入栈顶
在这里插入图片描述
6、iload_2 命令将将局部变量表中2 重新压入栈顶
在这里插入图片描述
7、iadd命令将两个数字出栈后,计算新值然后重新压入栈顶
在这里插入图片描述
8、istore3 命令将结果出栈后,存入到局部变量表中,最后return进行返回。
在这里插入图片描述

栈上的oom

栈的容量是由-Xss参数设定的,在虚拟机栈和本地方法栈中,虚拟机描述了两种异常:
1、如果线程请求的栈的深度大于虚拟机所允许的最大栈深度,将抛出StackOverFlow异常。
2、如果虚拟机在扩展栈时无法申请到足够的空间(hot spot无法扩展),则将抛出OutOfMemroyError异常。
当使用-Xss缩小栈容量时,抛出异常:


public class StackOOM {
    private void add(){
        add();
    }

    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.add();
    }
}

在这里插入图片描述
栈帧的大小是编译期间定义好的,但是在编译期间,并不会执行递归方法,所以在运行期间排出stack over flow error。

本地方法栈

本地方法栈和Java虚拟机栈的作用是一样的,区别是本地方法栈对应的native方法,java虚拟机栈对应的是Java方法。

Java堆

Java堆是所有线程共享的一块区域,基本所有的对象实例都会在这里分配内存。Java堆还可以细分为新生代和老年代,新生代又可以细分为eden、from、to区域。无论如何划分,存放的内容是没有差别的,划分的原因是为了内存进行更好的回收,或者更快的分配内存。Java堆可以是物理上不连续的,逻辑上连续即可。

堆上内存的分配

创建对象的过程

当使用一个new 命令,创建一个对象时,jvm首先会在方法区检查能否找到这个class 的符号引用,并检查这个class有没有被加载、解析和初始化过,如果没有,则必须执行相应的类加载过程。类加载后,jvm将会为该对象分配内存,该对象所需要的大小在类加载完之后便可以确定。

假设内存是规整的,所有用过的内存放到一边,没有用过的放到一边,中间放着一个指针,当给对象分配内存时,就将指针向空闲的地方挪动和对象大小相当的距离,这种方法叫做“指针碰撞”;如果Java中的内存并不是规整的,虚拟机就维护一个列表,列表上记录哪些内存是可用的,分配的时候从列表中找到一块足够大的空间分配给对象,并更新列表上的记录,这种分配方法叫做“空闲列表”。
选择哪种分配方式是由java堆是否规整来决定的。

分配完内存之后,jvm将分配的内存空间设置为零值(null),接下来,jvm要对对象的对象头(Object header)进行设置,这一套操作下来,从jvm的角度来看,对象已经创建完成了,但从java的角度来看,对象创建才刚刚开始,所以一般new之后,就会执行方法,即对成员变量进行赋值。

内存的分配

1、new 的对象先放到eden区。
2、当eden区填满之后,程序又需要创建新的对象时,此刻jvm进行回收(yong gc),将eden中不再使用的对象进行销毁,再将新的对象放入到eden中
3、然后将eden中剩余的对象移动到S0区,被移动到S0区中的对象有一个年龄计数器,此时值为1。
4、如果再次进行gc时,垃圾收集器将eden中和S0中进行垃圾回收,存活的对象移动到S1区,S0中移过去的对象年龄变为2,eden中转移过区的对象年龄变为1,此时S0中没有对象。
5、如果再次进行垃圾回收,存活的对象放入到S0中,接着再去S1区,对象在S0和S1每移动一次,年龄就+1。
6、默认的年龄是15,超过15的对象会进入老年代。
下面是示意图:
1、刚开始,对象的分配在Eden中
在这里插入图片描述
2、当Eden中放不下的时候,会发生Minor GC,找出不可达的垃圾对象,进行回收,然后将可达对象移动到S0区中。
在这里插入图片描述
3、随着时间的推移,当Eden中又满的时候,再次发生Minor GC,这时Eden和S0区可能都有垃圾对象了,然后将Eden和S0中的对象搬到S1区,这一轮GC之后,Eden和S0区变成了空的。
在这里插入图片描述
4、随着对象的不断分配,Eden可能又满了,重复之前的Minor GC过程,这时S0是空的,然后S0和S1的角色进行互换:存活的对象从Eden和S1复制到S0,然后清空Eden和S1中的对象垃圾。
在这里插入图片描述
5、总是存活的对象在复制过程中占用空间,所以会将达到年龄限制的对象转移到老年代中。

有关对象个数的问题

1、String str =new String(“ab”) 会创建几个对象?在这里插入图片描述
“ab”如果不在常量池中,会创建一个;new String 创建一个;一共最多会创建2个对象。

2、String str =new String(“a”) + new String(“b”)
在这里插入图片描述
当使用 “+”号时,是用StringBuilder拼接的,它算一个;然后 new String(“a”) 和new String(“b”)各是两个;最后拼接好的 “ab”算一个 ,一共是6个。

3、String str =“a”+ “b”
在这里插入图片描述
上面只会创建一个“ab”对象,是在编译期间就创建好了。

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值