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


先看一段派生对象的代码:
 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定义的类变量 。程序员对此一定要心中有数,一定要记住类变量生成的对象,它的生命周期是和程序本身一样的 。
而栈上所引用的对象,它的存活周期则和方法调用一致 。也就是说如果方法退出,那么期间所产生的对象不再使用了,是会被回收的 。
在多线程环境中,程序员要注意,如果一个方法是长期后台运行的,则不要进行频繁地创建对象的工作,以避免内存无法回收 。
程序运行后性能总会下降?你应该先了解编程语言的内存布局与管理

文章插图
被栈区和静态区引用的对象是不会被回收的
总结经过了解编程语言的内存布局与管理,我们发现还是有很多细节处不注意的话,很容易掉到坑里去的 。那时候,代码功能看着都正常,但程序运行一段时间后性能就下降 。不得不来一次万能的重启以解决问题,这显然不是最佳解决办法 。
所以,我将文中涉及到的注意事项,整理出来再列举如下 。希望可以帮助遇到性能问题的程序员们 。