Java学习之从“底”学起

临近秋招,许多小伙伴也开始狂刷面试题,总结面试考点,希望可以在今年这个不太一般的秋天去到心仪的公司。在这里,我来总结一下最近刷Java底层试题以及看面试视频的题型。
我的理解是,我们在回答Java方面的知识时,不仅仅是能达到“会用”,我们更要理解其原理是怎么实现的,理解了底层实现,才能决定以后在编程路上走到什么地步,所有语言大同小异,理解了设计思想,什么语言都是一样的。

文章目录

  • 谈谈你对Java的理解
    • 平台无关性
      • 基本指令
      • 为什么我们不直接将Java源码转化成该平台的机器码
      • Java反射
      • 类从编译到执行的过程
      • ClassLoader是什么
      • ClassLoader的双亲委派机制
      • 类的加载方式之——new、forName与loadClass
      • JVM的构造
        • 元空间与永久代的区别
        • JVM内存模型面试必知
    • GC垃圾回收机制
    • 语言特性
    • 面向对象
    • Java类库
    • 异常处理

谈谈你对Java的理解

Java是什么?如果面试官问了你这个话题,该怎么作答呢?Java可谓是包罗万象,从最初的Sysytem.out.println("hello world"),到类与对象,再到文件、线程、异常处理,乃至Spring、SpringBoot、SpringCloud。这些都是Java,我们似乎被这个话题迷昏了眼睛,一时说不上来,该从那儿开始,这里,我们要做一个Java的框架,将所学过的知识串起来,这样即时面对范围这么庞大的问题,我们也可以知道该怎么回答了。

平台无关性

Java为什么流行,很大一部分原因就是因为其平台无关性,优越的跨平台性能使我们无需关注如何去调用系统指令为我们服务,不管是Linux、MAC OS、WINDOWS乃至其他,只要有了JVM,一切都无需我们去想,只需要一份class文件,JVM就会给我们转化为各个平台上的机器码然后运行。

基本指令

  1. 编译:在有Java环境的机器上,我们可以使用javac将Java源码编译成.class文件(这里需要注意,我们的源码如果语法语义错误,是不会编译成功的),之后供Java虚拟机去运行。
  2. 运行:JVM将我们的.class文件通过java命令加载到JVM中运行。
  3. 反编译:在我们拥有一份.class文件后,我们可以通过反编译,执行javap -c的指令反编译成我们认识的语言。

为什么我们不直接将Java源码转化成该平台的机器码

为什么要转化一个中间文件.class文件?通过JVM直接转化成该平台机器码不是更省事儿吗?原因如下,上文提到,我们在进行.class文件编译时,需要对Java源码进行检查,通过后才可以转化,转化成功后就表明语法没问题,这个.class文件就可以拿到任意一个平台去执行,但是如果没有这一步,我们刚在Linux运行了Java文件,转头相同的文件在Win上执行,还得再检查一遍,更加浪费了时间以及JVM的性能。
平台复用性:既然Java虚拟机有这么好的转化机制,我们也可以将其他语言转化为字节码文件,交给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中执行,在这期间,会经过以下几个部分,我们先来看一下。
Java学习之从“底”学起_第1张图片
.class文件会经过一个ClassLoader(类加载器)将.class文件转化成Class 字节码文件对象,再通过JVM内存结构(Runtime Data Area)生成对象实例,然后通过Execution Engine将其解释称机器能识别的命令,期间可能会调用Native InterFace调用本地接口来调用本地方法(融合不同语言的原生方法为Java所用,这里的方法会在Native Method Stack中注册)之后提交命令到OS,让其执行。

ClassLoader是什么

ClassLoader(类加载器)负责将.class文件加载到JVM内存中,ClassLoader会寻找.class文件并将其转化成Class字节码对象文件。ClassLoader共有四种,分别为

  • BootStrapClassLoader:C++编写,在JVM启动时启动,负责加载Java核心库
  • ExtClassLoader:负责加载Java扩展库,会加载如ext/lib/jre文件夹下的扩展库
  • AppClassLoader:负责加载本程序的.class文件,加载classpath下的文件。
  • 自定义ClassLoader:可以加载自定义的.class文件,这里的class文件可以是本机的,也可以是网络上的,我们可以通过继承ClassLoader类,并重写其findCass(String name)方法并在其中执行defineClass(byte[],off,len),该方法会返回Class字节码对象。
    举个栗子(自定义ClassLoader)
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();//调用该方法才可以执行静态代码块
    }
}

Java学习之从“底”学起_第2张图片

以上通过重写findClass()方法,先获取到文件,之后使用字节流将其传入defineClass()中构建字节码对象,之后返回字节码对象。

ClassLoader的双亲委派机制

我们在查看ClassLoader的源码时,发现其中有一个parent参数。
Java学习之从“底”学起_第3张图片
说明ClassLoader也是有父类的,结合我们上面提到的四种ClassLoader,不难想出这四种有着某种关系,这里可以告诉大家是一种包含关系。我们可以借助上面的例子,来看一下具体是什么关系。
Java学习之从“底”学起_第4张图片
这里很清楚的表明了关系,从上而下依次是
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、forName与loadClass

说到类的加载,我们首先会想到new,这是一种隐式加载法,它隐式的调用了ClassLoader并且会返回一个对象实例。
其次,我们在上边已经实现了forName和loadClass的方法,关于前一种和后一种的区别,这里就要说一下类装载的的步骤了。
一、加载:通过调用loadClass()方法通过class名来找到.class文件,将二进制字节流转化为字节码对象Class<'T>
二、链接
1、检查:检查加载的class文件对象的正确性和安全性。
2、准备:为该对象分配存储空间以及设置该类的初始变量(ps:这里是设置该变量类型初始的值,比如int初始值是0,long是-1L,这里是这种初始值)
3、解析:(可选)JVM将常量池中的符号引用转化为直接引用。
三、初始化:执行类变量赋值,执行静态代码块
区别:forName执行时会直接进行一二三步,到初始化完成,这里我们用常见的加载JDBC驱动来说,查JDBC驱动的源码可知
Java学习之从“底”学起_第5张图片
加载驱动是要执行静态代码块的,所以forName是执行完第三步。
由上面我们自定义ClassLoader可以知道,如果我们没有使用newInstence方法来实例化这个对象的话,静态块是不会执行的,并且从该类传入的参数我们也可以知道loadClass(String name, boolean resolve),第二个参数是决定要不要执行链接操作,而在loadClass的源码中我们可以看到该值默认传入false。
在这里插入图片描述
问题:为什么要分loadClass和forName
俗话说的好,存在即合理,像加载mysql驱动时,我们要执行到静态块,所以选用forName。而在Spring中,Spring为我们管理着许多的bean,Spring为了快速,将许多的类只加载到了第一步,省去了初始化的时间,这样在我们真正调用某个类的时候,Spring就会为我们初始化。

JVM的构造

说到JVM,这个模型堪称神作,有了它,我们可以不必像C一样每次运行完手动垃圾回收,我们也规避了指针带来的风险,同时还可以进行多线程的实现。
Java学习之从“底”学起_第6张图片
JVM内存模型共分五个区域,其中线程独享的有 程序计数器、虚拟机栈、本地方法栈线程共享的有方法区、堆

  • 程序计数器
    1、程序计数器是用来记录当前执行字节码的行号的(行号是一个逻辑地址,不是真实地址)
    2、字节码解释器通过改变程序计数器的行号来执行下一条字节码指令。
    3、各个线程通过程序计数器来记住当前执行的进度,各个线程在切换之后还能记住自己当前执行到哪儿就是这个的功劳。
    4、程序计数器对Java方法进行计数的时候会显示行号,而对本地方法进行计数的时候会显示undefined
    5、程序计数器不会发生内存泄露
  • 虚拟机栈
    1、虚拟机栈是每个线程独有的,它通过将每个方法生成一个个栈帧,通过压栈弹栈的方式来执行各个方法。
    2、虚拟机栈由局部变量表、操作数栈、动态链接、方法出口等组成
    3、局部变量表中存放着方法中定义的参数,该空间大小在程序运行开始的时候已经固定。
    4、操作数栈中存放着方法执行过程中产生的消费变量,通过压栈弹栈的方式进行运算。
    5、栈不需要垃圾回收,因为使用过的方法(栈帧)都会被弹出。
    ps:为什么过多的嵌套会报错,因为会生成太多的栈帧,使得有固定长度的栈超出容量,使动态增长的栈申请不到那么多的空间。
    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
    如果虚拟机栈可以动态扩展,扩展时无法申请做够的内存,将会爬出OutOfMemorryError
  • 本地方法栈
    与虚拟机栈发挥的作用非常类似,他们之间的区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。与虚拟机栈一样,也会抛出StackOverflowErrorOutOfMemorryError异常。
  • 方法区
    具体来说,方法区其实是JVM规定的一种规范,是各个线程所共享的,它用来存储已被虚拟机加载的类信息、运行时常量池和静态常量池(字符串常量池已经被移动到堆中)、静态变量、即时编译后的代码等数据。具体的实现则在JDK1.8之前称为永久代,1.8之后被元空间替代。

  • 堆作为JVM中占地面积最大的一块区域,其中存放着所有实例化过后的对象。(这里等之后更新GC时候再增添老年代和新生代的分别

元空间与永久代的区别

元空间作为永久代的代替者,自然是有它的优势,首先,永久代是存储在JVM内存中的,而元空间是存储在系统内存中的,这就使得元空间的大小不是永久代可以比拟的。

  • 字符串常量池在永久代中,易出现性能问题和内存溢出。
  • 类和方法的大小难以确定,给永久代大小指定带来困难。
  • 永久代会给GC带来不必要的复杂性
  • 有的JVM虚拟机根本没有永久代这个区域,去除掉之后方便不同的虚拟机合并,比如hotspot和其他。

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对象运行行号的程序计数器。
Java学习之从“底”学起_第7张图片

GC垃圾回收机制

语言特性

面向对象

Java类库

异常处理

你可能感兴趣的:(面试,底层,jvm,java)