`
ayufox
  • 浏览: 276231 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java 栈内存介绍

    博客分类:
  • JVM
阅读更多

     本篇我们了解一下Java的栈内存空间。
     1.我们首先从Intel80386架构下的Linux汇编开始,看看会把什么东西存放在栈中。在开始之前,需要注意一点,Intel80386架构下的linux系统的堆是从高位地址往低位地址增长的。
     我们看一个简单的例子,计算从1加到100,文件存储为test.c

int sum(int max);
int test()
{
    sum(100);
}
int sum(int max)
{
    int result = 0;
    int i;
    for (i=1; i<=max; i++)
    {
        result += i;
    }

    return result;
}

     编译一下,生成test.o

gcc -c test.c

     反汇编一下test.o

objdump -d test.o

     得到如下的汇编代码


00000000 <test>:
0: push %ebp
1: mov %esp,%ebp                                       ;1)
3: sub $0x8,%esp
6: movl $0x64,(%esp)
d: call e <test+0xe>                                       ;2)
12: leave
13: ret                                                        ;3)

00000014 <sum>:
14: push %ebp
15: mov %esp,%ebp
17: sub $0x10,%esp
1a: movl $0x0,0xfffffff8(%ebp)
21: movl $0x1,0xfffffffc(%ebp)                          ;4)
28: jmp 34 <sum+0x20>
2a: mov 0xfffffffc(%ebp),%eax
2d: add %eax,0xfffffff8(%ebp)
30: addl $0x1,0xfffffffc(%ebp)
34: mov 0xfffffffc(%ebp),%eax ;
37: cmp 0x8(%ebp),%eax                               ;5)
3a: jle 2a <sum+0x16>
3c: mov 0xfffffff8(%ebp),%eax
3f: leave
40: ret

    1)push   %ebp
         mov    %esp,%ebp
      标准的函数进入后处理方式,将上一个方法的基址寄存器(ebp)入栈,以备在返回时恢复上一个方法的现场。并将当前的栈顶esp(即当前方法栈的开始地址)设置给ebp(即ebp总是指向当前方法栈的开始位置)
    2)sub    $0x8,%esp
        movl   $0x64,(%esp)
        call   e <test+0xe>
     需要再次提醒的是,栈是从高位地址往低位地址增长的,所以此处是减,从栈中分配8个字节,其中前4个字节预留给方法入参(movl   $0x64,(%esp)),后4个字节预留给返回地址(在call   e <test+0xe>,call指令自动会将下一条指令的地址塞入这个位置),需要注意的是,这里call的目标地址是e(当前指令位置d+1),是因为这是一个目标文件(.o),在连接(link)阶段,会自动解析正确的地址,有兴趣可以参考我的另一篇BLOG《Linkers and Loaders初探 》。
     3)leave
          ret
   标准的离开方法时的指令组合,这两条指令会进行返回到上一个方法的现场回复,譬如,恢复上一个方法的ebp/esp,并转向上一个方法的下一条指令的位置。
      4)现在进入到方法sum,同样的,经过标准参数进入处理后,ebp指向当前方法栈的开始位置,而指令(sub    $0x10,%esp)预留了16个字节给当前的局部变量(发现在这里预留给局部变量的空间总是16个字节的倍数,不知道为什么一定要这样子),经过这番折腾后,我们可以看到栈数据的分布大概是如下,有了这个图,后面的程序的理解就比较轻松了。
 
       5)mov    0xfffffffc(%ebp),%eax ;
          cmp    0x8(%ebp),%eax
       这里非常好理解,就是比较i(ebp-4)和入参max(ebp+8)
      2.通过如上程序的分析,我们大概可以知道,Intel80386架构下Linux汇编中,入参、返回方法地址、局部变量会存储到栈中,使用ebp+偏移量可以访问到特定的某个局部变量。JVM汇编与之类似,但有点不同的是,Intel80386的汇编中,计算是通过EAX/EBX等寄存器来进行的,而JVM中则没有这两个寄存器的概念,计算是通过堆来进行的。

      我们看看JVM汇编是如何出来的,首先是Java程序,如上的例子一样

package ray.test;

public class Test
{
    public int test()
    {
        return sum(100);
    }

    public int sum(int max)
    {
        int result = 0;
        int i;
        for (i = 1; i <= max; i++)
        {
            result += i;
        }

        return result;
    }
}

      我们看看JVM汇编结果

public class ray.test.Test extends java.lang.Object{
public ray.test.Test();
Code:
0: aload_0
1: invokespecial java/lang/Object.<init>:()V
4: return

public int test();
Code:
0: aload_0
1: bipush 100                                       ;1)
3: invokevirtual sum:(I)I                          ;2)
6: ireturn

public int sum(int);
Code:
0: iconst_0
1: istore_2
2: iconst_1
3: istore_3                                           ;3)
4: goto 14
7: iload_2
8: iload_3
9: iadd
10: istore_2
11: iinc 3, 1                                        ;4)
14: iload_3
15: iload_1
16: if_icmple 7                                      ;5)
19: iload_2
20: ireturn ;6)
}

      1)  aload_0                   :将第一个方法参数入栈,第一个参数就是this的地址
           bipush  100              :将参数值100入栈
      2)invokevirtual   sum:(I)I :调用sum方法,JVM是基于栈的运算,此处传入的参数就是栈顶部分的数据,即100,(实际上会传递2个参数,一个是this,一个是100,大部分面向对象的语言在转换成面向过程的汇编的过程当中,大体都会是类似的处理方法)
      3)iconst_0                   :将值0入栈,此指令类似于bipush 0,只是会更高效。JVM对于最常见的0-5,都有一条iconst_x指令。
          istore_2                    :把第3个局部变量(即result)的值设为当前栈顶值(0),第2个参数可以理解为ebp偏移4个字节。第1个局部变量是this,第2个局部变量是入参数,
          iconst_1        
          istore_3                    :与上类似,第4个局部变量为i,设置值为1
     4) iload_2                     :将第3个局部变量(result)入栈
          iload_3                      :将第4个局部变量(i)入堆栈
          iadd                         :将两个栈顶元素相加,并将两个栈顶元素移除,将结果再入栈
          istore_2                     :将栈顶元素(即相加结果)设到第3个局部变量(即result)
          iinc    3, 1                 :递增第4个局部变量(i)的值
     5)iload_3                      :将第4个局部变量(i)的值入栈
          iload_1                      :将第2个局部变量(max)的值入栈
          if_icmple     7             :比较栈顶两个值,如果i<=max,则重新进入指令7的位置(对应for循环)
     从如上的指令说明,我们可以看到JVM的指令都是基于栈来进行运算的。为加深运算,我们再举一个数学运算的例子说明。一般,进行数学公式描述的时候,我们比较喜欢使用中缀表达式,譬如:18 * 12 + 17 * 13,而对于JVM这种基于栈运算的汇编来说,采用后缀表达式(逆波兰表达式)描述会更方便:18 12 * 17 13 * +。如下我们采用JVM汇编来进行这个运算

bipush 18
bipush 12
imul
bipush 17
bipush 13
imul
iadd

     我们可以从栈的变迁来理解这个过程,从JVM汇编我们看不出栈是由上自下增长(Intel80386下的linux系统是采用这种方式)还是自下往上增长,我们这里假设是由下往上增长。
      3.从如上的分析过程,我们可以了解到JVM的栈中会存储一些什么东西,以及如何利用堆来进行运算的。除此之外,我们需要知道的是,在JVM中,栈是针对线程的,在线程构造函数中,我们可以看到可以传入栈的大小,需要注意的是,该值对JVM而言只是一个建议,JVM有权选择更合适的值

public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
    init(group, target, name, stackSize);
}

     当然也可以通过JVM启动参数来指定

-XX:ThreadStackSize=<value>:设置线程的栈大小(字节数)(0表示默认) [Sparc: 512, Solaris Intel: 256, Sparc 64bit: 1024 all others 0]

    一般情况下采用默认的值即可

20
2
分享到:
评论
1 楼 lydawen 2010-05-04  
java栈是倒着向下长的

相关推荐

    Java中堆内存与栈内存分配浅析

    通过本文的介绍,我们了解了Java中堆内存与栈内存的基本概念、特点及其应用场景。正确理解和使用这两种内存类型可以帮助开发者编写更加高效、健壮的代码。同时,在实际开发过程中还需注意合理选择数据存储位置,避免...

    Java栈内存与堆内存

    Java把内存划分成两种:一种是栈内存,一种是堆内存。 在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过...

    Java中堆内存和栈内存详解

    ### Java中堆内存和栈内存详解 #### 一、引言 Java作为一种广泛使用的编程语言,其内存管理机制是理解程序行为的关键。本文将深入探讨Java中的两种主要内存区域:堆内存(Heap Memory)和栈内存(Stack Memory)。...

    Java中栈内存和堆内存详解

    Java中栈内存和堆内存详解,非常容易理解

    java 栈的实现和应用

    Java栈是一种基于后进先出(LIFO)原则的数据结构,它在计算机科学和编程中具有广泛的应用。本文将深入探讨Java中栈的实现以及其在实际应用中的使用。 首先,我们来理解栈的基本概念。栈是一种特殊类型的线性数据...

    java实现内存动态分配

    Java 实现内存动态分配主要涉及Java内存模型以及内存管理机制,包括堆内存和栈内存的分配,以及垃圾回收等概念。下面将详细解释这些知识点。 1. **Java内存模型** Java程序运行时,内存分为堆内存(Heap)和栈内存...

    java-内存-栈介绍

    存放方法栈、成员基本数据类型变量的引用和值、成员引用数据类型变量的引用

    java内存机制及异常处理

    3. **Java Stack(Java栈)**:每个线程都有自己的Java栈,用于存储方法调用的局部变量、操作数栈和方法返回地址。当栈溢出时,会抛出`java.lang.StackOverflowError`。 4. **Program Counter Register(程序计数器...

    Java中堆内存和栈内存详解.doc

    本文将详细介绍Java中的两种主要内存区域——堆内存(Heap Memory)与栈内存(Stack Memory),并探讨它们之间的区别以及如何有效地使用这两种内存。 #### 二、Java内存区域概述 Java虚拟机(JVM)将内存分为几个...

    java中的栈(深层了解java虚拟机对对象的内存分布)

    ### 深层解析Java虚拟机中的栈与堆:对象的内存分布 #### 核心概念:栈与堆的本质及作用 在Java编程语言中,理解栈(stack)和堆(heap)的概念及其工作原理对于深入掌握Java虚拟机(JVM)如何管理内存至关重要。栈和堆...

    Java 中的堆和栈

    在Java中,内存主要分为两个区域:栈内存和堆内存。这两部分内存各自有不同的特点和用途。 首先,栈内存主要负责存储基础数据类型(如byte, short, int, long, float, double, boolean, char)和对象的引用。当在...

    关于Java栈与堆的思考

    关于Java栈与堆的深入解析 Java作为一种广泛使用的编程语言,其内存管理机制是学习者必须掌握的核心概念之一。在Java中,栈(Stack)与堆(Heap)是用于存储数据的主要区域,它们各自承担着不同的职责,对于理解...

    java内存原理.doc

    在 Java 中,内存被分为两种:栈内存和堆内存。 栈内存是指在函数中定义的一些基本类型的变量和对象的引用变量。这些变量在函数的栈内存中分配,当超过变量的作用域后,Java 会自动释放掉为变量分配的内存空间,该...

    Java内存泄露及内存无法回收解决方案

    其中,堆内存是Java对象的主要存储场所,栈内存主要存储方法调用时的局部变量,而方法区则存储类的信息,如类的常量池、字段和方法数据等。 内存泄漏通常发生在堆内存中,当程序创建对象并分配内存后,如果不再需要...

    Java的内存管理机制分析

    - 栈内存中的数据在方法执行完毕后会自动释放,因此栈内存的生命周期与方法的执行周期一致。 2. **堆(Heap)**: - 是Java虚拟机中最大的内存区域。 - 存储通过`new`操作符创建的对象实例。 - 堆内存是所有...

Global site tag (gtag.js) - Google Analytics