java基础之深⼊原理(重点)
危机感源于今天的差距,恐惧感来⾃未来5年、⼗年、⼗⼏年的差距。
java基础总结强化
⼀、从底层理解java反射机制
了解java程序从编译到执⾏的全过程,从底层理解学习,java代码编译和执⾏主要包含以下3个重要机制,有助于我们更好的理解学习程序的运⾏机制和原理。
(⼀)java源码编译机制
1、java源码编译是通过java源码编译器完成,将源码⽂件(.java)编译成字节码⽂件(.class即⼆进制⽂件)。
2、java源码编译由以下三个过程组成:
(1)分析和输⼊到符号表;
(2)注解处理;
(3)语义分析和⽣成class⽂件。
3、详细流程如下:
源代码⽂件*.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码⽣成器 -> JVM字节码⽂件*.class
4、class⽂件主要包括两部分:
(1)常量池:记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引⽤(⽅法引⽤,成员变量引⽤等等);编译器将源程序编译成class⽂件后,会⽤⼀部分字节分类存储类字段的名字/所属类型、类⽅法的名字/返回类型/参数名与所属类型、常量,还有在程序中出现的⼤量的字⾯值。
注意:在编译器编译Java源代码时,就已经在字节码中为每个⽅法都设置好了局部变量区和操作数栈的数据和⼤⼩。当JVM⾸次加载⽅法所属的Class⽂件时,就将这些数据放进了⽅法区。(从这可以看出JVM是⼀种解释执⾏class⽂件的规范技术)。
因此在线程调⽤⽅法时,只需要根据⽅法区中的局部变量区和操作数栈的⼤⼩来分配⼀个新的栈帧的内存⼤⼩,并压⼊Java栈。
(2)⽅法区:类中各个⽅法的字节码。
注意:事实上,只有JVM加载class后,在⽅法区中为它们开辟了空间才更像⼀个“池”。
(⼆)类加载机制
1、JVM(java虚拟机)的类加载是通过ClassLoader及其⼦类来完成的,默认采⽤的是双亲委派机制。
2、双亲委派机制:类的加载是⾃顶向下加载,检测该类是否加载是从下往上检查是否已加载,即就是某个特定的类加载器在接到加载类的请求时,⾸先将加载任务委托给⽗类加载器,依次往上传递加载请求 (本质上就是loadClass函数的递归调⽤)。因此,所有的加载请求最终都会传到顶层的启动类加载器中。如果⽗类加载器已完成这个类加载请求,就成功返回;只有当⽗类加载器⽆法完成此加载请求时,⼦加载器才会尝试⾃⼰去加载。
注意:双亲委派机制保证了类加载过程的安全性,避免⿊客利⽤同名类植⼊程序。
3、JVM预定义的三种类型的类加载器:
(1)启动(Bootstrap)类加载器:负责加载$JAVA_HOME中jre/lib/rt.jar⾥所有的class,由C++实现,不是ClassLoader⼦类;
(2)扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-dir指定位置中的类库加载到内存中。
(3)系统(System)类加载器:系统类加载器是由 Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将⽤户类路径(java -classpath或-Djava.class.path变量所指的⽬录,即当前类所在路径及其引⽤的第三⽅类库的路径下的类库 加载到内存中。开发者可以直接使⽤系统类加载器。
(4)Custom ClassLoader(⾃定义类加载器):程序员可以根据⾃⾝需要⾃定义类加载器。
注意:JVM主要在程序第⼀次主动使⽤类的时候,才会去加载该类。也就是说,JVM并不是在⼀开始就把⼀个程序就所有的类都加载到内存中,⽽是到不得不⽤的时候才把它加载进来,⽽且只加载⼀次。
(三)类执⾏机制
1、JVM是基于栈的体系结构来执⾏class字节码⽂件。
2、当线程创建后,JVM根据字节码为类的各种信息分配内存区域,分别为程序计数器、栈、堆和⽅法
区。
(1)程序计数器(PC)存放下⼀条要执⾏的指令在⽅法内的偏移量即当前线程所执⾏的字节码的⾏号指⽰器,存储着下⼀条被执⾏的指令地址。
(2)堆⽤于存储所有类实例和数组对象。
注意:堆内存中类实例有指向⽅法区中类⽅法的指针。
(3)栈⽤来存放基本类型的对象和⾃定义对象的引⽤。每启动⼀个线程,JVM都会为它分配⼀个Java栈,⽤于存放⽅法中的局部变量,操作数以及异常数据等。
(4)⽅法区⼜称为静态区,和堆⼀样被所有线程共享,⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
注意:⽅法区属于堆内存。
注意:(1)String str = "hello"; 先在内存中是不是有"hello"这个对象,如果有,就让str指向那个"hello".如果内存⾥没有"hello",就创建⼀个新的对象保存"hello",即这样定义的变量所有引⽤指向同⼀对象。(2)String str=new String ("hello") 就是不管内存⾥是不是已经有"hello"这个对象,都新建⼀个对象保存"hello"。
3、详解栈内存数据结构分为三部分:局部变量区、操作数栈和帧数据区。
(1)局部变量区:⽤来存放⽅法中的所有局部变量值,包括传递的参数。
(2)操作数栈:⽤来存储在执⾏指令时产⽣和使⽤的中间结果数据。
(3)帧数据区:⽤来存储常量池的解析、正常⽅法返回以及异常派发机制的信息数据
(四)实例说明java源码编译执⾏的过程
1、在编译好java程序得到MainApp.class⽂件后,在命令⾏上敲java AppMain。系统就会启动⼀个jvm进程,jvm进程从classpath路径中到⼀个名为AppMain.class的⼆进制⽂件,将MainApp的类信息加载到运⾏时数据区的⽅法区内,这个过程叫做MainApp类的加载。
2、然后JVM到AppMain的主函数⼊⼝,开始执⾏main函数。
3、main函数的第⼀条命令是Animal animal = new Animal("Puppy");就是让JVM创建⼀个Animal对象,但是这时候⽅法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到⽅法区中。
4、加载完Animal类之后,Java虚拟机做的第⼀件事情就是在堆区中为⼀个新的Animal实例分配内存, 然后调⽤构造函数初始化Animal实例,这个Animal实例持有着指向⽅法区的Animal类的类型信息(其中包含有⽅法表,java动态绑定的底层实现)的引⽤。
5、当使⽤animal.printName()的时候,JVM根据animal引⽤到Animal对象,然后根据Animal对象持有的引⽤定位到⽅法区中Animal 类的类型信息的⽅法表,获得printName()函数的字节码的地址。
6、开始运⾏printName()函数。
(五)反射机制原理
1、反射机制原理:在程序运⾏状态下,可以动态获取或者动态调⽤任意⼀个类的⽅法和属性。
注意:java反射本质:通过Class类实例获取指定类的字节码信息,进⽽获取或者调⽤该类的属性和⽅法。底层就是Class实例存储指向⽅法区中存储着该类⽅法的指针,进⽽调⽤该类的⽅法。
2、获取Class类实例的三种⽅法:
java重写和重载的区别(1)利⽤Object的getClass()⽅法,返回Object运⾏时的实例。
(2)利⽤类字⾯常量:通过类名.class获取class对象。
(3)利⽤Class类的forName(String 类全名)⽅法获取指定类的实例,其中,类全名=包路径+类名。(常⽤⽅法)
总结:第⼀种已经创建了对象,那么这个时候就不需要去进⾏反射了,显得有点多此⼀举。第⼆种需要导⼊类的包,依赖性太强。所以我们⼀般选择第三种⽅式。
3、利⽤java反射机制获取指定类的⽅法并调⽤该⽅法:
(1)getMethod(String ⽅法名,Class ⽅法参数)⽅法返回指定⽅法名的⽅法对象。
(2)getMethods()⽅法返回指定类的⽅法对象数组。
(3)Method类的invoke(Object obj, args):对带有指定参数的指定对象调⽤由此 Method 对象表⽰的底层⽅法。
注意:调⽤本质:Method类实例即⽅法对象存储着指向该类内存⽅法区⽅法的指针,进⽽直接调⽤该⽅法。
java绑定机制
JVM中⽅法的调⽤是通过静态和动态绑定机制实现的。其中,java的多态和向上转型都是借助动态绑定实现的,当我们理解了动态绑定就能搞定多态和向上转型。
(⼀)静态绑定机制
静态绑定是在程序执⾏前⽅法就已经被绑定即在编译过程中就已经知道被调⽤的⽅法到底是哪个类中的⽅法。
1、java当中的⽅法只有final、static、private修饰的的⽅法和构造⽅法是静态绑定的。
(1)private修饰的⽅法:private修饰的⽅法是不能被继承的,因此⼦类⽆法访问⽗类中private修饰的⽅法。所以只能通过⽗类对象来调⽤该⽅法体。因此可以说private⽅法和定义这个⽅法的类绑定在了⼀起。
(2)final修饰的⽅法:可以被⼦类继承,但是不能被⼦类重写(覆盖),所以在⼦类中调⽤的实际是⽗类中定义的final⽅法。
注意:使⽤final修饰⽅法的两个好处:(1)防⽌⽅法被覆盖;(2)关闭java中的动态绑定。
(3)static修饰的⽅法:可以被⼦类继承,但是不能被⼦类重写(覆盖),但是可以被⼦类隐藏。
注意:当⼦类对象向上类型转换为⽗类对象时,不论⼦类中有没有定义这个静态⽅法,该对象都会使⽤⽗类中的静态⽅法,因此这⾥说静态⽅法可以被隐藏⽽不能被覆盖。这与⼦类隐藏⽗类中的成员变量是⼀样的。隐藏和覆盖的区别在于,⼦类对象转换成⽗类对象后,能够访问⽗类被隐藏的变量和⽅法,⽽不能访问⽗类被覆盖的⽅法。
(4)构造⽅法:构造⽅法也是不能被继承的,因此编译时也可以知道这个构造⽅法⽅法到底是属于哪个类的。
注意:因为⼦类是通过super⽅法调⽤⽗类的构造函数,或者是jvm⾃动调⽤⽗类的默认构造⽅法。
2、静态绑定总结:
在编译器阶段就已经指明了调⽤⽅法在常量池中的符号引⽤,JVM运⾏的时候只需要进⾏⼀次常量池解析即可。
(⼆)动态绑定
动态绑定是指在运⾏时期根据具体对象的类型进⾏绑定。
1、若⼀种语⾔实现了后期绑定,同时必须提供⼀些机制,可在运⾏期间判断对象的类型,并分别调⽤适当的⽅法。
注意:编译器此时依然不知道对象的类型,但⽅法调⽤机制能⾃⼰去调查,到正确的⽅法主体。不同的语⾔对后期绑定的实现⽅法是有所区别的,但我们⾄少可以这样认为:它们都要在对象中安插某些特殊类型的信息进⾏标识。
2、动态绑定的过程:
(1)JVM虚拟机获取对象实际类型的⽅发表;
(2)JVM通过常量池解析获取在⽅法表中指定⽅法的定位;
(3)通过直接地址到该⽅法字节码所在的内存空间,调⽤⽅法。
注意:java中重载的⽅法使⽤静态绑定,重写的⽅法使⽤动态绑定。
3、动态绑定总结:
根据对象的声明类型(对象引⽤的类型)到“合适”的⽅法。具体步骤如下:
(1)如果能在声明类型中匹配到⽅法签名完全⼀样(参数类型⼀致)的⽅法,那么这个⽅法就是最合适的。
(2)在第1条不能满⾜的情况下,寻可以“凑合”的⽅法。标准就是通过将参数类型进⾏⾃动转型之后再进⾏匹配。如果匹配到多个⾃动转型后的⽅法签名f(A)和f(B),则⽤下⾯的标准来确定合适的⽅法:传递给f(A)⽅法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。
注意:第⼆个条件表达的是⽅法参数范围⼩的最合适。
(3)如果仍然在声明类型中不到“合适”的⽅法,则编译阶段就⽆法通过。
(三)⽅法表
1、当JVM使⽤类装载器定位Class⽂件,并将其输⼊到内存中时。会提取Class⽂件的类型信息,并将这些信息存储到⽅法区中。其中,有⼀种数据结构就是⽅法表,⽅法表以数组的形式存储当前类及其所有超类的可见⽅法字节码在内存中的直接地址。
2、⽅法表存储类信息的特点:
(1)⼦类⽅法表继承了⽗类的⽅法;
(2)相同的⽅法名(即⽅法名和参数列表相同)在所有类的⽅发表中的索引相同。
3、⽰例图如下:Father f = new Son();f.f1();
注意:⽗类引⽤指向⼦类对象在调⽤⽅法时,⾸先在⽗类⽅发表中查该⽅法,如果没有到该⽅法,则编译时便⽆法通过;如果有该⽅法,保留该⽅法的索引号,在根据⼦类对象引⽤到⼦类的⽅法表,通过⽅法索引号到⼦类的同名⽅法,然后通过直接地址到该⽅法字节码所在的内存空间。即根据对象(father)的声明类型(Father)还不能够确定调⽤⽅法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1⽅法所在的位置。
三、JDK动态代理原理
1、JDK动态代理是通过JDK的Proxy类和⼀个调⽤处理器InvocationHandler接⼝来实现的,通过Proxy
来⽣成代理类实例,⽽这个代理实例通过调⽤处理器InvocationHandler接收不同的参数灵活调⽤真实对象的⽅法。
(⼀)Proxy类
1、通过Proxy类的静态⽅法wProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)⽣成代理类实例。参数说明:
(1)loader表⽰获取当前被代理类的类加载器,作⽤是⽤来⽣成代理类的。
(2)interfaces表⽰获取真实对象的所有接⼝,作⽤是代理类要实现所有接⼝。
(3)h表⽰调⽤处理器,作⽤⽣成的代理不能直接调⽤真实对象的⽅法,⽽是通过调⽤处理器来调⽤真实对象的⽅法,即重写的
invoke(Object proxy, Method method, Object[] args)⽅法。
注意:JDK动态代理只能代理接⼝,不能代理类。
2、区分开equals和==:
(1)equals⽐较的是对象的内容;
(2)==⽐较的是存储对象的地址。
3、回调更像是⼀个约定:就是如果我调⽤了a()⽅法,那么就必须要回调,⽽不需要显⽰调⽤。
四、进程和线程
(⼀)进程
1、产⽣进程概念的原因:
由于cpu与其他pc资源(RAM,显卡、光驱、键盘等)之间速度的不协调,假如有两个程序A和B,程序A在执⾏到⼀半的过程中,需要读取⼤量的数据输⼊(I/O操作),⽽此时CPU只能静静地等待任务A读取完数据才能继续执⾏,这样就⽩⽩浪费了CPU资源。为了提⾼cpu资源利⽤率,在程序A读取数据的过程中,让程序B去执⾏,当程序A读取完数据之后,让程序B暂停。为实现程序间的切换,就需要状态的保存和恢复。
由于任务执⾏需要具备执⾏环境即程序执⾏所需的资源,称之为程序上下⽂。要实现程序间切换,轮流使⽤cpu资源即“程序同时执⾏”,就需要保存程序上下⽂状态信息,因此,推出进程的概念来表述程序上下⽂的状态信息即各种资源状态信息。
2、进程和程序区分开
(1)程序是⼀个静态概念,机器上的⼀个class⽂件或者.exe⽂件就是⼀个程序。
(2)进程是程序的⼀次动态执⾏,进程是资源分配的基本单位,拥有独⽴的内存区域。
3、进程定义
进程就是⼀个程序在⼀个数据集上的⼀次动态执⾏过程。进程⼀般由程序、数据集、进程控制块三部分组成。我们编写的程序⽤来描述进程要完成哪些功能以及如何完成;数据集则是程序在执⾏过程中所需要使⽤的资源;进程控制块⽤来记录进程的外部特征,描述进程的执⾏变化过程,系统可以利⽤它来控制和管理进程,它是系统感知进程存在的唯⼀标志。
4、进程实例讲解:
有⼀⼿好厨艺的计算机科学家正在为他的⼥⼉烘制⽣⽇蛋糕。他有做⽣⽇蛋糕的⾷谱,厨房⾥有所需的原料:⾯粉、鸡蛋、糖、⾹草汁等。在这个⽐喻中,做蛋糕的⾷谱就是程序(即⽤适当形式描述的算法)计算机科学家就是处理器(cpu),⽽做蛋糕的各种原料就是输⼊数据。进程就是厨师阅读⾷谱、取来各种原料以及烘制蛋糕等⼀系列动作的总和。现在假设计算机科学家的⼉⼦哭着跑了进来,说他的头被⼀只蜜蜂蛰了。计算机科学家就记录下他照着⾷谱做到哪⼉了(保存进程的当前状态),然后拿出⼀本
急救⼿册,按照其中的指⽰处理蛰伤。这⾥,我们看到处理机从⼀个进程(做蛋糕)切换到另⼀个⾼优先级的进程(实施医疗救治),每个进程拥有各⾃的程序(⾷谱和急救⼿册)。当蜜蜂蛰伤处理完之后,这位计算机科学家⼜回来做蛋糕,从他离开时的那⼀步继续做下去。
(⼆)线程
1、产⽣线程概念的原因
当程序增多时,程序间状态保存和切换耗时就会增⼤,为进⼀步提⾼进程对资源的利⽤率,在进程中引出线程的概念,线程的出现是为了降低上下⽂切换的消耗,提⾼系统的并发性,并突破⼀个进程只能⼲⼀样事的缺陷,使到进程内并发成为可能。
假设,⼀个⽂本程序,需要接受键盘输⼊,将内容显⽰在屏幕上,还需要保存信息到硬盘中。若只有⼀个进程,势必造成同⼀时间只能⼲⼀样事的尴尬(当保存时,就不能通过键盘输⼊内容)。若有多个进程,每个进程负责⼀个任务,进程A负责接收键盘输⼊的任务,进程B负责将内容显⽰在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这⾥进程A,B,C间的协作涉及到了进程通信问题,⽽且有共同都需要拥有的东西—⽂本内容,不停的切换造成性能上的损失。若有⼀种机制,可以使任务A,B,C共享资源,这样上下⽂切换所需要保存和恢复的内容就少了,同时⼜可以减少通信所带来的性能损耗,那就好了。这种机制就是线程。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论