0
点赞
收藏
分享

微信扫一扫

【JVM】--- 运行时数据区

前言

      ladies and gentleman , 你们好😊 ,我是羡羡 , 在上次我们提到了 jvm 概述, 已经它如何进行类加载, 在类加载之后, 数据会到达运行时数据区 , 那么运行时数据区是怎样的呢 ? 接着来一一概述

目录

👑1. 运行时数据区的组成

👑2. 虚拟机栈

💍 虚拟机栈的作用和特点

💍栈中的存储结构和运行原理

💍栈帧的内部结构

👑3. 程序计数器

👑4. 本地方法栈与本地方法接口

👑5. java堆

💍堆内存区域划分

💍对象创建的内存分配过程

💍新生区与老年区配置比例

💍 分代收集思想 : Minor GC , Major GC , Full GC

💍TLAB机制

💍 字符串常量池

👑6. 方法区

💍方法区, 栈 ,堆之间的关系

💍方法区的垃圾回收 


1. 运行时数据区的组成

      运行时数据区由5个部分组成 : 分别是: 程序计数器 , 虚拟机栈 , 本地方法栈 , java堆和方法区构成  , 如下图所示


      可以注意到 , 图中方法区和堆 用了红色标注 , 其他三个是灰色 , 这是因为方法区和堆是线程共享的 , 而程序计数器, 本地方法栈和虚拟机栈是线程私有的

2. 虚拟机栈

 虚拟机栈的作用和特点

       虚拟机栈主要来执行我们的java方法(main()等), 由于跨平台性的设计,Java 的指令都是根据栈来设计的.不同平台 CPU 架构不同,所以不能设计为基于寄存器的 , 栈主要是管程序的运行问题 ,如何去处理数据 , 而存放数据是堆的职责
       每个线程在创建是都会创建一个虚拟机栈, 它的内部保存着一个个栈帧 , 对应着一次方法的调用 , 它是线程私有的, 生命周期和线程一致

       如上图 , 方法A中调用方法B , 方法A先入栈 , 保存 局部变量 I , j 的值, 调用方法B, 方法B入栈, 保存方法B的局部变量m ,k


可能出现的异常
        StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
        如果我们写一个没有递归出口的递归调用 , 那么方法一直进栈, 最终会将栈中的空间占满, 抛出StackOverflowError(栈溢出)

栈中的存储结构和运行原理

        每个线程都有自己的栈 ,栈中以栈帧为单位存储数据, 在这个线程上正在执行的每个方法都各自对应一个栈帧
        在一条活动的线程中,一个时间点上,只会有一个活动栈 , 即只有当前在执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class)

栈帧的内部结构

1. 局部变量表

 局部变量表用来存放方法里的局部变量等

2. 操作数栈

3. 动态链接(或指向运行时常量池的方法引用)

4. 方法返回地址(或方法正常退出或者异常退出的定义)

3. 程序计数器

       顾名思义 , 程序计数器就是记录程序运行的位置,用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读取下一条指令

        程序计数器是一块很小的存储空间 , 也是运行最快的存储区域 ,是线程私有的, 与线程的生命周期一致 , 当线程中当前方法执行时 , 程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址

程序计数器主要记录当前线程执行到的位置


        如线程1 执行到第5行 ,cpu去执行其他程序了, 记录下这个第5行, 下次从记录的地方继续执行

4. 本地方法栈与本地方法接口

        本地方法栈也是线程私有的 , 本地方法并不是java语言写的(c语言) , 所以它执行时需要用到本地方法栈, 我们的java方法就在虚拟机栈执行 , 栈满也会抛出栈溢出
        它会去登记 native 修饰的方法(本地方法), 在 Execution Engine(执行引擎) 执行时加载本地方法库

像源码中这样被 native 修饰的就是本地方法了, 非java实现 

这里我们再说一下这个本地方法接口是怎么一回事

 jvm就是通过本地方法接口来实现本地方法的调用

栈是用来执行的 , 本地方法接口是处理如何调用的

5. java堆

        一个jvm实例只允许有一个堆内存 , 它在jvm启动的时候就被创建 ,空间大小这时也会确定(堆大小可以通过参数调节) , 是jvm最大的一块存储空间堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的
       堆虽然是线程共享的 , 但也可以划分线程私有的缓冲区(也就是TLAB机制,后面会说到)
       
堆是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域 , 当方法执行完后 , 堆中的对象不会被立即清除, 而是在垃圾收集时清除

堆内存区域划分

         java8 之后 堆内存分为 : 新生代老年代 ,新生代又分为Eden区(伊甸园区)和Survivor区(幸存者区) , 幸存者区有 1 和 2 ,或者也可以称为 from 和 to

  
那么为什么要进行分区呢?

       那么新创建的对象应该怎样在堆里去存放呢? 接着来看

对象创建的内存分配过程

         首先 ,我们new了一个新对象 ,它会先被放到伊甸园区 , 当伊甸园空间填满时, 垃圾回收器将会对其进行垃圾回收(Minor GC) , 它会将不被使用的对象进行销毁 , 此时会将幸存下来的对象放到幸存者0 区

       这时伊甸园区继续加载了新的对象 , 当伊甸园再次要进行垃圾回收的时候 , 此时伊甸园幸存下来的对象就和上次幸存者 0区中的对象(如果还没有被回收的话) 一起被存放到幸存者 1区中

      如果再次经历垃圾回收 , 存活下来的就放回到 幸存者 0区中, 依次交替 , 但是每次会保证有一个幸存者区是空的.

      那么什么时候该去老年区呢 ?

那么问题又来了 , 为什么最大值是 15呢 ?

          在老年区相对悠闲 , 垃圾回收的频率较低 , 老年区的垃圾回收 叫做Major GC ,养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常.Java.lang.OutOfMemoryError:Java heap space

public static void main(String[] args) { 
    List<Integer> list = new ArrayList(); 
    while(true){
       list.add(new Random().nextInt()); 
    } 
}

         如上面的程序 , 死循环一直往list中添加数据 , 此时 list 一直被占用, 老年区在Major GC后发现自己并不能回收任何的对象 ,这时再来新的对象就会抛出上面的异常 , 此时堆空间已被占满

新生区与老年区配置比例

        这个比例可以通过参数去调节, 但一般不会去调 , 默认的比例 : 年轻区 : 老年区 = 1 : 2 , 在HotSpot中 , 新生区中Eden(伊甸园) : 幸存者0: 幸存者1 = 8 : 1 : 1 , 当然这个比例也可以通过参数去调整

 分代收集思想 : Minor GC , Major GC , Full GC

         Jvm在 回收垃圾时 , 会进行分区回收 , 一般新生区回收的频率较高(Minor GC), 包括Eden,s0,s1 , 老年区收集就是Major GC

          还有一种叫做 Full GC , 我们在开发期间一般尽量避免整堆收集, 因为在Full GC收集时会触发Stop the World机制 , 此时所有线程都会停止运行, 等到Full GC 结束才会继续执行

整堆收集出现的情况有 : 

TLAB机制

       TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区

申请TLAB可以通过参数 -XX:UseTLAB开启

 字符串常量池

         我们知道创建字符串的时候, 我们没有使用直接 new 的方式去创建时 ,通过String s1 = "str" 这种方式字符串对象会存放到字符串常量池中 , 这里我们需要重点了解的是 : 

6. 方法区

       另外,方法区包含了一个特殊的区域“运行时常量池”。 Java 虚拟机规范中明确说明:”尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开.所以,方法区看做是一块独立于 java 堆的内存空间.

       方法区也是在 jvm 启动时就被创建 , 并且它实际的物理内存空间和堆一样也是不连续的 , 大小也可以调整 , 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误

方法区, 栈 ,堆之间的关系

 方法区大小设置

方法区中主要存放类信息

        如果我们有用 static final 修饰的常量就可以存放到运行时常量池中 , 这样如果需要这个值的话, 就不用去加载类, 直接从运行时常量池中去取 , 这样做减少了开销, 提高了效率

方法区的垃圾回收 

       方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
       这里我们主要看回收类的条件 , 类的回收也被称为类卸载

       判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:


结语

        到此关于运行时数据区就全部说完了 , 需要注意的是 , 本地方法接口不属于运行时数据区, 但是上面我把这两个放一起来说了 , 希望初学的同学不用混淆, 感谢您的阅读 , 下节会对执行引擎这块内容进行讲解, 再次感谢, 谢谢😊!!!

 

举报

相关推荐

0 条评论