以前的几篇文章中,总结了java中的基本语句和基本数据类型等等一系列的最基本的东西,下面就来讲说java中的函数部分java
在C/C++中有普通的全局函数、类成员函数和类的静态函数,而java中全部内容都必须定义在类中。因此Java中是没有全局函数的,Java里面只有普通的类成员函数(也叫作成员方法)和静态函数(也叫作静态方法)。这两种东西在理解上与C/C++基本同样,定义的格式分别为:数组
public static void test(arglist){ } public void test(arglist){ }
基本格式为:修饰符 [static] 返回值 函数名称 形参列表架构
修饰符主要是用来修饰方法的访问限制,好比public 、private等等;若是是静态方法须要加上static 若是是成员方法则不须要;后面是返回值,Java函数能够返回任意类型的值;函数名用来肯定一个函数,最后形参列表是传递给函数的参数列表。函数
Java中函数的使用方式与C/C++中基本相同,这里就再也不额外花费篇幅说明它的使用,我想将重点放在函数调用时内存的分配和使用上,更深一层了解java中函数的运行机制。3d
咱们说在X86架构的机器上,每一个进程拥有4GB的虚拟地址空间。Java程序也是一个进程,因此它也拥有4GB的虚拟地址空间。每当启动一个Java程序的时候,由Java虚拟机读取.class 文件,而后解释执行其中的二进制字节码。启动java程序时,在进程列表中看到的是一个个的Java虚拟机程序。
java虚拟机在加载.class 文件时将它的4GB的虚拟地址空间划分为5个部分,分别是栈、堆、方法区、本地方法栈、寄存器区。其中重点须要关注前3个部分。指针
class Demo{ public static void main(String[] args){ int n = 10; test(10); System.out.println(n); } public static void test(int i){ System.out.println(i); i++; } }
上述代码在函数中改变了形参值,那么在调用以后n的值会不会发生变化呢?答案是:不会变化,在C/C++中很好理解,形参i只是实参n的一个拷贝,i改变不会改变原来的n。这里咱们从内存的角度来回答这个问题
code
如上图所示,方法区中存储了两个方法的相关信息,main和test,在调用main的时候,首先从方法区中查找main函数的相关信息,而后在栈中进行参数入栈等操做。而后初始化一个局部变量n,接着调用test函数,调用test函数时首先根据方法区中的函数表找到方法对应的代码位置,而后进行栈寄存器的偏移为函数test分配一个栈空间,接着进行参数入栈,这个时候会将n的值——10拷贝到i所在内存中。这个时候在test中修改了i的值,改变的是形参中拷贝的值,与n无关。因此这里n的值不变对象
class Demo{ public static void main(String[] args){ String s = "Hello"; test(s); System.out.println(s); //"Hello" } public static void test(String s){ System.out.println(s); //"Hello" s = "World"; } }
在C/C++中,常常有这么一句话:“按值传递不能改变实参的值,按引用传递能够改变实参的值”,咱们知道String 是一个引用,那么这里传递的是String的引用,咱们在函数内部改变了s的值,在外部s的值是否是也改变了呢?咱们首先估计会打印一个 "Hello"、一个"World"; 实际运行结果倒是打印了两个 "Hello",那么是否是有问题呢?Java中到底存不存在按引用传递呢?为了回答这个问题,咱们仍是来一张内存图:
blog
从上面的内存图来看,在函数中修改的仍然是形参的值,而对实参的值彻底没有影响。若是想作到在函数中修改实参的值,请记住一点:拿到实参的地址,经过地址直接修改内存。进程
下面再来看一个例子:
class Demo{ public static void main(String[] args){ int[] array = new int[]{1, 2, 3, 4, 5}; test(array); for(int i = 0; i < array.length; i++){ System.out.print(array[i]); } System.out.println(); //98345 } public static void test(int[] array){ for(int i = 0; i < array.length; i++){ System.out.print(array[i]); } System.out.println(); //12345 array[0] = 9; array[1] = 8; } }
运行这个实例,能够看到这里它确实改变了,那么这里它发生了什么?跟上面一个字符串的例子相比有什么不一样呢?仍是来看看内存图
这段代码执行的过程当中经历了3个主要步骤:
这段代码与上面两段本质上的区别在于,这段代码经过引用类型中保存的地址值找到并修改了对应内存中内容,而上面的两段代码仅仅是在修改引用类型这个变量自己的值。
说到传递引用类型,那么我就想到在C/C++中一个经典的漏洞——缓冲区溢出漏洞,那么java程序中是否也存在这个问题呢?这里我准备了这样一段代码:
class Demo{ public static void main(String[] args){ byte[] buf = new byte[7]; test(buf); } public static void test(byte[] buf){ for(int i = 0; i < 10; i++){ buf[i] = (byte)i; } } }
若是是在C/C++中,这段代码能够正常执行只是最后可能会报错或者崩溃,可是赋值是成功的,这也就留给了黑客可利用的空间。
在Java中执行它会发现,它会报一个越界访问的异常,也就说这里赋值是失败的,不能直接往内存里面写,也就不存在这个漏洞了。
Java方法返回基本类型的状况很简单,也就是将函数返回值放到某块内存中,而后进行一个复制操做。这里重点了解一下它在返回引用类型时与C/C++不一样的点
在C/C++中返回一个类对象的时候,会调用拷贝构造将须要返回的类对象拷贝到对应保存类对象的位置,而后针对函数中的类对象调用它的析构函数进行资源回收,那么Java中返回类对象会进行哪些操做?
C/C++中返回一个类对象的指针时,外部须要本身调用delete或者其余操做进行析构。java中的类对象都是引用类型,在函数外部为什么不须要额外调用析构呢?带着这些问题,来看下面这段代码:
class Demo{ public static void main(String[] args){ String s = test(); System.out.println(s); } public static String test(){ // return new String("hello world"); return "Hello World"; } }
这段代码 不论是用new也好仍是直接返回也好,效果实际上是同样的,下面是对应的内存分布图
这段代码首先在函数test中new一个对象,此时对应在堆内存中开辟一块空间来保存"hello world" 值,而后保存内存地址在寄存器或者其余某个位置,接着将这个地址值拷贝到main函数中的s中,最后回收test函数的栈空间。
这里实质上是返回了一个堆中的地址值,这里就回答了第一个问题:在返回类对象的时候其实返回的值对象所在的堆内存的地址。
接着来回答第二个问题:java中资源回收依赖与一个引用计数。每当对地址值进行一次拷贝时计数器加一,当回收拷贝值所在内存时计数器减一。这里在返回时,先将地址值保存到某个位置(好比C/C++中是将返回值保存在eax寄存器中)。此时计数器 + 1;而后将这个值拷贝到 main 函数的s变量中,此时计数器的值再 + 1,变为2,接着回收test函数栈空间,计数器 - 1,变为1,在main函数指向完成以后,main的栈空间也被回收,此时计数器 - 1,变为0,此时new出来的对象由Java的垃圾回收器进行回收。