java与c语言的内存对比

问题来源

由于本人经常用java进行开发,对c语言的开发不是很熟悉,在用c语言写一个返回数组的函数的时候踩到了坑,具体问题如下:

这是一个普通的java函数,用来返回一个数组:用java语言在一个方法中创建一个数组,并进行初始化,编译以后可以运行通过

public class GetArray {
	public static int[] getArray(){
		int[] a = {1,2,3,4,5};
		return a;
	}
	public static void main(String[] args){
		int[] ret = getArray();
		for(int i = 0;i<5;i++){
			System.out.println(i);
		}
	}
}

用c语言写同样类似的函数,在函数中创建并初始化一个数组,编译完成后运行这个程序会发生段错误:

#include
int* getArray(){
	int a[] = {1,2,3,4,5};
	return a;
}
void main(){
	int *ret = getArray();
	for(int i = 0; i < 5 ;i ++){
		printf("%d\n",ret[i]);
	}	
}

这个问题产生的原因是java内存和c内存的布局以及每个区域存放的内容的不同而造成的,为了探明原因,首先来回顾一下java内存和c内存的布局。

Java内存:

由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。下图是java程序的执行流程。

java与c语言的内存对比_第1张图片

首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。Runtime Data Areas的分区如下:

java与c语言的内存对比_第2张图片

其中线程私有的区域为程序计数器、虚拟机栈、本地方法栈,线程共有的区域为堆和方法区。

方法区:存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息,通过Class类的对象来访问已经被加载的类的信息)、常量(final修饰的变量和String的字面值)、静态变量(static修饰的变量),即时编译器编译后的代码等数据,类名、访问修饰符、常量池、字段描述、方法描述。在Java虚拟机规范中,方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择不在方法区实现垃圾回收与压缩。因此有人称其为永久代,HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。

堆:是java内存中最大的一块,唯一目的是存放对象实例和数组,是垃圾回收的主要区域。对于堆,java虚拟机会对这一部分区域进行自动的垃圾回收。

虚拟机栈:虚拟机栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈,当前线程正在执行的方法就处于栈的顶部。

java与c语言的内存对比_第3张图片

 

其中,局部变量表用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量(对象和数组),则存的是指向对象的引用。

操作数栈,栈最典型的一个应用就是用来对表达式求值。一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此程序中的所有计算过程都是在借助于操作数栈来完成的。

指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

本地方法栈:本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。

程序计数器:当前线程所执行的字节码的行号指示器。

常量池:方法区的一部分(从JDK7开始移到了堆里面),JVM为每个已加载的类型维护一个常量池,用于存放编译期生成的字面量和符号应用,这部分内容将在类加载后进入方法区的时候存到运行时常量池中。运行时常量池还有个更重要的的特征:动态性。Java要求,编译期的常量池的内容可以进入运行时常量池,运行时产生的常量也可以放入池中。常用的是String类的intern()方法。

c语言内存

c语言内存分区,按照地址从低到高排列为下图

java与c语言的内存对比_第4张图片

text(文本/代码区):通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后在代码区中引用该数据地址;如果是BSS区和数据区,在代码区中同样将引用该数据地址。另外,代码段还规划了局部数据所申请的内存空间信息。

Initialized data(全局初始化数据区/静态数据区):通常用来存放程序中已初始化的(声明的时候显示赋值)全局变量、静态变量、常量和外部变量,这些变量没有在函数内部定义或者是以静态的方式在函数内部定义了,该区域只能初始化一次。

Uninitialized data(未初始化数据区):又叫BSS段,通常是指用来存放程序中未初始化(声明的时候没有显示赋值)的全局变量的一块内存区域,用来保存所有的初始化为0的全局变量和静态变量(全局变量和静态全局变量如果只是声明而没有显示赋值的话,编译器会将其值设置为0),BSS 是英文Block Started by Symbol 的简称。

Heap(堆区):用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并将在内存中为这些段分配空间。栈也是由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显式地申请和释放空间。

Stack(栈区):由编译器自动分配释放,存放函数的参数值、局部变量的值以及在进行任务切换时存放当前任务的上下文内容(比如函数的返回地址,即调用这个函数的地方)。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的局部变量(自动变量)和临时变量在栈区上分配空间。除此以外,在函数被调用时,其參数也会被压入发起调用的进程栈中,而且待到调用结束后,函数的局部变量等会被释放,其返回值会被存放回栈中。因为栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。

栈区是紧连着堆区的,堆和栈的增长方向相反,栈区是以后进先出的机制运行的,通常在内存的高地址处而堆区则在低地址处,当堆和栈的指针重合时,此时内存被耗尽。

分析:

进行对比可以看到,对于堆区,java虚拟机会自动对堆区进行垃圾回收而不需要程序员来进行显示管理,c语言的堆区则需要程序员显示的分配和释放;并且java的堆中存放的数据是数组和对象,即由new关键字初始化的数据,而c语言存放的数据则是由程序员通过malloc函数分配内存区域以后自己定义的。

对于栈区,两者的功能都是类似的,都描述了函数执行的内存模型。但是两者存放的数据是有区别的,在java的栈中,对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量(对象和数组),则存的是指向对象的引用;在c语言的栈中,存放的是函数的参数值、局部变量,所以在函数返回的时候,java栈中的基本数据类型的变量值以及应用类型变量的引用值会被释放,而c语言栈中的局部变量会被释放。

另外在c语言中变量类型包括全局变量,局部变量和静态变量。

全局变量(外部变量):出现在代码块{}之外的变量就是全局变量。

局部变量(自动变量):一般情况下,代码块{}内部定义的变量就是自动变量,也可使用auto显示定义。

静态变量:是指内存位置在程序执行期间一直不改变的变量,用关键字static修饰。代码块内部的静态变量只能被这个代码块内部访问,代码块外部的静态变量只能被定义这个变量的文件访问。

所以对于java程序

public class GetArray {
	public static int[] getArray(){
		int[] a = {1,2,3,4,5};
		return a;
	}
	public static void main(String[] args){
		int[] ret = getArray();
		for(int i = 0;i<5;i++){
			System.out.println(i);
		}
	}
}

数组a是存放在堆中的,只要对数组a的引用存在,jvm就不会对数组a进行垃圾回收,数组a会一直存放在内存当中,但是对于c程序

#include
int* getArray(){
	int a[] = {1,2,3,4,5};
	return a;
}
void main(){
	int *ret = getArray();
	for(int i = 0; i < 5 ;i ++){
		printf("%d\n",ret[i]);
	}	
}

    这里的数组a是一个局部变量(定义在函数内部的变量),数组a会被存放在栈中,当main函数调用getArray函数的时候会为数组a分配内存并进行初始化,但是当函数返回的时候,即使把数组a的地址返回给main函数,这个地址所指向的内存也随着getArray函数的返回被系统回收了,因此数组a已经不存在于内存当中了,所以在读取数组a的内容的时候会发生段错误,由此我们可以看出,不仅是对于数组,对于基本的变量也会存在同样的问题,下边是一个出错误的程序:

#include
int* getArray(){
	int a = 123;
	return &a;
}
void main(){
	int *a = getArray();
	printf("%d",*a);
}

整数a同样是函数的局部变量,在函数返回时会被系统回收,因此即便返回了整数a的地址,这个地址指向的内存区域也已经被回收了,同样会发生段错误。

因此,为了解决这个问题,可以把数组存在在堆中,即在函数内部用malloc进行初始化,但是需要程序员手动free

#include
int* getArray(){
	int *a = malloc(5*sizeof(int));
	a[0]=0;
	a[1]=1;
	a[2]=2;
	a[3]=3;
	a[4]=4;
	return a;
}
void main(){
	int *ret = getArray();
	for(int i = 0; i < 5 ;i ++){
		printf("%d\n",ret[i]);
	}
}

    也可以将数组存放在main函数的栈中,将数组的地址传递给getArray函数让其初始化,这样在getArray函数返回的时候main函数并没有返回,因此这个数组的内存不会被回收

#include
int* getArray(int *a){
	a[0]=0;
	a[1]=1;
	a[2]=2;
	a[3]=3;
	a[4]=4;
	return a;
}
void main(){
	int a[5];
	int *ret = getArray(a);
	for(int i = 0; i < 5 ;i ++){
		printf("%d\n",ret[i]);
	}
}

 

你可能感兴趣的:(编程语言)