深入理解以DEBUG方式线程的底层运行原理


Posted in Java/Android onJune 21, 2021
目录
  • 一、Java 运行时数据区域
  • 二、用 DEBUG 的方式看线程运行原理
  • 三、线程运行原理详细图解
  • 四、用 DEBUG 的方式看多线程运行原理

 

一、Java 运行时数据区域

友情提示:这部分内容可能大部分同学都有一定的了解了,可以跳过直接进入下一小节哈。

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。

全文我们都将以 JDK 7 的运行时数据区域为例:

深入理解以DEBUG方式线程的底层运行原理

先简单解释下线程共享和线程私有是啥意思。

所谓线程私有,通俗来说就是每个线程都会创建一个属于自己的东西,每个线程之间的这块私有区域互不影响,独立存储。比如程序计数器就是线程私有的,每个线程都会拥有一个属于自己的程序计数器,互不干涉。

线程共享就没啥好说的,简单理解为公共场所,谁都能去,存储的数据所有线程都能访问。

OK,然后我们来逐个分析下每个区域都是用来存储什么的。当然了,这里不会做太多详细的说明,不然会使文章显得非常臃肿,在理解本文的基础上能够让大家对各个区域有基本的认知就好了。

首先来看一下线程共享的两个区域:

1)Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

2)方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

很多人习惯的把方法区称为永久代(Permanent Generation),但实际上这两者并不等价。通俗来说,方法区是一种规范,而永久代是 HotSpot 虚拟机实现这个规范的一种手段,对于其他虚拟机(比如 BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。

另外,对于 HotSpot 虚拟机来说,它在 JDK 8 中完全废弃了永久代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Meta-space)来代替,把 JDK 7 中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

再来看看线程私有的三个区域:

1)虚拟机栈(Java Virtual Machine Stacks)其实是由一个一个的栈帧(Stack Frame)组成的,一个栈帧描述的就是一个 Java 方法执行的内存模型。也就是说每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法的返回地址等信息。

深入理解以DEBUG方式线程的底层运行原理

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,当然,出栈的顺序自然是遵守栈的后进先出原则的。

栈帧的概念在接下来的原理解析部分非常重要,各位务必搞懂哈。

2)本地方法栈(Native Method Stack)和上面我们所说的虚拟机栈作用基本一样,区别只不过是本地方法栈为虚拟机使用到的 Native 方法服务,而虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务。

这里解释一下 Native 方法的概念,其实不仅 Java,很多语言中都有这个概念。

"A native method is a Java method whose implementation is provided by non-java code."

就是说一个 Native 方法其实就是一个接口,但是它的具体实现是在外部由非 Java 语言写的。所以同一个 Native 方法,如果用不同的虚拟机去调用它,那么得到的结果和运行效率可能是不一样的,因为不同的虚拟机对于某个 Native 方法都有自己的实现,比如 Object 类的 hashCode 方法。

这使得 Java 程序能够超越 Java 运行时的界限,有效地扩充了 JVM。

3)程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过轮流分配 CPU 时间片的方式来实现的,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。

那么程序计数器里存的到底是什么东西呢?

《深入理解 Java 虚拟机:JVM 高级实践与最佳实战 - 第 2 版》给出了答案:如果线程正在执行的是一个 Java 方法,程序计数器中记录的就是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

 

二、用 DEBUG 的方式看线程运行原理

接下来,我们就通过 DEBUG 这段代码来看下线程的运行原理:

深入理解以DEBUG方式线程的底层运行原理

上述代码的逻辑非常简单,main 方法调用了 method1 方法,而 method1 方法又调用了 method2 方法。

看下图,我们打了一个断点:

深入理解以DEBUG方式线程的底层运行原理

OK,以 DEBUG 的方式运行 Test.main(),虽然这里我们没有显示的创建线程,但是 main 函数的调用本身就是一个线程,也被称为主线程(main 线程),所以我们一启动这个程序,就会给这个主线程分配一个虚拟机栈内存。

深入理解以DEBUG方式线程的底层运行原理

上文我们也说了,虚拟机栈内存其实就是个壳儿,里面真正存储数据的,其实是一个一个的栈帧,每个方法都对应着一个栈帧。

所以当主线程调用 main 方法的时候,就会为 main 方法生成一个栈帧,其中存储了局部变量表、操作数栈、动态链接、方法的返回地址等信息。

各位现在可以看看 DEBUG 窗口显示的界面:

深入理解以DEBUG方式线程的底层运行原理

左边的 Frames 就是栈帧的意思,可以看见现在主线程中只有一个 main 栈帧;

右边的 Variables 就是该栈帧存储的局部变量表,可以看到现在 main 栈帧中只有一个局部变量,也就是方法参数 args。

接下来 DEBUG 进入下一步,我们先来看看 DEBUG 界面上的每个按钮都是啥意思,总共五个按钮(已经了解的各位可以跳过这里):

1)Step Over:F8

深入理解以DEBUG方式线程的底层运行原理

程序向下执行一行,如果当前行有方法调用,这个方法将被执行完毕并返回,然后到下一行

2)Step Into:F7

深入理解以DEBUG方式线程的底层运行原理

程序向下执行一行,如果该行有自定义方法,则运行进入自定义方法(不会进入官方类库的方法)

3)Force Step Into:Alt + Shift + F7

深入理解以DEBUG方式线程的底层运行原理

程序向下执行一行,如果该行有自定义方法或者官方类库方法,则运行进入该方法(也就是可以进入任何方法)

4)Step Out:Shift + F8

深入理解以DEBUG方式线程的底层运行原理

如果在调试的时候你进入了一个方法,并觉得该方法没有问题,你就可以使用 Step Out 直接执行完该方法并跳出,返回到该方法被调用处的下一行语句。

5)Drop frame

深入理解以DEBUG方式线程的底层运行原理

点击该按钮后,你将返回到当前方法的调用处重新执行,并且所有上下文变量的值也回到那个时候。只要调用链中还有上级方法,可以跳到其中的任何一个方法。

OK,我们点击 Step Into 进入 method1 方法,可以看到,虚拟机栈内存中又多出了一个 method1 栈帧:

深入理解以DEBUG方式线程的底层运行原理

再点击 Step Into 直到进入 method2 方法,于是虚拟机栈内存中又多出了一个 method2 栈帧:

深入理解以DEBUG方式线程的底层运行原理

当我们 Step Into 走到 method2 方法中的 return n 语句后,n 指向的堆中的地址就会被返回给 method1 中的 m,并且,满足栈后进先出的原则,method2 栈帧会从虚拟机栈内存中被销毁。

深入理解以DEBUG方式线程的底层运行原理

然后点击 Step Over 执行完输出语句(Step Into 会进入 println 方法,Force Step Into 会进入 Object.toString 方法)

至此,method1 的使命全部完成,method1 栈帧会从虚拟机栈内存中被销毁。

深入理解以DEBUG方式线程的底层运行原理

最后再往下走一步,main 栈帧也会被销毁,这里就不再贴图了。

 

三、线程运行原理详细图解

上面写了这么多,其实也就是教会了大家栈帧这个东西,接下来我们通过图解的方式,来带大家详细看看线程运行时,Java 运行时数据区域的各种变化。

首先第一步,类加载。

《深入理解 Java 虚拟机:JVM 高级实践与最佳实战 - 第 2 版》中是这样解释类加载的:虚拟机把描述类的数据从 Class 文件(字节码文件)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

而加载进来的这些字节码信息,就存储在方法区中。看下图,这里为了各位理解方便,我就不写字节码了,直接按照代码来,大家知道这里存的其实是字节码就行

深入理解以DEBUG方式线程的底层运行原理

主线程调用 main 方法,于是为该方法生成一个 main 栈帧:

深入理解以DEBUG方式线程的底层运行原理

那么这个参数 args 的值从哪里来呢?没错,就是从堆中 new 出来的:

深入理解以DEBUG方式线程的底层运行原理

而 main 方法的返回地址就是程序的退出地址。

再来看程序计数器,如果线程正在执行的是一个 Java 方法,程序计数器中记录的就是正在执行的虚拟机字节码指令的地址,也就是说此时 method1(10) 对应的字节码指令的地址会被放入程序计数器,图片中我们仍然以具体的代码代替哈,大家知道就好

深入理解以DEBUG方式线程的底层运行原理

OK,CPU 根据程序计数器的指示,进入 method1 方法,自然,method1 栈帧就被创建出来了:

深入理解以DEBUG方式线程的底层运行原理

局部变量表和方法返回地址安顿好后,就可以开始具体的方法调用了,首先 10 会被传给 x,然后走到 y 被赋值成 x + 1 这步,也就是程序计数器会被修改成这步代码对应的字节码指令的地址:

深入理解以DEBUG方式线程的底层运行原理

走到 Object m = method2(); 这一步的时候,又会创建一个 method2 栈帧:

深入理解以DEBUG方式线程的底层运行原理

可以看到,method2 方法的第一行代码会在堆中创建一个 Object 对象:

深入理解以DEBUG方式线程的底层运行原理

随后,走到 method2 方法中的 return n; 语句,n 指向的堆中的地址就会被返回给 method1 中的 m,并且,满足栈后进先出的原则,method2 栈帧会从虚拟机栈内存中被销毁:

深入理解以DEBUG方式线程的底层运行原理

根据 method2 栈帧指向的方法返回地址,我们接着执行 System.out.println(m.toString()) 这条输出语句,执行完后,method1 栈帧也被销毁了:

深入理解以DEBUG方式线程的底层运行原理

再根据 method1 栈帧指向的方法返回地址,发现我们的程序已走到了生命的尽头,main 栈帧于是也被销毁了,就不再贴图了。

 

四、用 DEBUG 的方式看多线程运行原理

上面说的是只有一个线程的情况,其实多线程的原理也差不多,因为虚拟机栈是每个线程私有的,大家互不干涉,这里我就简单的提一嘴。

分别在如下两个位置打上 Thread 类型的断点:

深入理解以DEBUG方式线程的底层运行原理

然后以 DEBUG 方式运行,你就会发现存在两个互不干涉的虚拟机栈空间:

深入理解以DEBUG方式线程的底层运行原理

当然,使用多线程就不可避免的会遇到一个问题,那就是线程的上下文切换(Thread Context Switch),就是说因为某些原因导致 CPU 不再执行当前的线程,转而执行另一个线程。

导致线程上下文切换的原因大概有以下几种:

1)线程的 CPU 时间片用完

2)发生了垃圾回收

3)有更高优先级的线程需要运行

4)线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当线程的上下文切换发生时,也就是从一个线程 A 转而执行另一个线程 B 时,需要由操作系统保存当前线程 A 的状态(为了以后还能顺利回来接着执行),并恢复另一个线程 B 的状态。

这个状态就包括每个线程私有的程序计数器和虚拟机栈中每个栈帧的信息等,显然,每次操作系统都需要存储这么多的信息,频繁的线程上下文切换势必会影响程序的性能。

以上就是深入理解以DEBUG方式线程的底层运行原理的详细内容,更多关于DEBUG方式线程运行原理的资料请关注三水点靠木其它相关文章!

Java/Android 相关文章推荐
Java中常用解析工具jackson及fastjson的使用
Jun 28 Java/Android
SpringBoot读取Resource下文件的4种方法
Jul 02 Java/Android
Java实现聊天机器人完善版
Jul 04 Java/Android
IDEA2021.2配置docker如何将springboot项目打成镜像一键发布部署
Sep 25 Java/Android
MyBatis-Plus 批量插入数据的操作方法
Sep 25 Java/Android
Mybatis是这样防止sql注入的
Dec 06 Java/Android
关于ObjectUtils.isEmpty() 和 null 的区别
Feb 28 Java/Android
详解Flutter和Dart取消Future的三种方法
Apr 07 Java/Android
spring IOC容器的Bean管理XML自动装配过程
May 30 Java/Android
详解Spring Security如何在权限中使用通配符
Jun 28 Java/Android
阿里面试Nacos配置中心交互模型是push还是pull原理解析
Jul 23 Java/Android
spring boot实现文件上传
Aug 14 Java/Android
浅谈什么是SpringBoot异常处理自动配置的原理
SpringAop日志找不到方法的处理
详解SpringBoot异常处理流程及原理
Java新手教程之ArrayList的基本使用
Java各种比较对象的方式的对比总结
Jun 20 #Java/Android
Java Optional<Foo>转换成List<Bar>的实例方法
Jun 20 #Java/Android
详解Java实践之适配器模式
You might like
PHP入门学习的几个不错的实例代码
2008/07/13 PHP
有关php运算符的知识大全
2011/11/03 PHP
php生成zip压缩文件的方法详解
2013/06/09 PHP
php使用GD实现颜色渐变实例
2015/06/02 PHP
Zend Framework教程之模型Model用法简单实例
2016/03/04 PHP
PHP的Laravel框架中使用AdminLTE模板来编写网站后台界面
2016/03/21 PHP
php 下 html5 XHR2 + FormData + File API 上传文件操作实例分析
2020/02/28 PHP
javascript判断ie浏览器6/7版本加载不同样式表的实现代码
2011/12/26 Javascript
JS无限极树形菜单,json格式、数组格式通用示例
2013/07/30 Javascript
js 数组去重的四种实用方法
2014/09/09 Javascript
jQuery实现鼠标滑向当前图片高亮显示并且其它图片变灰的方法
2015/07/27 Javascript
javascript实现网页背景烟花效果的方法
2015/08/06 Javascript
java中String类型变量的赋值问题介绍
2016/03/23 Javascript
js中遍历对象的属性和值的方法
2016/07/27 Javascript
Javascript实现汉字和拼音互转的终极方案
2016/10/19 Javascript
vue使用自定义icon图标的方法
2018/05/14 Javascript
Angular5中调用第三方库及jQuery的添加的方法
2018/06/07 jQuery
详解webpack的proxyTable无效的解决方案
2018/06/15 Javascript
如何实现小程序tab栏下划线动画效果
2019/05/18 Javascript
js实现简单音乐播放器
2020/06/30 Javascript
Python greenlet实现原理和使用示例
2014/09/24 Python
Python import用法以及与from...import的区别
2015/05/28 Python
django主动抛出403异常的方法详解
2019/01/04 Python
Python3.7 读取 mp3 音频文件生成波形图效果
2019/11/05 Python
python判断两个序列的成员是否一样的实例代码
2020/03/01 Python
PyQt5事件处理之定时在控件上显示信息的代码
2020/03/25 Python
Python爬取数据并实现可视化代码解析
2020/08/12 Python
python中PyQuery库用法分享
2021/01/15 Python
加拿大领先家居家具网上购物:Aosom.ca
2020/05/27 全球购物
简述你对Statement,PreparedStatement,CallableStatement的理解
2013/03/25 面试题
DTD的含义以及作用
2014/01/26 面试题
致全体运动员广播稿
2014/02/01 职场文书
美德少年事迹材料500字
2014/08/19 职场文书
被告代理词范文
2015/05/25 职场文书
纪检监察立案决定书
2015/06/24 职场文书
面试分析分布式架构Redis热点key大Value解决方案
2022/03/13 Redis