您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

时间:2020-09-25 15:27:15  来源:  作者:

当今流行的编程语言,大多具备垃圾回收(Garbage Collection,以下简称GC)功能。它能够将不再使用的内存区域收回并重新分配。

这一功能可以说,将程序员的注意力从内存的分配/释放工作中解放了出来,可以专注于业务逻辑的实现。但这并不意味着说,程序员在写代码的时候就可以无所顾忌了。

因为他们面对的环境里,资源毕竟是有限的,而GC也不能包办一切工作。尤其是程序需要运行时性能的时候,对代码的编写就有更高的要求了。

而在优化程序性能时,也不能凭着猜想去实施,这就需要对编程语言的内存布局与管理有清楚的了解。这样才能做到有的放矢,事半而功倍。

下面我们先从编译技术的基本概念说起。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

 

编译技术

编译器方式,这种方式是将代码经过预处理、编译、汇编、链接之后,得到一个可执行文件。这个文件里面包含的都是二进制的机器指令,它的优点是程序执行速度快,能将硬件性能充分发挥出来。

它的缺点则是编译过程需要耗费时间,程序修改之后必须重新编译才能使用。在早些年硬件性能不高的时候,编译一个大型的程序需要一两个小时是很平常的事。

此类语言的典型代表是C/C++,以及现在十分流行的Go语言

解释器方式,程序代码直接运行在一个解释器中,没有编译的过程。优点则是可以立即运行,且可移植性好,代码编写一次即可在任何平台上运行,而且预期效果也一样。而编译器方式则要麻烦的多,它需要为每一个平台单独编译一次。

不过解释器方式的缺点也同样明显,就是它的性能受限。毕竟是隔着一层解释器去执行,远远比不了翻译成机器指令的二进制可执行文件。

此类语言的代表则有Python、ruby、phpJAVAscript等。可以认为,脚本类语言都属于解释器方式执行。

中间代码方式,这是一种折衷式的方案,它会先对代码有一次编译过程,但不是编译成可执行文件,而是一份中间代码。然后这份中间代码会放到一个虚拟机里去执行。以这样的方式既获得了良好的可移植性,也能够拥有高于解释器的速度。

java语言即是最佳代表。它会先编译出一个字节码文件,然后Java Virtual machine(JVM)通过读取字节码来运行程序。

微软的.NET也是类似的结构,它使用的是Common Language Runtime(CLR),以此支持多种语言。例如C#、VB.net等。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

 

基础知识

不论一个程序用何种语言编写,它的运行时内存布局都是一致的。我们先从一个程序的三种基本内存区域说起。

静态区:这个区域主要存放的是程序的全局变量、常量数据,以及编译成二进制指令的代码。可以看到,这个区域存放的,主要是贯穿于程序整个生命周期所要使用到的数据与指令。

栈区:熟悉数据结构的朋友们都知道,栈(stack)是一个后入先出(LIFO)的队列。在程序运行中,它用来实现函数的调用。程序执行函数调用时,会在栈上依次压入参数,局部变量、返回位置等,执行完成后再依次将数据出栈。所以,栈上的数据都是临时性的,只在调用时可用。

堆区:所有动态申请的内存都从堆区分配。在使用C/C++语言时,程序员对待内存的申请与释放就必须特别小心,一个疏忽就会造成内存泄漏。而后来的java、C#等,语言内置了GC技术,情况相对改善,但也要养成良好的编程习惯。

对于程序来说,静态区和堆区都是全局存在的,即所有线程共享这二者。而栈区则是为每个线程单独准备一个,这一点程序员要记住。因为栈区的数据在函数调用之后就会失效,如果还引用栈区的数据,则会产生不可预料的问题。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

程序运行时内存布局

OOP语言的内存结构

因为现在市场上面向对象编程语言(OOP)占据主流地位,所以接下来的讨论也将以OOP语言的典型内存结构进行讲解。我们了解清楚对象的存储区域,方法的调用之后,就会更加明白编程时应当注意哪些方面。

我们以使用较为广泛的Java语言进行说明,先要厘清一个总是争论不休的问题。就是Java语言中究竟有没有指针?

Java中的一系列逻辑功能,都是通过对象的间的消息传递和方法调用来实现的。对象是实现功能的最小单元,而一个对象是怎么来的,它存放在哪里?

先看一段派生对象的代码:

 MyCar one = new MyCar()

Java语言中的new的实质是动态创建内存,用以存放对象实例。根据上节的知识,我们知道new操作的结果是从堆区申请了一块内存,它将这块内存的地址返回,变量one就可以通过这个地址实现对象的操作了。

所以,变量one中存储的不是对象本身,而是指向对象所在内存的地址。好吧,简单说就两个字:指针。在Java的术语体系里,它也叫引用。不过不管怎么称呼,这种内存结构就是典型的指针式操作。

既然我们知道Java语言中所有的对象都生成在堆区,那么需要注意之处就来了:堆区的存储空间是有限的,不能将运行时环境想象成内存无限的场景,要对自己使用的对象所占空间做到心中有数。

接下来还要注意的,就是对象复制的操作,示例代码:

 MyCar one = new MyCar()
 MyCar two = one; one.SetSpeed(100);
 two.SetSpeed(0);

有了上面的知识,我们清楚地知道,MyCar two = one;这条语句并没有复制一个对象给two变量,它和one指向的都是同一个对象实例。所以代码执行的结果,就是这辆车以百公里时速狂奔的下一秒就减速到零,想想都挺吓人的吧。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

 

方法表与属性

那么,对象的方法代码是存放在哪里呢?答案是在静态区。因为方法是可以在编译时就形成二进制指令的,因此编译后放在静态区就可以了。

类的信息是存放在静态区的,它会包含一张方法表(有的语言中也称为虚函数表)。方法表中的方法名实际上是一个函数指针,它在运行时是指向静态区的方法代码的。有了方法表,OOP语言就可以实现多态机制了。

这种方式可以节省程序存储空间,所以从本质上说,所有的对象实例都是在共用同一段方法代码。只是在调用时通过压入不同的参数以实现对象个性化的操作。

对象的属性变量又是存放在哪里?答案是在堆区,所以我们现在知道,一个对象实例里,属性变量的大小决定了它实际占用的存储空间。

需要注意的事项又来了:不要在类的声明中,将属性变量定义的过大。例如为了图方便,定义个超大的数组。这样带来的问题,一是会影响对象生成的效率,因为动态分配一段大内存是很耗时的;二是会导致内存空间急剧减少。

GC的运行并不是实时清理的,它会有延时判断策略,那么大量闲置的内存还来不及回收,新的对象又得不到可用空间,这只会降低程序的运行时性能了。

通过方法表,继承结构也得以实现。对于超类中的方法,子类中无需再存储相同的副本,它只要在自己的方法表中增加一条指向超类的方法引用即可。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

对象通过方法表调用方法

GC会回收哪些对象实例?

通过上述几节的知识,我们知道GC要处理的肯定是在堆区上动态分配的对象实例。那是不是有了这个原则,我们就可以高枕无忧了呢?并不是,这要从GC的回收原理上说起。

GC的实现基础,必定是通过引用计数来判定对象是否被使用,未被使用的对象则会进入回收工作中。但是如果对象变量是在静态区或者栈区,那么这个对象永远都不会被回收。

静态区的对象,在Java中就是以static定义的类变量。程序员对此一定要心中有数,一定要记住类变量生成的对象,它的生命周期是和程序本身一样的。

而栈上所引用的对象,它的存活周期则和方法调用一致。也就是说如果方法退出,那么期间所产生的对象不再使用了,是会被回收的。

在多线程环境中,程序员要注意,如果一个方法是长期后台运行的,则不要进行频繁地创建对象的工作,以避免内存无法回收。

程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

被栈区和静态区引用的对象是不会被回收的

总结

经过了解编程语言的内存布局与管理,我们发现还是有很多细节处不注意的话,很容易掉到坑里去的。那时候,代码功能看着都正常,但程序运行一段时间后性能就下降。不得不来一次万能的重启以解决问题,这显然不是最佳解决办法。

所以,我将文中涉及到的注意事项,整理出来再列举如下。希望可以帮助遇到性能问题的程序员们。

  • 堆区的存储空间是有限的,创建对象时要心中有数;
  • 对象变量存储的不是实例本身,而是指向堆区实例的指针;
  • 类中属性变量不要定义过大,避免出现超大数组;
  • 堆区和栈区所引用的对象,是不会被GC所回收的。


Tags:编程 内存布局   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
当今流行的编程语言,大多具备垃圾回收(Garbage Collection,以下简称GC)功能。它能够将不再使用的内存区域收回并重新分配。这一功能可以说,将程序员的注意力从内存的分配/释放工...【详细内容】
2020-09-25  Tags: 编程 内存布局  点击:(90)  评论:(0)  加入收藏
▌简易百科推荐
1. 前言了解响应式编程,首先我们需要了解函数式操作和Stream的操作,下面我们简单的复习一下喽。1.1 常用函数式编程函数式接口中我们先来回顾一下Java中的函数式接口。常见的...【详细内容】
2022-07-15  二哥学Java    Tags:编程   点击:(1)  评论:(0)  加入收藏
在本文中,我们将学习如何使用 Next.js、 Prisma、 Postgres 和 Fastify 构建一个 Full-stack 应用程序。在本文中,我们将学习如何使用 Next.js、 Prisma、 Postgres 和 Fastif...【详细内容】
2022-07-12  qaseven    Tags:全栈   点击:(9)  评论:(0)  加入收藏
好的软件开发网站有哪些?做软件开发哪些网站能提供帮助呢?这些很多做软件开发的小伙伴都会问到的问题。007出海全球社交流量导航网站,整合了多方出海跨境网站资源,为你介绍出海...【详细内容】
2022-07-08  Chuhai007    Tags:软件开发   点击:(10)  评论:(0)  加入收藏
我们用monkey做压力测试后,会保存一个monkey日志,那如果想快速的分析日志中有哪些异常,我们可以用批处理工具进行快速的筛查,我们一起来看看吧。先编写个小脚本,然后修改为bat后...【详细内容】
2022-07-08  溪流涌动    Tags:monkey   点击:(13)  评论:(0)  加入收藏
白盒测试落地实践分为两个大方向,一个是静态分析,一个是动态分析,当然啦,也可以叫做静态测试和动态测试。那我们如何高质量保效率的做好白盒测试呢?Parasoft已经为您准备好了成熟...【详细内容】
2022-07-08  Parasoft中国    Tags:白盒测试   点击:(11)  评论:(0)  加入收藏
Altium Designer 自带脚本功能的开发项目,可以调用官方AD API接口对原理图或者PCB进行自动操作,本文主要分享开发的流程,和一些基本的概念信息,本文介绍的脚本工具例子可以用在P...【详细内容】
2022-07-07  电子工程师伟哥    Tags:Altium Designer   点击:(21)  评论:(0)  加入收藏
一、目录介绍 前置知识点 NIO Netty 的核心组件 Channel Callback Future 和 Promise 事件和 ChannelHandler Hello World二、前置知识点1、NIO首先我们需要回顾一...【详细内容】
2022-07-06  架构师jickly    Tags:聊天系统   点击:(16)  评论:(0)  加入收藏
1.事件流事件流是对事件执行过程的描述,了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。2.捕获和冒泡捕获阶段是【从父到子】的传导过程,冒泡阶...【详细内容】
2022-07-06  金乾坤    Tags:API   点击:(13)  评论:(0)  加入收藏
刷盘策略CommitLog的asyncPutMessage方法中可以看到在写入消息之后,调用了submitFlushRequest方法执行刷盘策略:public class CommitLog { public CompletableFuture<PutMe...【详细内容】
2022-07-06  Java码农之路    Tags:RocketMQ   点击:(16)  评论:(0)  加入收藏
最近读了本好书-《深度学习推荐系统》,读完不觉全身通畅,于是就有了写这篇文章的想法,把自己的理解和总结分享给大家。 本文将按照从算法到工程的顺序,先介绍一下推荐系统整体...【详细内容】
2022-07-05  InfoQ    Tags:推荐系统   点击:(22)  评论:(0)  加入收藏
站内最新
站内热门
站内头条