13 评论

Java 性能优化[1]:基本类型 vs 引用类型

  在 Java 性能优化系列中,内存管理是一个要优先考虑的关键因素。而说到内存分配,就必然会涉及到基本类型和引用类型。所以我们今天就先来介绍一下这两种类型在性能方面各自有什么奥妙(关于引用类型的其它奥妙,请看“这里”)。


★名词定义


  先明确一下什么是“基本类型”,什么是“引用类型”。
  简单地说,所谓基本类型就是 Java 语言中如下的8种内置类型:
boolean
char
byte
short
int
long
float
double
  而引用类型就是那些可以通过 new 来创建对象的类型(基本上都是派生自 Object)。


★两种类型的存储方式


  这两种类型的差异,首先体现在存储方式上。

◇引用类型的创建

  当你在函数中创建一个引用类型的对象时,比如下面这句:
StringBuffer str = new StringBuffer();
  该 StringBuffer 【对象】的内容是存储在堆(Heap)上的,需要申请堆内存。而变量 str 只不过是针对该 StringBuffer 对象的一个引用(或者叫地址)。变量 str 的【值】(也就是 StringBuffer 对象的地址)是存储在【栈】上的。

◇基本类型的创建

  当你在【函数中}创建一个基本类型的变量时,比如下面这句:
int n = 123;
  这个变量 n 的【值】也是存储在栈(Stack)上的,但是这个语句不需要再从堆中申请内存了。

  为了更加形象,便于大伙儿理解,简单画了一个示意图如下:
不见图 请翻墙


★堆和栈的性能差异


  可能有同学会小声问:堆和栈有啥区别捏?
  要说堆和栈的差别,那可就大了去了。如果你对这两个概念还是不太明白或者经常混淆,建议先找本操作系统的书拜读一下。
  由于本系列是介绍性能,所以来讨论一下堆和栈在性能方面的差别(这个差异是很大滴)。堆相对进程来说是全局的,能够被所有线程访问;而栈是线程局部的,只能本线程访问。打个比方,栈就好比个人小金库,堆就好比国库。你从个人小金库拿钱去花,不需要办什么手续,拿了就花,但是钱数有限;而国库里面的钱虽然很多,但是每次申请花钱要打报告、盖图章、办 N 多手续,耗时又费力。
  同样道理,由于堆是所有线程共有的,从堆里面申请内存要进行相关的加锁操作,因此申请堆内存的复杂度和时间开销比栈要大很多;从栈里面申请内存,虽然又简单又快,但是栈的大小有限,分配不了太多内存。


★当初为啥这样设计?


  可能有同学又问了,干嘛把两种类型分开存储,干嘛不放到一起捏?这个问题问得好!下面我们就来揣测一下,当初 Java 为啥设计成这样。
  当年 Java 它爹(James Gosling)设计语言的时候,对于这个问题有点进退两难。如果把各种东东都放置到栈中,显然不现实,一来栈是线程私有的(不便于共享),二来栈的大小是有限的,三来栈的结构也间接限制了它的用途。那为啥不把各种东东都放置到堆里面捏?都放堆里面,倒是能绕过上述问题,但是刚才也提到了,申请堆内存要办很多手续,太繁琐。如果仅仅在函数中写一个简单的“int n = 0;”,也要到堆里面去分配内存,那性能就大大滴差了(要知道 Java 是1995年生出来的,那年头俺买了台 PC 配【4兆内存】就属豪华配置了)。
  左思右想之后,Java 它爹只好做了一个折中:把类型分为“基本类型”和“引用类型”,两者使用不同的创建方式。这种差异从 Java 语法上也可以看出来:引用类型总是用 new 创建对象(提醒一下:某些单键对象/单例对象,表面上没用 new,但是在 getInstance() 内部也还是用 new 创建的);而基本类型则【不需要】用 new 来创建。


★这样设计的弊端


  顺便跑题一下,斗胆评价 Java 它爹这种设计的弊端(希望 Java Fans 不要跟我急)。我个人认为:这个折中的决策,带来了许多深远的影响,随手举出几个例子:
1、由于基本类型不是派生自 Object,因此不能算是纯种的对象。这导致了 Java 的“【纯】面向对象”招牌打了折扣(当年 Sun 老是吹嘘 Java 是“纯”OO 的语言,其实 Java 的 OO 是不够纯粹滴)。
2、由于基本类型不是派生自 Object,出于某些场合(比如容器类)的考虑,不得不为每个基本类型加上对应的包装类(比如 Integer、Byte 等),使得语言变得有点冗余。


★结论


  从上述的介绍,我们应该明白,使用 new 创建对象的开销是【不小】的。在程序中能避免就应该尽量避免。另外,使用 new 创建对象,不光是创建时开销大,将来垃圾回收时,销毁对象也是有开销的(关于 GC 的开销,咱们会在后面的帖子细谈)。下一个帖子,我们找一个例子来实战一下。


回到本系列的目录
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想和本文原始地址:
https://program-think.blogspot.com/2009/03/java-performance-tuning-1-two-types.html

13 条评论

  1. &^%$#
    被GC宠坏了 很少细心考虑内存开销 囧 受教

    回复删除
  2. 你说的弊端不算什么,如果说Java 不是纯面向对象就让Java蒙羞,那么说作者您不是贼,是不是也蒙羞呢。呵呵 基本类型不参与GC 这本来就给开发人员提供了一种选择,如果没有基本类型,全是面向对象的类型,那要是i++ 这样的操作就弄得程序性能傻X了。python就没基本类型,那for i in range(100000):估计就懵了

    回复删除
  3. 楼上的同学,
    我说的“Java的纯面向对象招牌打了折扣。”其实是针对Sun之前的宣传。因为Sun以前老鼓吹“纯面向对象”,但是严格意义上讲,Java在OO方面还不够“纯”。是不是有点咬文嚼字? :-)
    其实,关于Java的基本类型的设计是否合理,在Java社区内一直有争论。似乎有些大牛也卷入了口水战中。

    回复删除
  4. 感觉表达不是很精确。
    所谓的引用类型和基本类型指的是“变量”,不能笼统的说是存在堆里或者栈里。首先,java规范中列出的七种变量类型中,比如实例变量(不管是基本类型还是引用类型)一般会是放在堆里的(不排除为了jvm为了优化而允许的在栈中分配对象的情形)。其次,对与引用类型的变量来说,最好不要把引用类型变量和它所引用的对象直接混到一起说。

    回复删除
  5. 楼上同学提醒得好,关于“引用类型变量和它所引用的对象”是很容易被搞混淆的两个概念。我后面打算针对这个问题,写个帖子说一下。
    至于本文说的“引用类型”,主要针对的是“被引用的对象”。

    至于你说的实例变量,实际上就是类对象的某个成员,是类对象的一部分,而本文强调类对象是存放在堆上的。所以并不矛盾。

    回复删除
  6. 堆栈理解感觉应该看下数据结构的书更合适·
    呵呵·

    好帖·
    受教了·

    回复删除
  7. JAVA 硬性规定了对象实例必须存放在堆上,结果无论哪个短命且小巧的对象创建都要绕一个大弯去堆里面存取,影响了性能。而C++这方面比较灵活,可以选择是在栈上面还是在堆上面创建对象,那么JAVA在这一点上相对C++是否一种倒退呢?

    回复删除
  8. 楼上的同学:
    当初Java它爹可能是为了让语言更傻瓜化才这么干。俺个人觉得,傻瓜化和灵活性有时候是矛盾的,不太容易平衡。
    从内存存储的灵活性上看,可以说Java相对C++是倒退的。

    回复删除
  9. "堆相对进程来说是全局的,能够被所有线程访问"----为啥这里前面是进程后面是线程?? 一直关注楼主的博客,自己最近正在学习OS,对线程和进程还是不太能理解,楼主能专门写篇博客介绍一下吗?

    回复删除
    回复
    1. 博文中的这句:
      “堆相对进程来说是全局的,能够被所有线程访问”

      如果补上一个定语,变为如下,可能你就不会觉得疑惑了。
      “堆相对进程来说是全局的,能够被[b]该进程的[/b]所有线程访问”

      删除
    2. 关于“线程和进程的差异”,
      这算是比较基础的知识点,Google 一下应该不难找到相关的介绍性文章,或者也可以去看看维基百科的相关词条

      删除

  10. 使用new创建对象的开销是不小的。在程序中能避免就应该尽量避免。
    博主你好,对这句话我有点疑问:
    如果用其他的方式创建对象开销会相对小么?
    大多数时候我们还是无法避免用new创建对象吧?

    回复删除
  11. 看了博主的这篇文章, 感觉C#跟java采用的是一模一样的策略呀, 基本类型都是值类型(struct,enum列外,它们也是值类型), 其它类型都是引用类型。
    值类型可以通过boxing变成引用, 可以通过unboxing转换回来。它们都搞个虚拟机, 现在.net也跨平台了, 感觉他们越来越相似了。
    还是比较喜欢c++的方式, 用&表示引用, 否则就是值, 完全由程序员控制。

    回复删除