系列文章目录
1.编程基础
程序本质上就是计算机要操作的数和执行的指令序列
1.1 数据类型与变量
1.1.1 java的基本数据类型:
- 整型 byte/short/int/long
- 浮点数
- 字符
- 布尔
思考为了什么布尔类型没有SIZE,或者换句话说布尔类型大小是多少?
class LotsOfBooleans
{
boolean a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
boolean b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
boolean c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
boolean d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
boolean e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
class LotsOfInts
{
int a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
int b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
int c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
int d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
int e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
public class Test
{
private static final int SIZE = 100000;
public static void main(String[] args) throws Exception
{
LotsOfBooleans[] first = new LotsOfBooleans[SIZE];
LotsOfInts[] second = new LotsOfInts[SIZE];
System.gc();
long startMem = getMemory();
for (int i=0; i < SIZE; i++)
{
first[i] = new LotsOfBooleans();
}
System.gc();
long endMem = getMemory();
System.out.println ("Size for LotsOfBooleans: " + (endMem-startMem));
System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
System.gc();
startMem = getMemory();
for (int i=0; i < SIZE; i++)
{
second[i] = new LotsOfInts();
}
System.gc();
endMem = getMemory();
System.out.println ("Size for LotsOfInts: " + (endMem-startMem));
System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
// Make sure nothing gets collected
long total = 0;
for (int i=0; i < SIZE; i++)
{
total += (first[i].a0 ? 1 : 0) + second[i].a0;
}
System.out.println(total);
}
private static long getMemory()
{
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
}
}
上述代码说明boolean数组中boolean为一个字节
而对于一个boolean不是数组的情况下多大呢?
Java虚拟机规范一书提到 :
-
在Java虚拟机中没有任何供 boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替。
-
Java虚拟机直接支持 boolean类型的数组,虚拟机的 navarra指令参见第6章的newarray小节可以创建这种数组。boolean类型数组的访问与修改共用byte类型数组的baload和 bastore指令。
这里其实说明了,在单独使用boolean值时,最终会被编译成int数据类型,因此占四个字节,但是,boolean类型数组由于要与byte类型数组共用baload与bastore指令,因此此时boolean占一个字节
最后强调一句boolean的大小如何最终还是要看虚机机规范与规定
1.1.2 java的引用数据类型
引用数据类型,指的就是存放在栈空间中,指向堆空间中实际存储对象的那一部分内存的别名
引用类型的特征:
- 对象、数组都是引用数据类型
- 所有引用类型的默认值都是null
- 一个引用变量可以用来引用任何与之兼容的类型,例子:Object object = new Object (“Jinjx”);
在JDK1.2之后引入了4种引用类型:
- 强引用:无论如何都不会被回收,即使内存溢出也是直接抛出异常
- 软引用:内存足够时不会被回收,内存不够时gc回收弱引用,如果回收了仍然不够,再抛出异常
- 弱引用:无论内存是否足够,在下一次gc过程中,总会被回收
- 虚引用:最弱的引用,无法通过虚引用获取对象,随时可能被回收
具体四种引用类型的代码展示,可以参考深入了解java虚拟机这本书中gc模块
1.1.3 基本数据类型与引用数据类型的区别
- 存储位置:基本数据类型存储在栈空间中,引用数据类型的引用存储在栈中,指向的数据存储在堆空间中
- 传值方式:基本数据类型是通过传值的方式进行传递,引用数据类型是通过传引用的方式进行传递
1.2 赋值
1.2.0 赋值默认值
- 对于类成员变量,JVM虚拟机会在调用类构造方法之前对没有赋值的成员变量赋默认值;对于局部变量,则不会有默认值,声明后必须显示的初始化好值
1.2.1 基本数据类型
对于基础的变量赋值不必多谈,这里需要注意两个小点
对于上面这两种赋值方式,ide都报错了,原因很简单
- 第一个,java中整型默认是int,而上述数值超过了整型变量的上限,所以错误,改成3232343433L即可;
- 第二个,同样的默认的浮点数类型是double,double赋值给float没有办法隐式转换,报错,改成10.2f即可;
1.2.2数组类型
int[] arr = {1, 2, 3} //给定初始值,此时数组的长度由指定数字个数确定
int[] arr = new int[]{1, 2, 3} //同上
int[] arr = new int[3];//数组属于引用类型,每个位置的元素都会被赋初始值
int[] arr = new int[3]{1, 2, 3};//这种声明方式不被允许
这里需要强调一点,数组作为引用类型,存储和赋值的过程与一般基本数据类型不同,数组名只是用来记录在堆内存中存储的实际数据的起始地址,因此对于下面这种代码是可以执行的
int[] shortArr = new int[]{1, 2, 3};
int[] longArr = new int[]{1, 2, 3, 4};
shortArr = longArr;
两个大小不同的数组是可以互相赋值的
1.3 基本运算
1.3.1算术运算
算术运算符: + - * / % ++ –
注意事项:
- 不要超出数据类型的范围,如int a = 2147483647 * 2
- 整数的除法运算是直接舍去小数部分,并不是四舍五入,如 10 / 4的结果是2,不是3
- 浮点数的计算会存在误差(二进制存储的精度问题)
1.3.2比较运算符
比较运算符: > >= < <= == !=
注意事项:
- -比较的内存地址,并不是比较内容,对于两个内容相同的数组==返回的是false
1.3.3逻辑运算符
逻辑运算符:& | ! ^ && ||
注意事项:
- 短路运算的运算规则,在只判定第一个结果就可以确定整体结果时,后一个判定将不会发生
1.3.4位运算符
位运算符:<< >> >>> & | ~ ^
注意事项:
- 只有无符号右移>>>,没有无符号左移,因为左移本身就不会保留符号位
1.4条件执行
1.4.1if/else/switch的简单用法
略
1.4.2条件执行的原理
程序执行顺序的改变的本质依然是程序计数器中指令执行顺序的变化,由计算机组成原理的只是可以知道,一般情况下pc默认都是+1执行下一条地址上的指令,循环往复,但是对于操作系统而言总存在着一类的指令叫做跳转指令,能够动态改变pc中将执行下一条命令的地址,因此我们才存在条件执行实现的可能
在跳转指令中,一般我们又区分两个概念一个是无条件跳转,一个是有条件跳转,二者共同组成了程序流程控制的实现,如
int a = 10;
if(a%2 == 0) {
System.out.println("偶数");
}
执行的逻辑大致可以表示为以下:
int a = 10;
条件跳转:如果a%2 == 0,跳转到第四行,即调整程序计数器内容
无条件跳转:跳转到第7行
{
System.out.println("偶数");
}
//其他代码
对于switch语句,一般理解下会优化成一种更高效的方式跳转表,这是一个数组形式的结构,每一个元素存储的是条件达到时跳转到的地址值,这种实现方式,致使switch语句相比if-else有很大优势,因为switch语句不管跳转到哪个语句块,花费的时间是固定的,即时间复杂度为常数类型。而if-else如执行第n个语句块时,必须先做前n个语句块的判断,时间复杂度不固定。所以说,switch语句利用跳转表,将时间复杂度转换为了常数
但是这里依然存在一个问题,即对于switch判断的元素必须是整型,因为只有整型才能对数组下标进行索引,其中byte short int本身就是整数,char本质上依然是整数,枚举类型在指定的时候也有对应的整数,String可以通过hashCode转换为整数,以上在java里都可以作为switch判断的变量,不过long是一个例外,跳转表值的存储空间一般为32位,无法容纳long
1.5循环
1.5.1 简单的循环
- while
- do while
- for
- foreach
1.5.2循环实现原理
同条件执行一样,依然是依靠条件跳转和无条件跳转实现,如
int[] arr = {1, 2, 3, 4};
for(int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
执行逻辑大致如下:
int[] arr = {1, 2, 3, 4};
int i = 0;
条件跳转:如果i >= arr.length 跳转到第7行
System.out.println(a[i]);
i++;
无条件跳转,跳转到第3行
//其他代码
1.6函数
在java中,对于一个函数我们一般有以下的结构:
修饰符 返回值类型 函数名字(参数类型 参数名字, ...) {
函数体
return 返回值;
}
对于一个存在在类中的函数,在java里我们一般称其方法(后续非必要时不再区分),对于一个方法,存在一个重要的概念即方法的签名,方法的签名只包括方法名和形参列表,其中形参列表包括参数的数量、类型、顺序
一般的函数使用我们跳过,着重说一下可变参数的使用
public class VariableParameter {
public static void main(String[] args) {
VariableParameter v = new VariableParameter();
v.function1("hello", "world", "hi", "java");
}
public void function1(String a, String ... args) {
System.out.println(a + " " + args[0] + " " + args[1] + " " + args[2]);
}
}
以上就是可变参数的简单使用,对于可变参数,我们要求它必须作为方法的最后一个参数,它的实现原理其实很简单,编译之后仍然是一个相应类型的数组,因此我们其实可以直接传如一个相应类型的数组
public class VariableParameter {
public static void main(String[] args) {
VariableParameter v = new VariableParameter();
int[] arr = {1, 2};
v.function2(arr);
System.out.println(arr[0]);// 10
System.out.println(arr[1]);// 10
}
public void function2(int ... args) {
Arrays.fill(args, 10);
}
}
1.7函数调用的基本原理
同样,对于函数调用我们依然离不开指令的跳转,但是相比较于之前的流程控制,调用函数存在着以下几个需要被解决的问题:
- 怎么传递参数?
- 函数如何知道返回到什么地方?
- 函数的返回值怎么传给调用者?
在深入探讨函数调用的过程之前,需要简要认识以下JVM内存区域中的虚拟机栈。
Java虚拟机栈,当一个线程被启用的时候,系统会为每一个线程创建一个虚拟机栈,在每一个虚拟机栈中又存放着一个个栈帧,每一个栈帧都对应着一个方法,当我们调用某一个方法时,本质就是这个方法的栈帧压栈,而当一个方法结束时,本质就是这个方法的栈帧弹出。
因此,我们可以发现,在活动线程中,只有当前栈顶的栈帧才是有效栈帧,称为当前栈帧,这个栈帧对应的方法就是正在执行的当前方法,而执行引擎的所有字节码指令都只针对当前栈帧有效。
1.7.1栈帧的内部结构
每一个栈帧中都有着以下几个结构:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 一些其他信息
1.7.2局部变量表
-
局部变量表:Local Variables,被称之为局部变量数组或本地变量表,最基础的存储单元为slot(变量槽)
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些变量的数据类型包括各类基本数据类型、对象引用(reference),以及 return Address 类型,这也就回答了之前提出的第一个问题
-
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
-
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。
-
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
-
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
1.7.3操作数栈
- 操作数栈主要用于保存运行过程中的中间结果,为计算过程中产生的中间变量提供临时的存储空间
- 每一个操作数栈在编译结束就已经确定好了大小,后续无法改变
- 在程序运行的过程中会存在一些字节码指令可以操作操作数栈,对于操作数栈的操作仅限于压栈和弹栈两种,操作数栈无法通过索引访问,一些指令如bitpush就是将某个常数压入操作数栈,iload就是将局部变量表的变量写入操作数栈,iadd就是将栈顶的两个元素分别弹栈相加后再压入操作数栈等
- 如果被调用的方法带有返回值的话,其返回值将会被压入调用者栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令,这个操作就回答了上述提出的第三个问题
1.7.4动态链接
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里,而动态链接干的就是将这些符号引用转换为直接引用(静态链接也可以,但是针对的部分不同)
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
1.7.5方法返回地址
- 存放调用该方法的 PC 寄存器的值
- 一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息,这也就回答了上面提到的第二个问题
- 当一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口
- 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定
- 在字节码指令中,返回指令包含ireturn(当返回值是 boolean、byte、char、short和 int 类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用
- 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口
- 方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
- 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者方法继续执行下去
- 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
至此,我们通过介绍虚拟机栈,逐渐解决了之前提到的几个问题,也大致穿插地介绍了函数调用的底层逻辑和具体实现。