java 计数器_【性能调优专题】【Jvm性能调优】【Jvm内存模型】【程序计数器详解】...

ce7aa85eae91fd3b2238bc80b9b7a203.png

这一期我们讲一下JVM模型中的程序计数器,我们继续来回顾下JVM模型

一、JVM模型概述

  java虚拟机(JVM)在java程序运行的过程中,会将它所管理的内存划分为若干个不同的数据区域,这些区域有的随着JVM的启动而创建,有的随着用户线程的启动和结束而建立和销毁。一个基本的JVM运行时内存模型如下所示:

3e3e3f0d26b8bcb1cc3b430f73add7f0.png

  上图展示的是“JAVA SE7”的JVM虚拟机规范。注意,虚拟机规范并不是一成不变的,Oracle在发布新的JAVA版本时,可能会对JVM做一定的优化和改进,例如在JDK8的版本中,方法区被移除,取而代之的是metaspace(元数据空间)。

  在本章及下面的章节中,将以JDK7的标准作为例子,对JVM的运行时数据区进行讲解。

什么是程序计数器

  程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

  JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。

  从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。

  首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。

程序计数器的特点

  1.线程隔离性,每个线程工作时都有属于自己的独立计数器。


  2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。


  3.执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。

900e126910ca2ab61245c7894292697c.png

  4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。

  5.程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

简单的理解为,是程序计数器保证了程序的正常执行 。

有什么特点

  • 线程私有的
  • 是java虚拟机规范里面, 唯一 一个 没有规定任何 OutOfMemoryError 情况的区域
  • 生命周期随着线程,线程启动而产生,线程结束而消亡

为什么具有这些特点

要想理解什么是程序计数器,以及它的特点,需要理解上文中的一句话

这里重点理解 :程序计数器,可以看做是当前线程执行的字节码的 行号指示器 ,这句话;要理解这句话,需要先知道字节码文件长什么样子,看下面的代码

// java 文件被翻译为字节码的时候,字节码大概类似于下面的样子

上面左边的 0、2、4、5 ,就是类似于字节码的行号(实际是指令的偏移地址),程序计数器中保存中的值,就是它们;字节码解释器,就是根据它们,来执行程序的 。

理解了程序计数器,就好理解它的这些特点了。

我们都知道,java是支持多线程的,当CPU执行权从 A 线程,转移到 B 线程的时候,JVM就要暂时挂起线程 A ,去执行线程 B ;当线程 A 再次得到CPU执行权的时候,又会挂起B线程,继续执行 A 线程 。

我们想象下,CPU是怎么知道记住之前A线程,执行到哪一处的?

答案是,CPU根本就不会记住之前执行到哪里了,它只是埋头苦干。

那是什么保证了切换线程的程序可以正常执行的。

答案是 : 程序计数器 。

程序计数器里面保存的是 当前线程执行的字节码的行号(看着像行号,其实是指令地址)。

那么,我们需要几个程序计数器呢?

如果,我们只有一个的话,切换B线程以后,程序计数器里面保存的就是B线程所执行的字节码的行号了,再切换回A线程,就蒙圈了,不知道执行到哪里了,因为,程序计数器里面保存的是B线程当前执行的字节码地址 。

因此,我们可以想象出,要为每个线程都分配一个程序计数器。

因此,程序计数器的内存空间是线程私有的 。

这样即使线程 A 被挂起,但是线程 A 里面的程序计数器,记住了A线程当前执行到的字节码的指令地址了 。

等再次切回到A线程的时候,看一下程序计数器,就知道之前执行到哪里了。

那么程序计数器,什么时候分配内存呢?

我们试想下,一个线程在执行的任何期间,都会失去CPU执行权,因此,我们要从一个线程被创建开始执行,就要无时无刻的记录着该线程当前执行到哪里了。

因此,线程计数器,必须是线程被创建开始执行的时候,就要一同被创建。

程序计数器,保存的是当前执行的字节码的偏移地址(也就是之前说的行号,其实那不是行号,是指令的偏移地址,只是为了好理解,才说是行号的,),当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址。

因此,永远都不可能内存溢出的;

因此,jvm虚拟机规范,也就没有规定,也是唯一一个没有规定 OutOfMemoryError 异常 的区域。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值