java学习

java学习

Posted by DYC on April 15, 2024

语法基础

程序本质
编程语言

编程语言可以分为三类

  • 编译型语言

    类似于c++,代码会事先被编译成机器指令(可执行文件),然后再交给CPU执行,在执行时,CPU面对的是已经编译好的指令,直接执行即可。

    但由于不同型号的CPU指令集不同,所以编译好的可执行文件只能在特定的操作系统和机器上执行

  • 解释型语言

    类似Python,代码并不会被事先编译成机器指令,而是在执行的过程中,由Python虚拟机(也叫做解释器)逐条取出程序中的代码,然后编译成机器指令,交由CPU执行,执行完成之后,再取出下一条代码,重复上述的编译、执行过程

    相对于编译型,其执行速度较慢,因为需要一边编译一边执行,所以程序执行的时间要加上编译的时间,但可移植性高,因为虚拟机可以根据当前所在环境,将代码编译成不同的CPU指令

  • 混合型语言

    java会将代码编译成字节码,字节码是Java代码和机器码之间的一种中间状态,可以被快速的翻译成机器码,在执行过程中还是按照解释执行的,即字节码被逐行读出,然后翻译成机器码,再交给CPU执行,只不过由字节码转到机器码耗时更短,这样既保证了可移植性性,也减少了耗时

    实际上在解释执行的过程中,Java会将热点字节码(反复多次执行的代码)编译成机器码缓存起来,以供反复使用,这就是JIT编译

操作系统、虚拟机

操作系统在程序执行的作用

  • 操作系统用来管理硬件资源和调度程序的执行
  • 操作系统还担当了类库的作用。对于通用的功能代码,比如读写硬盘等,没必要在每个程序中都从零编写一遍。操作系统将这些通用的功能代码,封装成API(专业名称叫做系统调用),供我们在编写应用程序时直接调用。

虚拟机在程序执行中的作用

1
2
3
4
5
6
7
8
9
10
// C++
$ g++ helloword.cpp -o helloworld
$ ./helloword

// Python
$ python helloworld.py

// Java
$ javac HelloWorld.java
$ java HelloWorld

对于使用解释型和混合型语言编写的代码,其执行过程都需要虚拟机的参与

虚拟机本质还是一个程序,并且是CPU可执行的机器指令,程序员编写的代码相当于嵌套在虚拟机中,只不过不能直接交给CPU执行

当CPU执行虚拟机代码时将应用程序的字节码编译成CPU指令,放在固定的内存位置,再修改IP寄存器(存储CPU将要执行的指令位置),来引导CPU去执行这块位置的CPU指令

站在操作系统和CPU的角度,Java程序编译之后的字节码跟虚拟机合并在一起,才算是一个完整的程序,才相当于C++编译之后的可执行文件。CPU在执行程序员编写的代码的同时,也在执行虚拟机代码,并且是先执行虚拟机代码,然后才引导执行程序员编写的代码

CPU指令、汇编语言、寄存器

CPU指令

CPU指令、机器码、机器指令,实际上都是一个东西,就是CPU可以执行的操作

一条CPU指令包含的信息主要有:操作码、地址、数据三种,分别指明所要执行的操作数据来源和去向、数据本身

汇编语言

在计算机发展的早期,程序员直接使用机器码来编写程序,但由于机器码是二进制码,编写起来机器复杂,所以才有了汇编语言,由一系列汇编指令组成,汇编指令和机器码一一对应,将汇编语言编译成机器码就可以被CPU执行

C/C++语言的编译过程,实际上也包含汇编这一过程。编译器会先将C/C++代码编译成汇编代码,然后再汇编成机器码

寄存器

内存的读写速度比cpu指令的执行速度要慢很多,所以为了匹配两个之前的速度,就出现了寄存器,内存中的数据会先读取到寄存器中再参与计算。这样的好处是除了最开始和最后的数据需要在内存中读写以外,中间的计算过程涉及到一些临时结果的存取,都可以在寄存器中完成

CPU指令执行具体过程
  1. 将机器码加载到内存的代码段中,将代码中变量等数据加载到内存的数据段中
  2. CPU根据寄存器中存储的地址,得到下一条指令的地址,进行执行

对于解释型或混合型语言,操作系统将虚拟机本身的机器码,加载到内存中的代码段。CPU执行虚拟机代码,将程序编写的代码解释为机器码,并放入某块内存中,然后将PC寄存器的地址设置为这块内存的首地址,于是,CPU就被虚拟机引导去执行程序员编写的代码了

基础语法

变量

内存被划分为一个个的内存单元(一个内存单元为1个字节大小)。每个内存单元都对应一个内存地址,方便CPU根据内存地址来读取和操作内存单元中的数据

变量可以看作是内存地址的别名,在机器码中通过内存地址可以实现对内存中数据的读写,在代码中通过变量来实现对内存中数据的读写,在编译时会将变量替换成内存地址

总的来说,对数据段进行分区,是为了便于管理不同生命周期的变量,设置不同的生命周期是为了有效的利用内存空间,便于变量结束后,能被快速回收,供重复使用

栈存储作用域为“函数内”的数据,如函数内局部变量,函数参数等,生命周期为函数生命周期,函数结束后,所占用的内存就可以释放,供其他变量使用

堆一般存储作用域不局限于“函数内”的数据,如对象等,只有当程序员主动释放或虚拟机判定不再使用时对象对应的内存才会被释放

常量池一般用于存储常量常量的生命周期跟程序的生命周期一样,只有程序结束时,对应的内存才会被释放

重点

在java中,变量存储的位置由其生命周期决定,实例变量属于对象,所以存储于堆中,静态变量属于类,所以存储于方法区中,局部变量属于方法,所以存储于栈中

对于引用类型,引用本身根据生命周期存储于对应的空间中,但引用指向的对象是存放在堆中,这样的好处是因为引用对象可能不只一个方法在引用,可能是多个,放在堆内存中适合管理对象的动态生命周期和多线程环境中的共享访问,并且堆中有垃圾回收机制,使用引用计数法,来清理没有引用的对象

比如说在方法中定义的数组,其在jvm中就存储在两个位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
栈内存:
-----------------------------------
|  栈帧 (method)                  |
|  -----------------------------  |
|  | localArray (引用地址)       |  --> 指向堆内存中的数组对象
|  -----------------------------  |
-----------------------------------

堆内存:
-----------------------------------
|  数组对象 (new int[10])         |
|  -----------------------------  |
|  | int[0]                      |
|  | int[1]                      |
|  | ...                         |
|  | int[9]                      |
|  -----------------------------  |
-----------------------------------

数组

使用数组,我们可以定义一块连续的内存空间,通过数组下标来进行每个元素的访问

当通过下标访问的时候,编译器会将这语句分解为多条CPU指令,先通过变量a中存储的首地址和寻址方式,找到对应的内存地址,再进行访问

类型

根据变量的类型是否可以动态变化和类型检查发生的时期,我们将类型系统分为静态类型和动态类型

  • 静态类型指的是,一个变量的类型是唯一确定的,类型检查发生在编译期。
  • 动态类型指的是,一个变量的类型是可变的,具体看赋值给它的数据是什么类型的,类型的检查发生在运行期

函数

函数底层实现依赖一个非常重要的东西:栈

每个函数都是一个相对封闭的代码块,其运行需要依赖一些局部数据,比如局部变量等,由于函数运行过程中,需要相互调用,对于数据的使用来说,A函数调用了B函数,就会在A方法该位置暂停,使用B函数相关