JVM-02-程序计数器、虚拟机栈

1.程序计数器

在这里插入图片描述
程序计数器处于JVM内存结构的部分

1.1 定义

Program Counter Register 程序计数器(寄存器)

1.2 作用

在这里插入图片描述
以上是一个完整的java程序代码编译执行的流程:
最右侧黄色字体是我们编写的java代码,当我们要运行的时候,这些java代码就会被编译器编译成为能够被jvm读懂的二进制字节码,也叫做jvm指令,即中间绿色字体部分,然后jvm指令会被解释器解释为能被机器听懂的机器码,交给cpu执行。也正是因为java代码与机器码之间有一层jvm指令,这也就保证了java代码能在不同的操作系统中运行。
而程序计数器的作用,就是能够记住下一条jvm指令的执行地址,解释器就可以从程序计数器中拿到下一条jvm指令的执行地址,从而找到这条jvm指令并将其解释为机器码,然后程序计数器再记录下一条jvm指令的地址, 依次类推。
程序计数器的物理实现是寄存器。

1.3 特点

  • 每个线程都有自己的程序计数器,程序计数器是线程私有的
    解释:在线程中有时间片的概念,当一个线程的时间片用完后就会切换到另外一个线程,如果线程1执行代码中就发生了线程切换,程序计数器就会记录线程1中代码执行位置,从而保证线程1获得时间片的时候能继续从中断位置继续执行。
  • 程序计数器是java虚拟机规范中唯一一个不会存在内存溢出的区域

2.虚拟机栈

在这里插入图片描述

2.1 定义和作用

虚拟机栈就是每个线程运行是需要的内存空间,多个线程就有多个虚拟机栈
一个栈内可以看为由多个栈帧组成,一个栈帧就对应着次线程中一个方法的调用,代码是由一个个方法组成的,所以我们将运行时的每个方法运行时所需的内存就叫做一个栈帧,一个方法运行的时候需要给方法的参数、局部变量、返回地址分配内存,所以我们要在方法执行之前预先给其分配好内存。
调用第一个方法时,就给其划分一块栈帧空间,并将其压入虚拟机栈中,当这个方法执行完后,就将该栈帧出栈,释放该方法占用的内存。
在这里插入图片描述
综上,虚拟机栈就是:

  • 每个线程运行时所需要的内存,成为虚拟机栈
  • 每个栈由多个栈帧(Frame)构成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,就是在栈顶部正在执行的方法。

可以进行如下测试:
在这里插入图片描述

2.2 问题辨析

  1. 垃圾回收是否会涉及栈内存?
    答:不会,因为方法执行完后自动就会弹出栈,不需要我们对其进行手动弹出
  2. 栈内存分配越大越好吗?
    答:我们在java代码运行时可以手动为栈指定内存,但并不是越大越好,栈内存越大,反而会使线程数变少,因为物理内存是一定的,线程使用的是栈内存,栈内存越大,而且物理内存一定,所以导致可以运行的线程数变少,增大栈内存只是能够进行更多次的方法递归调用,而不会增强代码运行效率。
  3. 方法内的局部变量是否是线程安全的?
    答:我们只需要看这个局部变量是能够被多个线程共享的还是线程私有的,一个线程对应一个栈,线程内的每一次方法调用都会产生一个栈帧,所以如果两个线程调用同一个方法,它们会创建各自的两个栈,然后创建两个独立的栈帧存放在各自的栈中,所以不会影响各自方法中的局部变量。
    如下面的方法:
    在这里插入图片描述
    在这里插入图片描述
    但是如果将此局部变量设置为static时,情况就变了:
    线程1需要读取static变量,线程2也读取static变量,然后线程1完成自增后,将结果写回给static变量,线程2完成自增后也会将结果写回给static变量,所以就会产生局部变量的线程安全问题。
    当然,在java中是不允许将局部变量设置为static的,因为被static修饰的属性或者方法是被此类共用的。
    在这里插入图片描述

示例:
在这里插入图片描述
上面3个方法中,可能有线程安全问题的方法是:m2,m3
根据上面的分析,m1方法不会有线程安全问题;
m2方法中的参数是传递进来的,所以可能会与其它线程共同修改同一个sb变量,从而导致线程安全问题,例如:
在这里插入图片描述
此例中主线程与new的Thread线程共同修改了sb变量,导致最后主线程出现的sb的内容与预期不相符。
m3方法也会发生线程安全问题,因为其他线程可能拿到m3的返回值并进行修改从而导致结果与预期不符。
所以要看一个变量是否是线程安全的,一是看其是否是局部变量,二是看其是否逃离了方法的作用范围,比如作为返回值就会逃离此方法的作用范围,就会出现线程安全问题。

2.3 栈内存溢出

2.3.1 什么情况下会发生栈内存溢出?

(1)栈帧过多:
如果方法递归调用中没有设置合适的递归终止条件,导致不断调用不断产生新的栈帧,即使栈内存再大也会有用完的一天,从而出现栈内存溢出;
在这里插入图片描述
代码示例:

public class JVMTest {
   private static int count;
   public static void main(String[] args) {
      count = 0;
      try {
         method1();
      }catch (Exception e){
         e.printStackTrace();
      }finally {
         System.out.println(count);
      }

   }

   public static void method1(){
      count++;
      //递归调用自己
      method1();
   }

}

执行结果:

38309
Exception in thread "main" java.lang.StackOverflowError
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	...
	...
	...

即进行了38309次递归调用后发生了StackOverflowError栈内存溢出。
可以通过设置来自定义栈内存大小:
在这里插入图片描述
找到VM options,如果没有,可以点击右侧Modify options添加VM options:
在这里插入图片描述
在这里插入图片描述
将栈内存缩小至256k,点击应用,然后再次运行此程序,运行结果:

3728
Exception in thread "main" java.lang.StackOverflowError
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	at com.hspedu.JVMTest.method1(JVMTest.java:23)
	...
	...
	...

可见由于栈内存缩小,递归次数也减少了。
(2)栈帧过大,这种情况不太容易出现

2.4 线程运行诊断

案例1:cpu占用过多

描述:有些时候如果出现cpu占用过多的时候,比如cpu占用达90%或者更多,一般说明有正在执行的java代码有问题,怎么定位到有问题的线程?

  • nohup命令:让java代码在后台运行

  • top命令:能监测java代码在后台的执行情况以及各项性能数据,top命令只能定位到进程

  • ps命令:查看线程对cpu的占用情况

    • ps H(打印进程数) -eo(规定输出哪些感兴趣的内容) pid(输出进程id),tid(输出线程id),%cpu(输出对cpu的占用);
      ps H -eo pid,tid,%cpu | grep 32655 只输出32655编号的内容
  • jstack 进程id:使用jstack 进程id命令可以获取此进程中所有线程的情况,其输出的线程编号是十六进制的。
    在这里插入图片描述
    该线程的运行状态是RUNNABLE,所以是此线程占用过多的cpu资源,而且还标出了有问题的代码的位置。

先使用top定位哪个进程对cpu占用过高,然后使用ps命令定位进程中哪个线程对cpu占用过高,通过上面一系列命令就可以判断出哪个线程对cpu的占用过高。也可以使用jstack进程id进一步定位到问题代码的位置。

案例2:程序运行很长时间都没有计算结果。

如果程序运行很长时间都没有计算结果,那么可能就是程序中发生了死锁而导致的,如何排查这种线程死锁的问题呢?
同样使用上一个案例中的命令来定位问题。
使用jstack 进程id就可以查看某个进程中是否有死锁发生:
在这里插入图片描述
Found one java-level deadlock即发现了一个死锁。

2.5 本地方法栈

在这里插入图片描述
本地方法不是由java代码编写的代码,java编写的代码有限制,有时候不能与操作系统底层方法打交道,而java代码可以间接通过本地方法来调用底层方法,本地方法运行的时候使用的内存就是本地方法栈被native关键字修饰的方法就是本地方法,例如Object类中的clone方法:
protected native Object clone() throws CloneNotSupportedException;
其被native修饰,并且没有方法体,其底层实现是由c或者c++语言实现的,java代码通过调用clone方法去间接调用c或者c++的方法实现。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值