【随笔】CLR:.net的类型,内部到底长啥样?
前言
一提到.net的类型,首当其冲的就是“引用类型”、“值类型”;我们在面试中,也会经常被问“来说说值类型和引用类型。。。。”,这时候第一反应就是:“哎呀,这还不简单,值类型是传递的值的copy,值对象存储在栈中;引用类型传的是引用,对该引用对象的修改都会影响到原本的内容,引用对象存储在堆中”,额。。。往往第一时间想到此处,似乎就“词穷”了,不知道你有木有这样的感觉。哈哈哈哈!但是真理往往没那么简单 - -!
引用类型(Reference Type)
引用类型和值类型其实有一个很大的、并且很明显,但容易被大家忽视的区别 “引用类型的对象是受GC控制的,而值类型的对象则不受GC控制”。
“不为人知”的开销
其实CLR针对于引用类型对象的创建,会额外有2个字段的开销,一个是同步块索引(SBI),另一个是方法表指针(MT Pointer)。每个引用类型的对象,在内容中都会额外创建这2个对象
大家看到这2个名词的时候,似乎觉得既熟悉又陌生,不过其实也没那么玄奥,让我们往下看↓(下面的截图中的地址,可能前后对不上,因为我本地重启过)
让我们先创建一个简单的Person类:
public class Person
{
public int Id { get; set; }
}
然后打个断点:
接下来我们按F5开启调试,并且打开3个神器窗口
在解释之前呢,我想说一句大家都知道的一句话 “通过C#编写的cs文件,都是经过csc.exe编译器,编译成IL中间语言(.exe, .dll),然后由JIT即时转化成本机代码执行”,就目前而言,最终就是汇编代码了,这也是为啥要打开这3个窗口来剖析的原因。囧~~~
我们可以通过内存窗口,看看我们创建的对象,在内存中到底是怎么布局的。
当断点执行到这个地方的时候,在内存中已经创建了Person对象,可以通过寄存器指令ECX所对应的值02585DCC(都是16进制的),贴到内存窗口中
回到我们上面说的,一个引用类型的对象,除了方法表指针,应该还有一个同步块索引,那SBI在哪里呢?哈哈,其实就在方法表指针地址的上面,鼠标滑轮滚一滚就到了
然后继续执行你的程序,给id赋值。
至此,大家通过以上的剖析,知道了引用类型对象在内存中长成什么样了
细心的同学应该发现了,SBI永远是在MT的上面(偏移负4个字节,x32位系统)
那最后我们这个对象的内存布局,从宏观上看应该是(以32位系统为例):
看到这里,小伙伴们不放按照我上面的步骤,动手试一下,会加深自己的理解。
不过我相信,有的小伙伴对上面的MT和SBI并没有直观的印象,这2个家伙有啥用的,为啥CLR在创建引用类型的对象的时候会用到它呢,且看下面的代码
SBI
下面是大家常见的,锁机制的代码
我们先快速定位到p1对象的SBI,如下图
由上可以得出,CLR中的lock机制,其实是通过对象的SBI来实现的,上锁则设置成1(其实这个1是,当前执行线程的id号,你可以试试上书lock代码外面包一层Task,你会发现可能不是1了,不要误解凡是上锁的地方要么1,要么0),lock结束则重置成0,这就是同步块索引的用处之一,没错,是之一,他还没有其他用途(一些大家平常挂在嘴边,但是很少去深挖的)。
看到这个案例,不知道大家有没有想起一个面试题,为啥lock不能锁值类型对象,其实本质原因是:值类型对象是没有SBI的,从而CLR无法实现lock(下面说值类型的时候,会做详细的阐述),在代码的编译阶段vs就会给你报错了
MT
且看如下代码
细心的同学在内存中,可以看到,p1和p2的MT指向的是相同的地址,这也是CLR优化的地方,因为两者的对象类型都是Person.
通过 LLDB从另一个角度来细细看下Person对象
大家也许对lldb陌生,不过应该对windbg不陌生,lldb是和windbg一样,可以抓dump,分析内存的一个组件,在core里面,我们大部分情况会把我们的app部署到linux,或者容器中,这个时候
windbg是没法用的(windows),附上lldb相关链接:
https://docs.microsoft.com/en-us/dotnet/framework/tools/sos-dll-sos-debugging-extension#commands
https://lldb.llvm.org/lldb-gdb.html
根据上述结果,我们可以找到Person对象的MT地址,我们看看具体里面有些什么,执行dumpmt -md MT的地址
从图中我们得知,一个引用类型的对象的MT指针,所指向的内容包含:EEClass、token、size、以及很重要要的MethodDesc Table(方法表)
细心的同学会发现,方法表除了包含Person类本身的方法,还有它的基类方法。方法表是程序运行时供CLR选择对应的方法进行调用的。
上述信息有一列JIT,它有3个状态,分别解释下:
PreJIT:该方法被NGEN(Native Image Generator) 编译了。
JIT:该方法,CLR在runtime的时候被JIT即时编译了。
NONE:该方法,暂时还没有被编译。(回到我们上面的代码,我们只对id做了set,并没有get,所以get_Id()的JIT状态是NONE)。
接下来,我们看下Person类的构造函数所对应的本机代码,我们执行下:dumpmd 方法描述地址
继续执行:u codeaddress
但是当前的插件版本,似乎不支持u命令,那我们查下当前sos plugin支持的命令有哪些
u命令不行,那我们用它的全称去尝试下 clru 方法地址:
我们当前看到的就是,Person类的构造函数,所对应的代码了。
我们在看看Person类的构造函数,所对应的IL代码:
值类型(Value Type)
值类型对象分配的地方不是在Managed Heap(引用类型),而是在当前程序所在的执行线程的Stack里(thread stack)。
我们先创建一个简单的结构体:
public struct Line
{
public int Length { get; set; }
}
然后,老样子,开启F5调试,打开我们的3大神器窗口:
ps:细心的同学有没有发现,这个截图里的地址,是16位长度的16进制来显示的显示的,上面讲引用类型的时候,截图是8位长度的16进制来显示的。
其实上面的环境是x86, 下面的是x64位系统:以64位系统举例,16进制的F,表示成二进制则是1111,那么想表达64位的话,16进制的长度就为64%4=16;同理32位系统,想要表达32位的话,16进制的长度就为32%4=8。
有的同学在自己vs行试的时候,也许不是上图的16长度,因为我为了使用lldb,方便我剖析问题,我创建了一个.net core 2.0的console项目。在调试的时候,vs会默认启动你本机安装的dotnet.exe程序。我的电脑安装列表如下
我装的都是64位的,如果你想vs能调试32位的话(像我当前的情况的话,你以32位环境调试的话,会直接crash的),你需要安装dotnet的32位版本的sdk才行,为什么需要这样,详情戳这里。
不过。。。。64位调试下,也没啥影响,我们继续
哈哈,会对比的同学,看到这里,应该发现了2点不一样了:
1. 值类型对象并没有MT和SBI
2. 值类型对象的对象存储是从 高地址位--->低地址位
boxing
经过上面的剖析,我们引出我们经常遇到的一个问题“装箱(boxing)”,大家都知道装箱会带来性能问题(个人觉得,看情况,毕竟现在硬件这么牛逼,某些场景下没必要吹毛求疵),但是大家思考下,性能的问题,具体体现在哪里呢?
带着问题,我们再改下我们的代码:
通过IL代码,很容易看出,我们把obj装箱了,那我们看看装箱后的对象obj长什么样:
根据上面的例子,其实已经验证了,值类型对象经过boxing之后,CLR在内存中,其实创建了一个引用类型对象,然后把值对象的值copy过来,产生MT和SBI。所以装箱的性能损耗也显而易见了。
并且,值类型对象一旦boxing之后,新产生的obj,它的释放,则交由GC来控制。这也给GC间接增加了压力(还是之前提到的那句话,现在硬件这么牛逼了,某些场景下,不要吹毛求疵,囧~~~~)
用lldb进一步验证boxing
我们不妨用lldb再来深入验证下,新产生的对象,到底存不存在,我们稍微调整下我们的代码,方便我们做验证:
此时,我们当前的内存里,应该是没有obj对象的(被注释了,当然没有了。。囧),l1也并没有被装箱,然后我们通过lldb的dumpheap -stat指令来看下,托管堆里的对象有哪些。
强调下哈,dumpheap指令看的是托管堆对象、托管堆对象、托管堆对象,重要的事情说三遍,所以值类型的对象,不应该也不可能出现在该指令下,向下看↓↓↓↓↓↓
由上图我们看到,其实并没有任何和Line相关的对象信息,到当前位置,一切的现象都是十分正确的。
接下来,我们改下代码,进行boxing,我们再来对比下:
上图可以得到,我圈起来,其实就是我们的obj对象,它是由l1 boxing而来的,类型为Line,我们继续执行下我们上面执行过的指令,看看这个boxing而来的对象,内部到底长什么样!?
总结
至此,对于.net的类型,是不是又多了一层认知,除了知道2者的传值方式的不同、直接继承类的不同、Compare的不同,还有他内存分布的不同
相信小伙伴们,以后再回答这类问题的时候,又多了一个关注点。
ps:文章中,有很多步骤并没有细说,比如docker中怎么使用lldb,lldb指令的详解,3个vs窗口的使用等等,都是一笔带过,后面有时间再补起来,到时候会在文章中加link方便跳转。
文章中有些地方,自己也不是理解的很透,比如说汇编(大学没学好,基本上还给老师了,囧),有不足以及错误的地方欢迎大家讨论。
【随笔】CLR:.net的类型,内部到底长啥样?的更多相关文章
- 漫画赏析:Linux 内核到底长啥样(转)
知乎链接:https://zhuanlan.zhihu.com/p/51679405 来自 http://TurnOff.us 的漫画 “InSide The Linux Kernel” 本文转载自: ...
- 再深入一点|binlog和relay-log到底长啥样?
上一篇mysql面试的文章之后收到不少朋友的意见,希望深入讲讲复制.日志的格式这些,今天,我们就来深挖一下mysql的复制机制到底有哪一些,以及binlog和relay-log的结构到底是什么样子的. ...
- 不知道Linux内核到底长啥样?这幅漫画让你秒懂!
下面给大家分享一个[超全2020Linux学习教程],点击链接免费领取哦~ https://www.magedu.com/?p=84301&preview=true
- Linux 内核到底长啥样
目录 一.简介 二.结构 地基 地面层 进程表 http进程 21进程 22进程 到文件系统 定时任务 管道 411进程 跃层 一.简介 今天,我来为大家解读一幅来自 TurnOff.us 的漫画 & ...
- 用漫画了解Linux内核到底长啥样
一个执着于技术的公众号 原文链接:http://985.so/hRL6 往期精彩 ◆ 干货 | 给小白的Nginx10分钟入门指南 ◆ 什么是集群?看完这篇你就知道啦! ◆ 干货 | Linux ...
- 【笔记】js Function类型 内部方法callee
运用function实现阶乘 以往的做法是如下的 function factorial(num){ if(num <= 1){ return 1; }else{ return num * fac ...
- 深入研究 Mini ASP.NET Core(迷你 ASP.NET Core),看看 ASP.NET Core 内部到底是如何运行的
前言 几年前,Artech 老师写过一个 Mini MVC,用简单的代码告诉读者 ASP.NET MVC 内部到底是如何运行的.当时我研究完以后,受益匪浅,内心充满了对 Artech 老师的感激,然后 ...
- 当程序执行一条查询语句时,MySQL内部到底发生了什么? (说一下 MySQL 执行一条查询语句的内部执行过程?
先来个最基本的总结阐述,希望各位小伙伴认真的读一下,哈哈: 1)客户端(运行程序)先通过连接器连接到MySql服务器. 2)连接器通过数据库权限身份验证后,会先查询数据库缓存是否存在(之前执行过相同条 ...
- 硬刚Google ,这家小公司的增长团队长啥样
背景: AdRoll 是一家主打重定向广告(Retargeting)服务的技术公司,基于用户浏览记录等信息,为广告主提供几乎瞬时的广告位购买服务,当前估值15.5亿美元.吊打谷歌, AdRoll 已经 ...
随机推荐
- PHP setcookie 网络函数
setcookie - 发送 Cookie. 语法: setcookie ( string $name [, string $value = "" [, int $expire = ...
- linux shell通过curl获取HTTP请求的状态码
直接上代码: curl -I -m -o /dev/null -s -w %{http_code} www.baidu.com 参数说明: -I 仅测试HTTP头 -m 10 最多查询10s -o / ...
- selenium 优化 提升性能
结果: 用时:7.200437545776367s用时:5.909301519393921s headless用时:4.924464702606201s headless\phone用时:4.9358 ...
- image-webpack-loader包安装报错解决
在家里安装这个包,总是报错安装失败,换成最快的淘宝镜像也是如此,先卸载重新安装亦是如此,于是想到了原因,到了公司,公司的网是可以连接国外的,安装成功了! 也就是说,需要翻墙才可以装成功.
- sockjs+stomp的websocket插件
/** * 依赖文件sockjs.js.stomp.js * */ ;!(function (window) { 'use strict' let WS = function () { //保存所有的 ...
- RV32FDQ/RV64RDQ指令集(1)
Risc-V架构定义了可选的单精度浮点指令(F扩展指令集)和双精度浮点指令(D扩展指令集),以及四精度浮点指令集(Q扩展指令集).Risc-V架构规定:处理器可以选择只实现F扩展指令子集而不支持D扩展 ...
- YYLable 的使用 以及注意点
NSString *title = @"不得不说 YYKit第三方框架确实很牛,YYLabel在富文本显示和操作方面相当强大,尤其是其异步渲染,让界面要多流畅有多流畅,这里我们介绍下简单的使 ...
- 剑指offer 16:反转链表
题目描述 输入一个链表,反转链表后,输出新链表的表头. 解题思路 单链表原地反转是面试手撕代码环节非常经典的一个问题.针对一般单链表,反转的时候需要操作的是当前节点及与之相邻的其他两个节点.因而需要定 ...
- [b0030] python 归纳 (十五)_多进程使用Pool
1 usePool.py #coding: utf-8 """ 学习进程池使用 multiprocessing.Pool 总结: 1. Pool 池用于处理 多进程,并不 ...
- python字符减运算
在C语言等高级语言中,字符之间的减运算都是支持的,但是python不然,在python中直接进行字符减运算是不被允许的. >>> print('c'-'a') Traceback ( ...