博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JVM(2)---程序计数器与虚拟机栈
阅读量:4087 次
发布时间:2019-05-25

本文共 3778 字,大约阅读时间需要 12 分钟。

jvm内存结构,如下图:

java虚拟机(jvm)在java程序运行的时候,会将它所管理的内存划分为若干个不同的数据区域,这些数据区域有的随着jvm的启动而创建,有的随着用户线程的启动和结束而建立和销毁。

一、程序计数器(Program counter register)

根据上面的内存结构图,我们来了解以下什么是程序计数器

1、什么是程序计数器(program counter register) ?

程序计数器是用来记住下一条jvm指令的执行地址和行号的。

当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存中读取到指令寄存器中,此过程称之为"取指令"。

同时,PC中的地址给出下一条指令的地址。此后,经过分析指令,执行指令,完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

2、程序计数器的特点

1)、线程私有,即每个线程都有自己的程序计数器

cpu会为每个线程分配时间片,当当前线程的时间片使用完后,cpu就会去执行另一个线程中的代码。

程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令。

2)、不会存在内存溢出,它是唯一的一个在java虚拟机规范中没有任何OutOfMemoryError的区域。

3)、线程隔离性,每个线程工作时都有属于自己的独立计数器

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

5)、执行native本地方法时,程序计数器的值为空(Undefined)

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

3、jvm的指令也称之为二进制字节码指令

java虚拟机的指令由一个字节长度的,代表某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。

虚拟机中许多指令并不包含操作数,只有一个操作码。格式如下:

opcode (1 byte)  operand1 (optional)  operand2 (optional)

在当前执行方法的栈帧里,一条指令可以将值在操作栈中入栈或出栈,可以在本地变量数组中悄悄的加载或者存储值。

下面我们来看一段java代码,如下:

import java.io.PrintStream;public class ProcessCounter {    public static void main(String[] args) {        PrintStream out=System.out;        out.println(1);        out.println(2);        out.println(3);        out.println(4);        out.println(5);    }}

比如,该java类在F盘test文件夹下,我们通过javac ProcessCounter.java编译生成ProcessCounter.class二进制文件

 

cmd窗口操作,进入F盘下的test文件夹,进入ProcessCounter.class类所在文件目录

执行如下命令:

javap -c ProcessCounter.class

得到如下结果

上面是二进制字节码,二进制字节码主要先交给解释器来进行解释成机器码,这样CPU才能看懂。

那么问题来了,解释器一次解释一句二进制字节码指令,那么解释器如何知道下一条二进制字节码指令是什么呢?

这时就需要程序计数器了,程序计数器记录着下一条指令的地址,例如此时解释器执行第一条字节码指令,那么解释器中的地址码是0,而程序计数器中记录下一条指令的地址就是3,紧接着就是4,5,6.....

 

注:说白了,程序计数器就是记录下一条JVM指令的地址。

例如:现有线程1和线程2,假设线程1需要执行的代码量比较大,我们不可能让线程1全部执行完,再执行线程2中的代码,每个线程的执行都会有一个时间片,线程1的时间片用完,就会执行线程2,线程2执行完,接着反过来执行线程1,那么如何知道线程1上1次执行到哪里了,就需要通过程序计数器的记录告知下一条JVM指令的地址,然后根据该地址继续执行后续代码。

 

二、虚拟机栈(java virtual machine stacks)

1、相关概念

栈:就是线程运行所需要的内存空间

栈帧:每个栈由多个栈帧组成,每个栈帧对应每次调用方法时所占用的内存。

每个线程只能有一个活动栈帧,对应着当前正在执行的方法。

如下:

栈就好比是子弹夹,栈帧就好比是子弹,我们可以将子弹一颗一颗放进子弹夹,遵循:先进后出,后进先出。

如上面:我们依次将栈帧1、栈帧2、栈帧3放入到栈中,出的时候顺序是栈帧3、栈帧2、栈帧1.

 

一个栈帧对应一次方法的调用,线程是由一个一个方法组成,每个方法运行时需要的内存就是栈帧。

方法运行时需要内存做什么?方法里面的参数、局部变量、返回地址都是需要占用内存存储的。

2、栈和栈帧的示例演练

package com.wzy.test;/** * 栈和栈帧演练 * */public class StackTest {    public static void main(String[] args) {        method1();    }    public static void method1(){        method2(1,2);    }    public static int method2(int a,int b){        int c=a+b;        return c;    }}

我们在打上三个断点,进入debug调试一下,如下图:

 

逐步调试如下:

Frames下显示三个栈帧,分别为main、method1、method2。这三个方法的执行符合栈的特点,先执行的先入栈。

 

3、栈问题引入分析?

问题1、垃圾回收是否需要栈内存?

不需要,因为虚拟机栈是由一个个栈帧组成的, 在方法执行完毕后,对应的栈帧就会被弹出栈。所以不需要垃圾回收机制去回收栈内存。

 

问题2、栈内存分配得越大越好吗?

不是,因为物理内存都是固定的,如果栈内存设置得越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

如物理内存为500M,栈内存为1M,那么就可以执行500个线程,如果栈内存为2M,那么就只能执行250个线程。

 

问题3、方法内的局部变量是否是线程安全的?

1)、如果方法内的局部变量没有逃离方法的作用范围,则线程是安全的。

2)、如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全的问题。

 

4、栈内存溢出

java.lang.stackOverflowError就是栈内存溢出

导致栈内存溢出的原因:

1)、虚拟机栈中,栈帧过多(无限递归)

2)、每个栈帧所占用的内存过大

 

栈帧过多测试,如下:

package com.wzy.test;/** * 栈内存溢出测试 * */public class StackTest {    public static void main(String[] args) {        method1();    }    public static void method1(){        method1();    }}

上面声明一个方法method1,递归调用method1方法。改造上面的代码,我们声明一个count计数器,在程序执行捕获异常时,看看递归调用多少次method1方法才会导致栈内存溢出

package com.wzy.test;/** * 栈内存溢出测试 * */public class StackTest {    private static int count;    public static void main(String[] args) {        try{            method1();        }catch(Throwable e){            e.printStackTrace();            System.out.println(count);        }    }    public static void method1(){        count++;        method1();    }}

执行结果,如下:

即递归调用method1方法17380次,导致栈内存溢出。

我们可以修改栈内存,设置VM options中参数为-Xss256k,再次测试

 

再次执行

递归调用3555次方法,才出现栈内存溢出

转载地址:http://bmuii.baihongyu.com/

你可能感兴趣的文章
如何自己成功搭建一个SSM框架的WEB项目
查看>>
webservice知识一、SOAP风格的webservice——通过JDK的API发布一个webservice服务和创建一个webservice客户端用于访问该服务
查看>>
JavaEE开发之Spring中的多线程编程以及任务定时器详解(有源码)
查看>>
JS实现2,8,10,16进制的相互转换
查看>>
mysql的存储函数和存储过程
查看>>
nginx和ftp搭建图片服务器
查看>>
solr5.5基础教程
查看>>
Java中的Zip进行多文件的保存
查看>>
微信扫码支付官方下载的demo本地运行时遇到的坑以及对应解决方法
查看>>
关于js中连续click时不执行访问后台请求,当点击停止2s之后,立即发起访问后台的请求的解决方案
查看>>
RESTClient工具访问服务如何传参
查看>>
MySQL中的分组查询与连接查询语句
查看>>
浮点数精度控制方式总结(含mysql和java)
查看>>
并发限流工具类RateLimiter介绍
查看>>
如何配置Tomcat使用https协议
查看>>
linux下安装mariadb以及相关配置
查看>>
Java中的Gzip进行多文件的保存
查看>>
Java中的Future模式原理自定义实现
查看>>
vmware桥接模式下,配置centos的ip地址网关等,搭建局域网服务器
查看>>
xstream练习
查看>>