Java基础考点:
开放式面试题
面试题:java是如何实现平台无关性的啊?
答案如上图
面试题:为什么需要把源码编译成字节码,然后交由jvm进行解析成机器码
1.该题也可以理解为 编译的作用是什么? ,如果没有编译这个过程,那么每次执行程序前做的语法,句法,语义的检查的结果都不会被保存,也就是,每次在执行程序前都需要重新进行检查,这样做了很多重复的工作,然后影响性能
2.只要对源码进行了编译,我们多次执行程序也不需要进行多次的检查.
面试题:jvm如何加载.class文件
解答如下:
首先,ClassLoader把经过编译之后生成的.class文件依据一定的格式加载到内存中,然后Execution Engine,对内存中的字节码文件进行解析,然后解析生成的机器码会被提交给操作系统进行执行.
jvm架构图:
前提知识补充(上图中4大组件的作用如下):
1. classLoader: 它负责把经过编译生成的.class文件加载到内存.(它对所要加载的文件的格式是有要求的,只有复合一定格式的才会加载),生成相应的类的对象
2. Execution Engine:对字节码文件进行解析,解析完成之后的命令会被提交到操作系统中进行执行
3. Native Interface : 本地接口,它主要是融合不同语言开发的原生库为java所用
4. Runtime Data Area : JVM内存空间结构模型,该结构的设计堪称神作,是我们学习和性能优化的重点.
重点: 关于反射的面试题
反射(reflect) :是指在运行状态时,对于任何一个类我们都能够知道该类的所有的属性和方法,对于任意一个对象,都能够调用该对象的所有方法和属性,这种动态的获取信息以及动态调用对象方法的功能称为是 java的反射机制
面试题:列举出几个常见的反射函数,或者写几个反射的例子
定义一个实例Robot
package 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;
}
}
创建反射实例:
package reflect;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class ReflectSimple {
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException, InstantiationException,
InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
//通过类的全路径类名,获取类
Class rc = Class.forName("reflect.Robot");
//创建类的实例,然后强转为robot,默认返回值是Object类型的
Robot r = (Robot) rc.newInstance();
//打印出当前类的名字
System.out.println("Class name is "+ rc.getName());
//获取对象私有的方法
//在java世界中,万物皆是对象,我们通过Method的实例来接收通过反射获取的方法
//第一个参数是方法的名称,第二个是方法的参数的类型
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
//私有的属性,或者是方法,要进行设置,默认值是false,否则会报错
getHello.setAccessible(true);
//执行方法,需要传入对象实例和方法所需的参数,默认返回值是Object
Object str = getHello.invoke(r, "Bob");
System.out.println("getHello result is "+ str);
//获取被public修饰的方法
Method sayHi = rc.getMethod("sayHi", String.class);
sayHi.invoke(r,"welcome");
//获取私有的属性,通过Field接收
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r,"welcome ");
}
}
运行结果如下图:
小结:
1..getDeclaredMethod :,他能够获取类的public(公有的),private(私有的),protected,等所有的方法,除了,类继承的方法,还有类所实现的接口的方法
2..getMethod: 它只能获取public(公有的)方法,但是,它还可以获取所继承类的公有的方法,所实现的接口的方法.
3.获取私有的方法,或者属性,一定要执行,method.setAccessible(true);或者,field.setAccessible(true);,否则会报错.
4.反射就是把java类中的各种成分,映射成一个个的对象,
面试题
ClassLoader小结
ClassLoader在java中发挥非常重要的作用,它主要工作在Class装载的加载阶段,(Class的装载总共包括加载,连接,和初始化三个阶段),其主要的作用就是从系统外部获得Class二进制数据流,它是java的核心组件,所有的类都是ClassLoader进行加载的,ClassLoader负责将class文件中的二进制流文件加载到系统,然后交给jvm进行连接和初始化等操作.
ClassLoader的种类
- BootStrapClassLoader :它主要是用来加载Java自带的核心类,是由C++编写,它主要加载java.lang包下的内容更,加载的都是java最核心的的内容.
- ExtClassLoader : 它是用户可见的Classloader,用户加载jre外,Ext目录下的jar包,用户自定义的jar包也可以放到该目录下.
- AppClassLoader : 他主要是用来加载Classpath路径下的内容
- 自定义Classloader : 定制化加载内容和加载方式.
自定义ClassLoader
关于自定义的ClassLoader:
自定义ClassLoader有很多的好处,我们可以指定加载的路径(这个路径可能不在系统的Classpath范围内),定制化加载(要加载的甚至不是class文件或者是jar文件),自定义的ClassLoader有很多的好处,关键是重写图片上的两个方法,findClass,和 defineClass
自定义ClassLoader的实例Demo:
前提是需要预先创建目标类并且对目标类进行编译,否则会报错.
package reflect;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
//类加载器名称
private String classLoaderName;
//加载路径
private String path;
public MyClassLoader(String path, String name) {
this.classLoaderName = name;
this.path = path;
}
//用于寻找类文件,根据名称加载.class字节码文件
@Override
public Class findClass(String name){
//通过loadClassData方法,把二进制的字节码文件转换为byte数组
byte[] b = loadClassData(name);
//通过defineClass来解析定义Class字节流返回class对象
return defineClass(name,b ,0 ,b.length );
}
//用于加载类文件(通过类名称进行加载)
private byte[] loadClassData(String name) {
//对类名称进行拼接,方便加载指定路径下的指定名称的.class文件
name = path +name +".class";
//通过输入流读取类文件
InputStream in = null;
//转化为byte数组的输出流
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 ClassLoaderChecker {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassLoader m = new MyClassLoader("D:/", "MyClassLoader");
Class c = m.loadClass("Wali");
System.out.println(c.getClassLoader());
c.newInstance();
}
}
执行结果如下:
小结:
1.只要我们传入的二进制流是合法的,我们便可以通过defineClass来进行解析.
2.我们可以使用findclass访问远程的网络,去获取二进制流,并生成我们需要的类,也可以对某些敏感的类进行加密,然后在findclass 中进行解密,还可以对生成的二进制流进行修改,通过修改二进制流的代码来给类添加信息,这就是传说中的字节码增强技术.
3.AOP的实现虽然不是如此,但是也是可以借鉴该方法的.
双亲委派机制的诞生背景:
不同的ClassLoader加载类的方式和路径都是不同的,为了实现分工,各自负责各自的区块,使得逻辑更加的明确,我们才有这么多的ClassLoader并存,加载类的时候肯定会按照各自的管理的区域各司其职,我们肯定要有一个机制,让他们协同工作, 形成一个整体,这个机制就是双亲委派机制
原理图:
双亲委派机制的工作机制:
- 首先会从用户自定义的ClassLoader中查找是否加载过目标类,如果没找到就会委托给它的父类进行loadClass,以此类推,直到BootStrapClassLoader,
- 如果查找的结果依然是没有被加载过,那么,BootstrapClassLoader就会去他对应的路径或者是通过参数指定的路径下进行查找,如果没有找到就会委托给它的子类去相应的路径下进行查找.如果都没有直到就会抛出 ClassNotFoundException异常.
- 以上过程中,只要有一个步骤找到了目标类,都会直接返回,装载到JVM中,不会继续向下执行
loadClass 的源码分析:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//锁住这个class,因为有可能会有多个线程调用同一个类加载器去加载同一个类
//,锁住它可以避免冲出
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检车该类是否已经被加载过,如果加载过就直接返回,然后把该类装载到JVM中
Class<?> c = findLoadedClass(name);
//c==null,说明该类没有加载过,如果它的父类不为空,直接委托给父类进行加载
//如果父类为null,说明走到了BootStrapClassLoader.因为它是用C++写的,所以查出来是null
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//委托给父类进行加载
c = parent.loadClass(name, false);
} else {
//让BootStrap ClassLoader进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果依然没有找到该类,那么就要按照.
//ClassLoader从上到下的顺序执行loadClass方法,对该类进行查找.
//具体的查找的路径如上图所示.如果都没有查到到,就会抛出ClassNotFoundException
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//resolve默认值是false,也就是说loadClass默认是不会链接目标类的.
if (resolve) {
resolveClass(c);
}
//以上步骤如果有一个步骤找到了目标类,就会直接返回,都找不到就会抛出异常
return c;
}
}
双亲委派机制的作用
- 主要是为了防止多分同样的字节码文件被重复加载.如果没有双签委派机制,很有可能多个类加载器会把多个同样的字节码文件加载到内存.那么在内存中就会有多个相同的Class对象,这样是比较浪费内存的.内存是宝贵的,
类的加载方式:
显示加载和隐式加载的区别:
- 显示加载
loadClass和forName 在获取到Class对象之后,都需要调用Class.newInstance()方法才能获取到对象的实例,它不支持直接传入参数,需要通过反射调用构造器对象的newInstance()方法才能支持传入参数.- 隐式加载:
隐式加载无需调用对象的newInstance()方法,就能获取对象的实例,并且他支持带参数的构造器生成对象的实例.
loadClass和forName的区别和共同点 :
在了解他们之前,我们首先需要了解Java类装载的过程:
类的装载过程
装载 : 表示Class对象的生成过程,包含加载,链接和初始化*三个步骤.*
装载 : 表示的是外部的二进制字节码文件读入JVM然后是生成Class对象的过程.它包含以下三个部分.
- 加载: classloader通过loadClass把字节码文件加载到内存,生成Class对象 (并把静态数据转换成运行时的数据区中的方法区中的类型数据,在运行时,数据区堆中生成一个代表这个类的Java.lang.Class对象,作为方法区类数据的访问入口)
- 链接:它包含三个小步骤
校验: 检查加载的class的正确性和安全性.(比如检查class文件的格式是否正确)
准备 为类变量分配存储空间,并且设置类变量的初始值(类变量随类型信息存放在方法区中,生命周期很长,使用不当容易造成内存泄露)
解析 该步骤可选,JVM将常量池总的符号引用转换为直接引用.
- 初始化 : 执行类变量赋值和静态代码块
类变量: 即static变量,初始值指的是类变量类型的默认值,而不是实际要赋的值.
共同点 :
- 他们都能够在运行时对任意一个类,都能够知道该类的所有属性和方法,对于任意一个对象,都能够调用它的任意方法和属性.
区别以及产生的影响:
loadClass :
loadClass执行得到的是还没有进行链接操作的Class对象(只完成到了加载步骤),由于没有执行链接和初始化操作,它可以加快加载的速度,把初始化工作留到实际使用到该类的时候去做…
Spring的IOC容器技术,就大量使用了这种lazyloading (延时加载技术)forName:
它得到的是已经执行过初始化操作的class对象.
如果你需要在类装载完成就已经完成了初始化,或者说在装载期间执行一些操作(比如给类变量赋值),就可以是用Class.forName()来装载目标类.