Java学习笔记:2022年1月7日
作者:互联网
Java学习笔记:2022年1月7日
摘要:今天的这篇笔记主要是通过讲解基本的JVM结构来理解Java中一些行为的运行机制,进而更加的深入理解Java的运行原理。
@
目录1.Java虚拟机
我们大家在初学Java时都会接触一个叫JVM的东西,这个JVM实际上全名叫做:Java Virtual Machine,即Java虚拟机。JVM属于JRE的一部分,是Java程序能够运行的不可或缺的一个东西,那么JVM到底是什么呢?这里我主要参考了一篇很不错的博文进行学习,大家也可以来看看:点击跳转。文中提到Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。也就是说Java的跨平台特性实际上是它的字节码文件可以在任何装载了Java虚拟机的计算机上运行,而Java虚拟机干的事情就是将字节码文件转变为当前计算机可以识别的机器码,虚拟机中存在这样的文件解释器来进行这个功能。
因此我们现在有了一个基本上正式的理解,Java虚拟机干的事情实际上是在将整个字节码程序转变为计算机可以运行的机器码,而在Java虚拟机中,其将字节码进行翻译的过程,也类似于一个程序运行的过程,我们可以理解为,在JVM中Java程序通过一个和实际运行过程非常相似的过程被翻译成机器码。文中也提到了:JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。也就是说JVM在运行的过程中,确确实实的模拟了程序在计算机中的运行过程,进而翻译成计算机能够理解的机器码,确保计算机能够按照模拟的行为进行运行。
文中提到:粗略分来,JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎。其中这个运行时数据区是Java虚拟机最为重要的部分,今天我们对这里进行着重的研究。
2.虚拟机还是运行时?
很多人在解释虚拟机时,通常会将运行时和虚拟机混淆,这是不准确的,上面也提到了,Java虚拟机和Java运行时不是一个东西,Java运行时是Java虚拟机中主要模拟程序运行的区域,因此这也是最重要的区域,我们对这里进行主要的研究。在这之前,我还是想声明一下:JVM并不是Java运行时,只不过我们在研究JVM的时候主要研究的是运行时,Java运行时不能和Java虚拟机相混淆。Java虚拟机和Java运行时是包含关系,而非同等关系。
3.Java运行时
Java运行时也叫作运行时数据区,它处于内存中,拥有自己完整的体系结构,其主要结构如下:
如图所示,Java运行时在内存中大体分为:方法区,栈区,堆区。其中栈区还分为:虚拟机栈,本地方法栈,程序计数寄存器,他们是被某一个线程私有的,但是现在为了方便理解,我们先不区分到这么详细,我们仅认为:在栈区存在很多栈,每一个栈都代表一个线程,它是被一个线程私有的,就像图片中表示的那样。
现在我们根据这张图片阐述一下类和对象的关系。
4.类和对象的关系
类和对象的关系,实际上有点像图纸和产品的关系,类具备所有对象的特性,根据这些特性制造出来的实例,就是对象,我们可以说:类是对象的母板,对象是类的一个实例。再确确切点可以说:人类就是一个类,而我这个人就是一个好对象;计算机的图纸是一个类,而我们使用的笔记本,台式机等都是这个类的对象。因此我们可以知道,对象是通过类,以类为蓝本制造出来的。
5.程序与进程
进程实际上是运行起来的程序,输入到CPU中参与电路行为的电平信号其实是进程,内存中每个进程都有一块自己的区域,每个进程的区域互不干涉,不一定是连续的,而Java的字节码文件在进行运行时,Java的JRE提供的一个内存进程,就是JVM,而在这个JVM中最核心的模拟运行的部分,就叫Java运行时,如图:
在Java运行时中,主要分为三大块区:方法区,栈区,堆区。其中方法区主要是存放各种类的信息,栈区为各个线程所有,程序的运行就发生在栈区,堆区则是存储了各种各样的对象实体值以及引用类型的的值的信息。使用new语句获得的内存空间,都会在堆区获取一块自己的内存空间,当没有句柄指向它时,Java的自动回收机制会将其删除。
1.方法区
字节码文件被运行起来之后,里边的逻辑信息数据最先进入的就是方法区,也就是说,字节码文件中的各种类的逻辑信息,都会被加载到方法区中,以备栈区中的程序调用操作。方法区中有一块特殊的位置,被称为静态资源区,静态资源区存的是各种静态变量静态方法以及静态类,在一般的方法区中,只是存有各种类的性质,他们并不是真正可以使用的实体,因为他们没有属于自己的内存空间,或者说他们他们只具备特性信息,而没有保存具体特性值的内存空间,因此他们此时是不可以被使用的,只有在实例化之后,他们获得了自己的内存空间之后,他们才可以被调用。它们此时只是位于内存上的预备队列,并没有被写成具备自己内存空间的进程,因此在这个状态下他们无法被执行,自然没有资格注入CPU运行,在方法区中,存储了Java程序所需要的所有的类资源。
静态资源区则与一般方法区不同,位于静态资源区的信息是可以被使用的,因为他们在Java信息进入内存时,就会被虚拟机识别并为它们分配一块自己的内存空间,使得他们可以被使用,这就是静态类型。下面再复述一遍程序位于方法区时与其真正可以被操作时的区别:程序有了属于自己的内存,就可以对其操作,如果没有就不行。程序位于方法区时,通常只有自己的蓝图,只有自己的创建方法的代码,还称不上是一个线程或者说是一个动态可用的东西,也就是类信息,没有自己的实体内存资源区,没有属于自己的内存,而在方法区还有一个静态资源区,凡是被static标识符修饰的东西,在类被加载到方法区时,都会在静态资源区被加载,获得自己的内存,因此就变得可以直接被使用了,变成了可使用的东西。变量也好函数也好,有的类里边的属性加上了static,于是不用new这个类,就可以被调用。(面试点!)
因此被加载到静态资源区的方法,属性,都是相当于获得了自己的内存,可以直接被调用,如静态方法可以不用实例化其所在的对象,直接使用类名进行调用即可,静态变量也不用实例化,直接使用类名进行调用取值即可。注意,被static修饰的变量就会变成一个全局变量,在整个程序中它一直可以通过类进行调用且是唯一的,整个程序中它不会因为对象被实例化而导致背个对象都有不同的该属性,这个属性为对象共用,且可以通过对象或者类来调用。
静态变量就不属于对象了,属于类,通过类可以直接调用这个静态变量,同时也可以使用对象调用这个静态变量。因此对所有对象来说,静态变量是一致的,但实际上是因为这个变量是属于类的。
现在我们知道,方法区存在两个部分,其中一个部分是一般方法区,这里存储的是各种类信息,他们是不具备自己的运行内存的,只具备一个特征,而位于静态资源区的方法,变量甚至类,都拥有了自己的运行内存,可以直接进行调用,需注意的是,存在静态内部类,目前不研究这个知识点。
那么位于一般方法区的类信息,如果想要变得可以被运行,那它需要怎么办呢?它需要进行实例化。实例化是根据一个普通的类,不能运行的特征信息得到一个可以使用的对象的关键所在,使用实例化,系统会按照这个类的特征,制造一个具备自己内存空间的实体,也就是类,它会在堆空间得到一块属于自己的空间,在这个空间里,它可以存储自己的属性,以及存储自己的方法代码,就像根据蓝图制造出了一个真正可以使用的商品一样。静态的东西内存空间在方法区,非静态的内存就在堆中。使用new语句可以为一个类的实例分配堆空间。
2.栈区
栈区是方法进行运行的区域,同时也是Java运行时中最为重要的一个块区。栈区表现为一个一个的栈,每一个栈都代表着一个线程,每个栈是被单个线程拥有的。通常我们写的程序都是单线程的,但是当我们使用多线程写法,在栈区会为每一个线程分配一个栈,线程越多,栈越多,但是线程过多时,就会产生栈溢出的风险,现在我们利用栈区来详细说明一下Java中模拟方法的运行。
我们都知道,Java在进行方法调用时,被调用的方法的内部变量无法在外边使用,同时变量的先后定义顺序也会影响到变量的使用情况,如先定义的变量可以在后边使用,但后定义的变量无法在前边使用,这是为什么呢?实际上,原因就是Java程序运行的机制,Java程序的运行,实际上就是在一个栈中,如图:
如图所示的栈区,就是java程序主要运行的位置,接下来我们来探讨程序在栈区上运行的过程,我们编写如下代码并运行:
public class Text{
public static void main(String[] args){
m1();
m2();
}
public static void m1(){
System.out.println("m1");
m2();
m3();
System.out.println("m1");
}
public static void m2(){
System.out.println("m2");
m3();
m4();
System.out.println("m2");
}
public static void m3(){
System.out.println("m3");
m4();
System.out.println("m3");
}
public static void m4(){
System.out.println("m4");
m5();
System.out.println("m4");
}
public static void m5(){
System.out.println("m5");
System.out.println("m5");
}
public static void m6(){
System.out.println("m6");
System.out.println("m6");
}
}
这个代码的Text类实际上就是方法区的Text类,我们注意到它有一个主方法和6个自定义方法,各种方法如代码所示,现在我们运行主方法并模拟出整个代码的运行过程:
1.主方法入栈
首先主方法在静态资源区,可以直接调用运行,主方法运行,系统为主方法线程分配了一个栈空间,主方法入栈。(主方法入栈实际上是把主方法中的一些状态变量,方法环境记住,将一个方法的所有特征保存,这就是入栈。)之后,之方法开始运行,并调用了m1()。
2.m1()第一次被调用
现在方法m1入栈了,main不再执行,执行的是m1。根据代码,我们发现m1先是进行了一次输出,然后又调用了m2,此时输出是这样的:
m1
3.m2的第一次调用
此时栈区是这样的,m2方法入栈了,注意这个方法是m1调用的,m2方法先是进行了一次输出,然后又调用了m3,此时输出如下:
m1
m2
4.m3的第一次调用
此时的栈区是这样的,m3压入栈,取代了m2的栈顶地位,开始运行,m3先是进行了一次输出,然后调用了m4,现在系统的输出是这样的:
m1
m2
m3
5.m4的第一次调用
现在的栈区是这样的,被m3调用的m4取代了栈顶,开始执行,m4先是进行了一次输出,然后调用了m5,现在的系统输出如下:
m1
m2
m3
m4
6.m5的第一次执行
现在的栈空间如图所示,m5取代了栈顶,开始执行,m5并没有继续调用任何函数,而是进行了两次输出,就结束了,方法结束之后,会被弹出栈,其中的任何变量都会出栈,烟消云散,现在的系统输出为:
m1
m2
m3
m4
m5
m5
7.m5出栈之后,m4重新成为栈顶
如图所示,现在m4又是栈顶了,我们发现m4在调用完m5之后,并没有继续调用其他方法,而是输出了一次之后结束了,现在的系统输出为:
m1
m2
m3
m4
m5
m5
m4
8.m4出栈之后,m3继续执行
现在的栈区是这样的,我们发现m3在调用完m4之后,没有调用其他方法,输出了一次之后结束了,现在的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
9.m3出栈后,m2重新执行
m2在调用完m3之后,又调用了m4,因此m4会入栈,成为新的栈顶,这是m2没有进行新的输出,因此系统输出没变。
10.m4第二次入栈
现在的栈区如图,m4又被调用了,m4显示输出了一次,之后又调用了m5,此时的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
11.m5的第二次执行
此时m5成为栈顶,m5没有调用任何方法,在进行两次输出之后出栈了,现在的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
12.m5出栈后,m4继续执行
此时的栈空间如图所示,m4调用完了m5后,进行了一次输出就结束了,此时系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
13.运行权又回到了m2
在m4运行结束之后,m4出栈,而此时栈顶成为了m2,m2再一次拥有了运行权,这一次,m2在调用完m4之后,没有再调用任何方法,输出了一次之后也结束了,控制权有回到了m1。现在的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
14.m1又开始执行
此时栈区的情况是这样,m1在调用完m2之后,又调用了m3,同时没有进行任何输出,因此系统输出没有任何变化,而m3将开始运行。
15.m3的第二次调用
此时的栈区如图所示,m3取代m1成为了栈顶,m3在进行一次输出后,调用了m4,此时的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
16.m4的第二次执行
此时的栈空间如图所示,m4重新成为了栈顶并开始执行,m4先是进行了一次输出,然后调用了m5,此时的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
m4
17.m5的第三次执行
此时的栈空间如图所示,m5被调用进来,成为了新的栈顶。m5并没有再调用任何方法,而是进行了两次输出之后便结束了,弹出栈,当前的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
m4
m5
m5
18.m4重新成为栈顶
此时的栈空间如图所示,m4重新取得运行权,m4在调用完m5之后并没有调用其他方法,而是输出一次之后结束了,此时的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
m4
m5
m5
m4
19.m4出栈后,m3重新成为栈顶
m3在调用完m4之后,并没有进行任何其他调用,而是在输出一次结束了,此时的输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
m4
m5
m5
m4
m3
20.m1重新成为栈顶
此时m1又成为了栈顶,m1在调用完m3之后没有进行任何调用,耳熟输出了一次就结束了,至此的系统输出为:
m1
m2
m3
m4
m5
m5
m4
m3
m4
m5
m5
m4
m2
m3
m4
m5
m5
m4
m3
m1
21.运行权重新交到了main手上
main方法在之后没有进行其他更多的操作了,直接结束了。
最终main方法也执行结束,整个栈清空,这个线程的寿命宣告终结,空间被释放。
3.栈的小结
因此,我们知道了Java程序运行的原理,方法在栈区被执行,方法调用会引起压栈,被新调用的方法会被压栈并成为栈顶,栈顶的方法是被执行的方法,方法是在线程栈的栈顶被执行的。而主动调用某个方法的方法此时就会被往下压,数据被保存起来,当栈顶方法执行完毕并被弹出时,它再执行。
栈可以有很多个,在Java的多线程编程中,一个栈实际上就代表一个线程,当我们编写了多个线程进行运行时,这些线程就会被系统在栈空间内分配属于自己的栈,并开始进行执行。
4.方法内部的情况
在使用栈结构执行程序时,方法之间的调用表现为出栈入栈的操作,而方法内部的变量声明,实际上也是一个出栈入栈的操作,如:
public class Text{
public static void main(String[] args){
int a = 9;
int b = 8;
if(a > b){
int k = m1();
}
for(int i = 0; i<2; i++){
int s = 8;
if(i == 1){
int f = 8;
}
}
}
public static void m1(){
int a = 10;
int b = 8;
if(a>b){
a = 4;
int k = 3;
}else{
int h = 8;
}
return a +b;
}
}
现在的情况是这样,这个程序在系统中会如何执行呢?
1.主方法部分
首先主方法入栈:
栈空间如图所示,之后主方法中的变量会按照顺序,同样以入栈的方式进入主函数,主函数整体上也是一个栈,它是变量们的栈。
之后,在系统中,会让两个变量进行对比,并进行下一步的操作,可见a大于b,因此需要声明一个变量k,而在k的值便是m1,要是想得到k的值,必须先执行m1,因此此时m1入栈,成为新的栈顶方法。
此时的栈如图所示。进入m1后,在m1中,有定义了两个变量,因此栈区变为下边的样式
可见,在m1中,有定义了新的两个变量,之后二者进行比较,并发现a大于b,因此要进行a大于b下的操作,即先把a赋值为4,在声明一个变量k,k为3,栈区变为这种样子:
然后,计算a加b的值,然后将这个值返回到调用位置,并出栈,出栈后只有返回值被调用位置处的k保存,栈区为:
之后main方法继续向下执行,遇到一个循环,注意,循环语句是一个语句块,在语句块中定义的任何变量也都单独属于一个栈,这个语句块结束后,这个栈也会被弹出,与方法调用不同的是,这个栈属于main方法的子栈,因此有:
for语句块开始执行,栈创建成功后,for内定义了一个i变量,并开始第一次循环,第一次循环中,i = 0,且创建了一个变量s = 8。栈空间为:
之后进行了一次判断语句,此时i = 0,并不等于1,因此不执行语句,至此第一轮循环结束,循环进入第二轮,i在第一轮循环结束之后发生自增,且在第二轮循环的开头又发生了一次s的定义,但是之前已经有s的定义了,因此这里不会再次重复定义。栈状态如下:
之后发生了一次判断语句,这次判断生效了,创建了一个新的变量f = 8,栈状态为:
之后第二轮循环结束,i变为2,此时短时间内栈空间的i是2,但在这之后不久就会一道循环条件的判别结束,for语句块结束,而整个for语句块出栈,里边的所有变量全部失效。与此同时整个过程也结束了,栈清空并被回收。
因此我们知道,对于一个方法内的变量,也是以栈的形式存放的,变量的声明先后顺序,就是它们入栈的先后顺序,而语句块在方法中,也算是一个独立的栈,一个语句块的引入相当于引入了一个整体,这个语句块本身也是一个栈元素,而其内部也是一个栈,内部的变量声明仍然满足外部模式,需要注意的是,语句块在使用结束后,会像方法那样出栈,里边的变量也会随之出栈,这也是语句块中声明的变量无法在块外使用的原因,无论是循环语句还是块,只要是有大括号,那就是块,里边的变量在外边是无法使用的。
3.堆区
1.类的实例化以及赋值过程
堆区也被称为堆内存,堆区是存储实例的真实值的地方,在这里我们首先要知道:Java中存在基础类型变量和引用类型变量。如上面的栈区讲解所示,在程序运行时,栈上边会有各种变量进行压栈,每一个变量都有自己的句柄以及值,句柄的位置被称为引用地址,系统通过引用地址找到句柄以及值,进而确定变量,其中基础类型的变量的值和句柄是相邻的,而引用类型的句柄后边存放的只是一个地址,这个地址实际上是指向了堆内存中的一个地址,这个地址上的值才是引用类型的值,这样一来,引用类型并没有和其真实值相邻,而是通过类似指针的方式引导到了一个堆地址上,因此它被称为引用类型,如下面的代码:
public class Person{
public int age;
public int height;
public String name;
public static int flag;
public static void m1(){System.out.println("社会你虎哥,我他妈来辣!");}
public void m2(){}
}
public class Thum{
public static void main(String[] args){
Person a = new Person();
Person b = new Person();
a.age = 10;
a.height = 140;
a.name = "杀马特团长";
b.age = 11;
b.height = 150;
b.name = "黑牛";
}
}
在主方法线程中,我们首先定义了一个a类型的person,其次我们定义了一个b类型的person,然后我们为a的age属性赋值为10,a的身高属性赋值为140,a的姓名属性赋值为“杀马特团长”,之后为b的姓名属性赋值为11,b的身高属性赋值为150,b的名称属性赋值为“黑牛”。那这个过程是怎样的呢,如下图:
这个过程并不比上边的栈运行复杂,我们看到,首先我们在栈空间的主线程中,压入了方法main,代表着方法main开始执行了,其次我们在某个时间点,实例化了person类,并为其赋予一个句柄a,实例化实际上就是系统对方法区的类信息进行复制,并在堆内存上为其开辟一块空间,如图这个过程已经实现,a句柄指向了堆空间中的一个地址为462的内存地址;之后,b也入栈了,它被赋予了一块位于地址654的存储空间并将这个地址给到了b句柄中。之后改变值的操作是在哪里执行呢?实际上这个操作没有在栈中执行,而是直接在堆区进行执行,系统会根据a的指向在堆区找到462地址,然后根据相关的属性名进行修改,之后根据b句柄的地址找到其堆区的值信息,然后进行修改,这就是对象实例化并进行属性修改的过程。
对象的实例化本质上就是Java运行时程序在方法区内找到一个类,对其进行复制并赋予堆空间,并将它的地址赋予给一个句柄,需要注意的是,在堆空间没有句柄对应的值信息会被认为是废料被Java删除。引用类型的值实际上都在堆空间中,而没有和他们相邻在栈区中。
1.关于引用类型的一个小重点
在栈空间中,变量的存储是通过引用地址-句柄-值的方式进行存储的,引用地址不要和引用类型指向的地址相混淆,引用地址是栈空间中的变量地址,而引用类型变量值的地址是堆内存中的地址,这里一定要记住。
在引用类型变量中,它的值并不是逻辑上的它的值,它的真实值是一个地址,指向了它的逻辑上的值,也就是说引用类型变量是一个类似指针的东西,它表示的值是它指向的,在堆空间中的一个值,如果使用引用类型A,和B作为参数传入到一个方法中,在这个方法中,让A和B的指向发生交换,那么在函数调用结束后,A,B的指向仍然不会发生改变,这是因为在方法中,只是形参进行了指向的改变,承接A和B的形参交换,并没有让A,B的真实值发生交换,而指向的交换也不会引起堆地址中值的变化,因此这是没有用的。
Person a = new Person();
Person b = new Person();
change(a,b);
public static void change(Person a, Person b){
Person temp = new Person();
temp = a;
a = b;
b = temp;
}
也就是类似这样的操作,这个方法不会让a,b发生交换,二者的指向也不会变,在方法change中,形参a,b的指向确实变化了,但是他们是被复制的,这个方法在运行时,会在栈区复制他们,并作为形式参数传进来,被改变的知识复制品,复制品的指向交换首先不会影响到原句柄,更不会影响堆中地址,因此这个交换没有任何意义。然而引用地址的好处是,形式参数的指向也是堆地址,也就是说尽管它是一个复制品,但它的地址指向和原句柄是一样的,尽管修改复制品的地址指向没意义,但通过复制品可以确确实实的访问到堆上边的真实值信息,堆中的真实值是唯一的,这样一来,就可以通过方法对原句柄的值进行修改,这是一个很重要的特性,在C语言中通常使用指针来实现这一功能。因此我们需要记住,引用类型的句柄并没有挨着自己的真实值,而是指向了自己的真实值,传参以及等号赋值赋予复制品的都是地址,他们的指向在这种浅拷贝下,都是唯一的。
6.笔记原文
类和对象的关系,类就是图纸,象是实体
进程实际上是运行起来的程序,输入到CPU中参与电路行为的电平信号其实是进程,内存中每个进程都有一块自己的区域,每个进程的区域互不干涉,不一定是连续的。
Java占据的区域叫java运行时JVM
第一块叫做方法区,方法区里边加载了所有的类。所有的文件进入内存后都被保存进了方法区,他们相当于设计图纸。
实例化:new操作
Java信息首先进入的是方法区,所有的类信息会存在JVM的方法区内。
在内存上的程序不是进程,而是进程所需要的资源,或者说是进程未进入CPU的预备队列,只有在CPU上跑起来的电平信号才是进程的物理形态。
Java运行时就是Java进程对应的资源区,为真个Java提供支持的内存部分,或者说是还未被注入CPU的Java程序。在整个Java进程的运行过程中,Java运行时为Java进程提供资源,方法区存储了所有的Java类信息。
Java还存在栈区域,堆区
栈区存在真正的栈数据结构,该结构为函数调用提供方便。在函数调用时,如果某函数内调用其他函数,就会将当前函数信息压入栈。
方法执行是不是立即执行,而是被拷贝执行,被拷贝到栈顶执行,开始执行就是入栈,执行完毕就是出栈
整个程序的执行顺序,是由栈来控制的,开始执行的时候相当于入栈,执行完毕的时候相当于出栈。
实际上,程序就是在栈空间运行。
线程的本质就是栈,整个函数包括其各种行为在栈里边的整个出入栈过程,叫做一个线程。也就是说进程中的一个问题解决,实在栈区运行,这个问题在栈里边所有的行为就叫线程。这个栈不是唯一的,多线程其实指的就是多个栈同时进行程序吞吐。栈是会增加的,如果栈过多会导致栈溢出。
Java运行时
Java进程有一个在内存上属于自己的资源区,其中包括方法区,栈区,堆区。栈区是程序运行的地方,栈区中会存在一个或者多个栈,当程序中的方法开始运行,会向栈中拷贝一份,压入到栈顶,如main开始运行,则拷贝一份main并将其压入到栈顶,main调用m1函数,这时拷贝一份m1函数并压入到栈顶,m1开始调用m3函数,这时拷贝一份m3函数压入到栈顶,m3执行结束,则弹出栈,然后m1成为栈顶,继续执行。每个时刻仅有栈顶的函数在执行,某一个主方法在栈中执行导致其他方法进栈出栈的整个过程就叫做线程,这个栈就是一个线程。多线程是存在多个这样的栈,当栈太多时会发生栈溢出。
程序的单个执行也是栈,当其作用域结束,里边所有的变量都会出栈并释放
public class Thum{
public static void main(String[] args){
m1();
m2();
}
public static void m1(){
System.out.println("m1");
m2();
m3();
System.out.println("m1");
}
public static void m2(){
System.out.println("m2");
m3();
m4();
System.out.println("m2");
}
public static void m3(){
System.out.println("m3");
m4();
System.out.println("m3");
}
public static void m4(){
System.out.println("m4");
m5();
System.out.println("m4");
}
public static void m5(){
System.out.println("m5");
System.out.println("m5");
}
public static void m6(){
System.out.println("m6");
System.out.println("m6");
}
}
返回值的意思就是在出栈的时候把在它里边的值返回到调用它的位置。
在一个方法运行期间,它也是一个小栈,具体的栈元素实际上就是作用域,相同层的变量位于一个栈元素内,一旦遇到一个作用域,就会压一层栈,作用域结束后,栈就会弹出,里边的变量就会失效。出栈是句柄出栈,仅仅将句柄进行删除,通常来讲基本类型的值也会被栈回收,然而引用类型的值会被堆回收,引用类型的值可能会被保留一段时间,因为可能存在他人引用的情况。
在方法内的作用域的使用,也是栈的运用,其实现也是因为栈,因此我们注意,作用域内的变量在外边用不了,其实是被回收了。
作用域就是用栈实现的,块也是一种作用域,在块结束之后,也会被弹出,也就是说块外边的也不能用块里边的变量。所有作用域都满足这个机制。
public class Thum{
public static void main(String[] args){
int a = 9;
int b = 8;
if(a > b){
int k = m1();
}
for(int i = 0; i<2; i++){
int s = 8;
if(i == 1){
int f = 8;
}
}
}
public static void m1(){
int a = 10;
int b = 8;
if(a>b){
a = 4;
int k = 3;
}else{
int h = 8;
}
return a +b;
}
}
程序有了属于自己的内存,就可以对其操作,如果没有就不行
程序位于方法区时,通常只有自己的蓝图,只有自己的创建方法的代码,还称不上是一个线程或者说是一个动态可用的东西,也就是类信息,没有自己的实体内存资源区,没有属于自己的内存,而在方法区还有一个静态资源区,凡是被static标识符修饰的东西,在类被加载到方法区时,都会在静态资源区被加载,获得自己的内存,因此就变得可以直接被使用了,变成了可使用的东西。变量也好函数也好,有的类里边的属性加上了static,于是不用new这个类,就可以被调用。这里是非常重要的面试点!很重要的知识!
在静态方法中不可调用非静态方法,其实是因为非静态方法还不存在,还没有属于自己的内存空间,还没有成为可以使用的资源,包括变量也是。
静态变量就不属于对象了,属于类,通过类可以直接调用这个静态变量,同时也可以使用对象调用这个静态变量。因此对所有对象来说,静态变量是一致的,但实际上是因为这个变量是属于类的。
静态的东西内存空间在方法区,非静态的内存就在堆中。用new的方式将非静态的东西变成可以使用的,为其分配自己的空间
对象是按照类创造出的个体,类内信息尽在方法区存在时,没有自己的内存,在静态资源区算是有自己的内存,在栈区也有自己的内存,在堆区 也有自己的内存
静态的变量与方法不含在对象中,通常基本变量的值信息会直接保存在栈区,但是复杂的引用类型的值信息会保存在堆区,堆区专门放引用类型的值信息,而留在栈中的是引用类型的句柄,句柄相对应的值在堆区中。
以上提到的几个区是给java进程中各种小物件分配内存的地方。
主要有三部分值得画图理解的地方,把这三个地方好好看看
创建对象的过程就是使用new方法,根据方法区的类信息,在栈中创建不同的句柄后,为每个句柄按照相应的要求在堆区创建实体值,这些实体值就是对象的主体,句柄就是这个对象的宏观上的名字,一个句柄指向着对象的实体值部分,我们广义上理解的对象本质上是一个句柄和一个实体值的整体。实体值是多个不同的,对应着不同句柄的,互不相关的,具备相似性质的,就像照着坦克设计图,制造了虎式1和虎式2。
静态类直接实例化在静态方法区,它就存在于静态方法区,静态类没有办法被实例化,调用的话直接就调用就行了,它可以直接被使用,因为它已经在静态方法区了。
new出来的对象属性都有默认值,基本类型是0,布尔是false,引用类型是null,方法里面直接声明的变量没有默认值。
实例化的过程就是被分配给一块内存变得可以使用的过程,它里边的东西就有依托了,可改写,可操作了。
类就相当于人类的生物母板,对象就是真正的人,而人的具体属性想要被操作,必须要真的有这么一个人,因此生一个人出来就是分配内存,想要给一个人起名字,首先要生一个人。人与人是不同的,每个人都有自己的信息,这就是不同的对象之间的差异,面向对象就是在模拟这个过程。
public class Person{
public int age;
public int height;
public String name;
public static int flag;
public static void m1(){System.out.println("社会你虎哥,我他妈来辣!");}
public void m2(){}
}
public class Thum{
public static void main(String[] args){
Person a = new Person();
Person b = new Person();
a.age = 10;
a.height = 140;
a.name = "杀马特团长";
b.age = 11;
b.height = 150;
b.name = "黑牛";
}
}
方法区内一个程序方法整体(例如Public class Person创建的Person,里面包含age、height等方法)是类,以类为模板在堆里创建的个体就是对象。栈中的是引用句柄。 理解 实际上 Java就是类语言,Java的程序就是在类里边写的
标签:调用,Java,2022,笔记,m5,m4,m1,m3 来源: https://www.cnblogs.com/B307-NCAI/p/16053472.html