临近秋招,许多小伙伴也开始狂刷面试题,总结面试考点,希望可以在今年这个不太一般的秋天去到心仪的公司。在这里,我来总结一下最近刷Java底层试题以及看面试视频的题型。
我的理解是,我们在回答Java方面的知识时,不仅仅是能达到“会用”,我们更要理解其原理是怎么实现的,理解了底层实现,才能决定以后在编程路上走到什么地步,所有语言大同小异,理解了设计思想,什么语言都是一样的。
Java是什么?如果面试官问了你这个话题,该怎么作答呢?Java可谓是包罗万象,从最初的Sysytem.out.println("hello world")
,到类与对象,再到文件、线程、异常处理,乃至Spring、SpringBoot、SpringCloud。这些都是Java,我们似乎被这个话题迷昏了眼睛,一时说不上来,该从那儿开始,这里,我们要做一个Java的框架,将所学过的知识串起来,这样即时面对范围这么庞大的问题,我们也可以知道该怎么回答了。
Java为什么流行,很大一部分原因就是因为其平台无关性,优越的跨平台性能使我们无需关注如何去调用系统指令为我们服务,不管是Linux、MAC OS、WINDOWS乃至其他,只要有了JVM,一切都无需我们去想,只需要一份class文件,JVM就会给我们转化为各个平台上的机器码然后运行。
javac
将Java源码编译成.class文件(这里需要注意,我们的源码如果语法语义错误,是不会编译成功的),之后供Java虚拟机去运行。java
命令加载到JVM中运行。javap -c
的指令反编译成我们认识的语言。为什么要转化一个中间文件.class文件?通过JVM直接转化成该平台机器码不是更省事儿吗?原因如下,上文提到,我们在进行.class文件编译时,需要对Java源码进行检查,通过后才可以转化,转化成功后就表明语法没问题,这个.class文件就可以拿到任意一个平台去执行,但是如果没有这一步,我们刚在Linux运行了Java文件,转头相同的文件在Win上执行,还得再检查一遍,更加浪费了时间以及JVM的性能。
平台复用性:既然Java虚拟机有这么好的转化机制,我们也可以将其他语言转化为字节码文件,交给Java虚拟机去执行,这样,就符合了计算机系统中复用的理念。
反射:在运行状态下,我们可以获取到任意一个类的属性和方法,也可以获取到任意一个对象的方法和属性,这种动态获取类信息以及动态调用对象的机制称为反射。
举个栗子
package com.app.demo
public class People{
private String name;
public String sayHello(String name){
return name;
}
private String sayHi(String name){
return name;
}
}
package com.app.test
public class toTest{
public static void main(String[] args){
Object x = Class.forName("com.app.demo.People");//获取字节码文件对象
People people = (PeoPle)x.newInstence();//获取实例对象
//获取私有方法
Method sayHi = x.getDeclaredMethod("sayHi",String.class);//传入要获取的方法名以及其参数列表的参数类型.
sayHi.setAccssible(true);//执行了这个方法才可以使用私有方法
Object str = sayHi.invoke(people,"Tom");//传入对象实例以及参数
//获取公有方法
Method sayHello = x.getMethod("sayHello",String.class);
Object str1 = sayHello.invoke(people,"Tom");
//获取私有属性
Filed name = x.getDeclaredFlied("name");
name.setAccessible(true);
name.set(people,"Tom");
}
}
注意事项⚠️:
我们在获取私有属性/方法时,必须要setAccessible(true)
才可以获取到方法。
getDeclaredFlied([MethodName],[参数类型])
可以获取全部方法,但是无法获取通过继承得到的方法。
getMethod([MethodName],[参数类型])
无法获取私有方法,但是可以获取继承的到的方法。
知道了Java反射的含义以及用法之后,我们再来看看类是如何从编译到运行的。
我们的类通过javac编译成.class文件之后是,还需要加载到JVM中执行,在这期间,会经过以下几个部分,我们先来看一下。
.class文件会经过一个ClassLoader(类加载器)将.class文件转化成Class 字节码文件对象,再通过JVM内存结构(Runtime Data Area)生成对象实例,然后通过Execution Engine将其解释称机器能识别的命令,期间可能会调用Native InterFace调用本地接口来调用本地方法(融合不同语言的原生方法为Java所用,这里的方法会在Native Method Stack中注册)之后提交命令到OS,让其执行。
ClassLoader(类加载器)负责将.class文件加载到JVM内存中,ClassLoader会寻找.class文件并将其转化成Class字节码对象文件。ClassLoader共有四种,分别为
findCass(String name)
方法并在其中执行defineClass(byte[],off,len)
,该方法会返回Class字节码对象。class wail{
static{
System.out.println("hello wail");
}
}
package com.interview.javabasic.reflect;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
//用于加载类文件
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();
}
}
package com.interview.javabasic.reflect;
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("/Users/baidu/Desktop/", "myClassLoader");
Class c = m.loadClass("Wali");
Object w = c.newInstence();//调用该方法才可以执行静态代码块
}
}
以上通过重写findClass()方法,先获取到文件,之后使用字节流将其传入defineClass()中构建字节码对象,之后返回字节码对象。
我们在查看ClassLoader的源码时,发现其中有一个parent
参数。
说明ClassLoader也是有父类的,结合我们上面提到的四种ClassLoader,不难想出这四种有着某种关系,这里可以告诉大家是一种包含关系。我们可以借助上面的例子,来看一下具体是什么关系。
这里很清楚的表明了关系,从上而下依次是
BootStrapClassLoader、ExtClassLoader、AppClassLoader、自定义ClassLoader。(这里的null是BootStrapClassLoader,因为其是C++编写,已经嵌入到了JVM的核心代码中,所以这里显示null。)
我们再来看ClassLoader源码中最重要的方法loadClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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();
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
从这里可以看出,当我们在加载一个class文件时,ClassLoader会先看自己有没有加载该文件,如果没有,则向上一级去询问,一层一层,直到BootStrapClassLoader,如果都没有,则由顶级ClassLoader尝试加载class文件(在自己管辖的文件范围内),此时还是加载不到的话,就返回null,由下一级尝试加载,直到加载到或者到了自定义ClassLoader还没加载到就报ClassNotFoundException。
总结:自下而上查看是否加载,自上而下尝试加载
问题:为什么要实现该机制? 因为在整个JVM中,我们只需要一份class字节码对象,好比我们加载System这个系统静态类,这个类已经在BootStrapClassLoader中被加载了,如果我们写了一个同名的class文件,就会先去加载这个,顶替了系统设定的这个,就会出现不安全的隐患。
说到类的加载,我们首先会想到new
,这是一种隐式加载法,它隐式的调用了ClassLoader并且会返回一个对象实例。
其次,我们在上边已经实现了forName和loadClass的方法,关于前一种和后一种的区别,这里就要说一下类装载的的步骤了。
一、加载:通过调用loadClass()
方法通过class名来找到.class文件,将二进制字节流转化为字节码对象Class<'T>
二、链接
1、检查:检查加载的class文件对象的正确性和安全性。
2、准备:为该对象分配存储空间以及设置该类的初始变量(ps:这里是设置该变量类型初始的值,比如int初始值是0,long是-1L,这里是这种初始值)
3、解析:(可选)JVM将常量池中的符号引用转化为直接引用。
三、初始化:执行类变量赋值,执行静态代码块
区别:forName执行时会直接进行一二三步,到初始化完成,这里我们用常见的加载JDBC驱动来说,查JDBC驱动的源码可知
加载驱动是要执行静态代码块的,所以forName
是执行完第三步。
由上面我们自定义ClassLoader可以知道,如果我们没有使用newInstence
方法来实例化这个对象的话,静态块是不会执行的,并且从该类传入的参数我们也可以知道loadClass(String name, boolean resolve)
,第二个参数是决定要不要执行链接操作,而在loadClass
的源码中我们可以看到该值默认传入false。
问题:为什么要分loadClass和forName
俗话说的好,存在即合理,像加载mysql驱动时,我们要执行到静态块,所以选用forName。而在Spring中,Spring为我们管理着许多的bean,Spring为了快速,将许多的类只加载到了第一步,省去了初始化的时间,这样在我们真正调用某个类的时候,Spring就会为我们初始化。
说到JVM,这个模型堪称神作,有了它,我们可以不必像C一样每次运行完手动垃圾回收,我们也规避了指针带来的风险,同时还可以进行多线程的实现。
JVM内存模型共分五个区域,其中线程独享的有 程序计数器、虚拟机栈、本地方法栈 而 线程共享的有方法区、堆。
undefined
StackOverflowError
OutOfMemorryError
StackOverflowError
,OutOfMemorryError
异常。元空间作为永久代的代替者,自然是有它的优势,首先,永久代是存储在JVM内存中的,而元空间是存储在系统内存中的,这就使得元空间的大小不是永久代可以比拟的。
一、JVM如何调优
Java分为编译和执行两个过程,在我们执行.class文件的时候,会通过java xxxx.class
或者java -jar xxx.jar
来执行对应文件,此时,有三个参数需要我们记住,它们是-Xms -Xmx -Xss
.
-Xms :设置JVM中栈的大小
-Xmx:设置JVM中堆的初始空间大小
-Xss:设置JVM中堆的最大空间
这里可以使用 java -Xms 128M -Xmx 128M -Xss 128M
来设置对应空间的大小,其中堆的初始值以及最大值最好设置称相同,否则在进行堆扩容的时候会发生内存抖动现象。
二、JVM中堆和栈的区别
1、内存分配策略
1.1、静态储存:静态储存要求在代码编译期就确定其大小,要求代码中不能包含可变长字符串以及实例对象等代码块,不允许有嵌套以及递归等。
1.2、栈式存储:栈式存储在编译器不要求确定其大小,而是要求在代码执行时(模块入口)确定其大小,结合我们之前说的栈的存储结构,每个栈帧都会为其分配固定的空间。
1.3、堆式存储:编译时以及运行时都无法确定其大小,全部进行动态分配,这里存放着可变长字符串对象以及对象。
2、堆和栈之间的联系
我们都知道,每个线程在执行某个代码块时,其中可能会有多个方法以及对象的引用,每执行一个方法都需要创建一个栈帧,此时运行过程中会用到对象的引用,为此,栈特意创建了一个引用变量,该变量指向引用对象在堆内的首地址,该变量在初次引用时创建,在运行到该引用的作用域之外时被释放。堆中的对象在没有引用指向时,不定期的被回收。
3、堆栈之间的区别
管理方式:栈自动释放空间,栈帧的弹栈以及引用变量的释放就是自动释放,堆进行垃圾回收。
空间大小:堆的空间远大于栈的空间。
碎片相关:栈产生的碎片远远小于堆,一方面是因为栈规定了具体的大小,二是因为栈的操作简单,只有弹栈和压栈。
分配方式:栈有静态分配以及动态分配,而堆只有动态分配。
效率:栈的执行效率较高。
三、栈、堆、元空间之间的联系
元空间存放着一段程序执行所用到的全部的类信息,即所有方法及属性。
堆中存放着一段程序执行所用到的全部对象实例,其中包括方法及属性。
栈中存放着一段程序执行所用到的全部堆中对象实例的引用变量,存放着指向当前Class对象运行行号的程序计数器。