这个考察的很宽泛,答案不是唯一的,往往考察多个方面,其实这个问题考察的是你是否真的掌握了Java,对基础知识点的理解是否真的很清楚。是否掌握主要的模块和运行 原理等,同时也会对你接下来的答案进行深究,要回答这个问题我们需要对java语言的特点做个汇总,主要分为以下几点:
complie Once,Run Anywhere 如何实现,java是怎么做到很平台无关的呢?我们Java分为
javap:是jdk自带的反汇编译器,可以查看java编译器为我们生成的字节码。javap -c
.java ———javac编译,生成字节码———>.class文件————————>jvm解析转换成特定平台的执行指令:Jvm for Linux ,Jvm for Win,Jvm for IOS
总结:Java源码首先被编译成字节码,再由不同的平台Jvm进行解析,Java语言在不同平台上运行不需要重新编译,Java虚拟机在执行字节码的时候,把字节码转换成平台上的机器指令。
为什么jvm不直接将我们的java源码解析成机器码去执行?
准备工作:每次执行都需要各种检查。(语法,句法,语义的检查,即每次执行的时候这些语义分析的结果都不会保存下来,都要重新编译,重新地去分析,所以整体的性能都会受到影响,因为做了很多重复的事情,所以这里我们引入了中间字节码,保证在编译成字节码之后,多次执行程序是不需要各种校验 和补全)**
jvm如何加载.class文件?
想要更深刻的回答这个问题那我们先了解一下java虚拟机。
java虚拟机
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。jvm有自己完善的硬件架构如 处理器,堆栈,寄存器等。还具有相应的指令系统jvm屏蔽了与具体平台系统相关的信息,使得java程序只需在java虚拟机运行的目标代码即字节码,就可以在多种平台上不加修改的运行。前面我们也验证了这个问题,一般情况下我们不需要知道虚拟机的运行原理,只要专注写java代码就可以了,这也就是虚拟机之所以存在的原因,就是屏蔽底层操作系统平台的不同,并且减少基于原生语言开发的复杂性。只要虚拟机厂商在特定系统上实现虚拟机定义如何将字节码解析成本操作系统可执行的二进制码,java这门语言呢就可以实现跨越各种的平台。
对于jvm来讲他最值得我们学习的两点分别是
这两个知识点对于我们来说,既是面试的重点更是程序调优的关键。
这里大家需要抓住一个重点那就是 jvm是一个内存中的虚拟机,已就意味着jvm的存储就是内存,我们写的所有类,变量,常量,方法都在内存中这决定着我们程序运行的是否健壮,是否高效。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6F5viTmK-1632112600114)(C:\Users\黄福荣\Desktop\md\ms图库\jvm架构.png)]
我们先来大致看看jvm的架构,大家看看这个图。整个Jvm大致分为4个部分
Class Loader (类加载器)
Runtime Date Area (运行时数据区域)
Execution Engine (执行引擎)
Native Interface (本机接口)
class Loder:依据特定格式,加载class文件到内存,不是自己乱写一个.class文件就能加载的,是有格式的要求的,具体的格式要求在这里我们不去深究
Execution Engine:Class Loader 字段加载只要符合格式要求就能加载,至于能不能运行就看Execution Engine了,它负责对我们的命令进行解析,解析完之后就直接到我们真正的操作系统里面进行执行了
因此可以简要回答 jvm如何加载.class文件 这个问题了:
jvm主要由 Class Loader,Runtime Data Area,Execution Engine,Native Interface这四个部分组成。它主要通过Class Loder将符合其格式要求的Class文件加载到内存中并且通过 Execution Engine去解析Class文件里的字节码,并提交给操作系统去执行。
我们再来看看Runtime Data Area和Native Interface的作用分别是什么呢。
Native Interface:即本地接口,融合不同开发语言的原生库为java所用(不管我们如何吹嘘java它的执行效率都没有C或者C++的执行效率高,主流的Jvm也是由C++去实现的,因此在需要涉及需要较高性能的运算操作的时候是需要Java里直接调用它们的,此外呢本着不重复造的原则在实际生产中某个库如果 已经用别的语言来开发了,我们就不需要再开发一套,而是希望Java对这些库进行调用。因此为了满足上述需求Jvm在内存中专门开辟了一块区域处理标记为native的代码,它的做法就是native Matter set中等级Native方法,在Execution执行时加载Native Libraies)
大家可以参考我们常用的:Class.forName();方法它就调用了本地库函数。
Runtime Data Area:Jvm内存结构模型 (我们所写的程序都会被加载到这里之后才开始被执行,该结构模型的设计堪称神作,是我么要重点学习的)
先上答案:
JAVA反射机制是在运行状态中,
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个属性和方法;
这种动态获取信息以及动态调用对象方法的功能成为语言的反射机制。
不直接这样回答可能理论性太强了,要会用才叫真的了解:
写一个反射的例子
先创建一个Robot对象
package com.interview.javabasic.reflect;
public class Robot {
private String name;
public void sayHi(String helloSentence){
System.out.println(helloSentence + " " + name);
}
private String throwHello(String tag){
return "Hello " + tag;
}
static {
System.out.println("Hello Robot");
}
}
现在我们就开始用反射获取这个类里面的方法和属性
package com.interview.javabasic.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
// 1.先获取这个类,参数是目标类的全路径
Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
// 2.创建rc的实例
Robot r = (Robot) rc.newInstance();
// 打印类的名字
System.out.println("Class name is " + rc.getName());
// 3.获取对象中的throwHello方法
// getDeclaredMethod 可以获取目标类的所有方法,但是不能获取继承或者实现接口的方法
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
// 因为throwHello是私有的,所以要设置Accessible为true不然会报错
getHello.setAccessible(true);
// 利用方法名.invoke()调用这个方法
// 第一个参数是对象实例,第二个参数是该方法所需的参数
Object str = getHello.invoke(r, "Bob");
// 打印调用结果
System.out.println("getHello result is " + str);
// 5.利用getMethod获取pulic方法
// getMethod 可以获取目标类的public方法,也能获取继承或者实现接口的public方法
Method sayHi = rc.getMethod("sayHi", String.class);
// 利用方法名.invoke()调用这个方法
sayHi.invoke(r, "Welcome");
// 6.调用属性
Field name = rc.getDeclaredField("name");
// 因为属性是privte的所有要设置Accessible
name.setAccessible(true);
// 给属性赋值
name.set(r, "Alice");
// 调用welcome方法打印,赋值结果
sayHi.invoke(r, "Welcome");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
通过上面的例子可以知道反射就是把java类中的各种成分映射成一个个的java对象。
大家有没有注意到我们上面讲解的例子,我们之所以能够获取类的属性还有方法,并对其进行调用,必须要获取它的对象,而要获取该类的Class对象,必须要先获取该类的字节码.clss文件对象,这就跟我们之前所学的知识连接起来了。
就是一个类从写好代码到一个类被使用的过程,我们可以做个总结,我们拿Robot例子去举例:
编译器将Robot.java源文件编译成Robot.class字节码文件
ClassLoader将字节码转换为JVM中的Class对象
JVM利用Class对象实例化为Robot对象
什么是ClassLoader?
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据字节流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作。
ClassLoader的种类
自定义ClassLoader
在写之前我们先来了解几个关键的函数,通过覆盖这两个关键的函数我们就能够去自定义我们ClassLoader的行为
函数一:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这个函数是用来寻找我们的Class文件的。就是包括怎么去读进我们的二进制流,并且去对它进行处理,进而给它返回一个Class对象;
函数二:
protected final Class<?> defineClass(byte[] b,int off,int len) throws ClassFormatError {
return defineClass(null,b,off,len,null);
}
顾名思义这个函数的意思就是去定义一个类,通过传进来的byte[] b,返回一个Class
总结一下:findClass就是根据名称或者位置加载.class字节码,然后他在调用defineClass解析定义.clss字节流返回它对象。
别光说不练,我们来自定义一个ClassLoader
package com.interview.javabasic.reflect;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
// 1.继承ClassLoader父类
public class MyClassLoader extends ClassLoader {
// 路径,我们要到哪个路径去加载
private String path;
// 自定义ClassLoader的名称
private String classLoaderName;
// 通过构造函数给成员变量赋值
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件, 这个方法在寻找到我们的类文件之后,就会将它以二进制Byte数组的形式将它加载进来,然后传递给我们的defineClass方法
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
// defineClass这里已经默认为我们创建好了,这里不需要自己去实现
return defineClass(name, b, 0, b.length);
}
//用于加载类文件,读取我们自定义的Class
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
验证一下我们自定义的ClassLoader
public class Wangli{
static {
System.out.println("Wangli");
}
}
package com.interview.javabasic.reflect;
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 参数一.class文件的位置,参数二ClassLoader的名字 随便定义
MyClassLoader m = new MyClassLoader("/Users/baidu/Desktop/", "myClassLoader");
// 获取它的类。loadClass是父类ClassLoader的方法 参数包.类名
Class c = m.loadClass("Wali");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
System.out.println(c.getClassLoader().getParent().getParent().getParent());
c.newInstance();
}
}
通过上面的例子我们大致已经明白了如何创建自定义ClassLoader来灵活的加载类,在实现findClass方法是我们不仅可以在自定义的目录下.clss文件,通过dfineClass方法我们可以知道,只要传入的是二进制的流,它是合法的。那我们便能通过不同的形式去加载,比如说在findClass去访问某个远程的网络,去获取二进制流并生成我们需要的类,又可以说对某些敏感的Class文件进行加密在findClass里面进行解密,此外我们还可以对生成的二进制流做手脚修改二进制流的代码给添加些信息如AFM。
}
通过上面的例子我们大致已经明白了如何创建自定义ClassLoader来灵活的加载类,在实现findClass方法是我们不仅可以在自定义的目录下.clss文件,通过dfineClass方法我们可以知道,只要传入的是二进制的流,它是合法的。那我们便能通过不同的形式去加载,比如说在findClass去访问某个远程的网络,去获取二进制流并生成我们需要的类,又可以说对某些敏感的Class文件进行加密在findClass里面进行解密,此外我们还可以对生成的二进制流做手脚修改二进制流的代码给添加些信息如AFM。
### **谈谈类加载器(ClassLoader)的双亲委派机制。**
从前面我们可以知道,不同ClassLoader加载类的方式和路径有所不同。