欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

剑指Java面试-JVM整理(不定期更新!)

程序员文章站 2022-03-10 16:51:07
...

一、谈谈你对Java的理解

  • 平台无关性
  • 面向对象
  • GC
  • 类库
  • 语言特性
  • 异常处理

二、平台无关性如何实现

Java可分为:

  • 编译时
  • 运行时

1.javap 指令

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
语法:
javap <options> <classes>
其中classes就是你要反编译的class文件。

一般常用的是-v -l -c三个选项。

  • javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
  • javap -l 会输出行号和本地变量表信息。
  • javap -c 会对当前class字节码进行反编译生成汇编代码

2. Compile Once,Run Anywhere 如何实现

剑指Java面试-JVM整理(不定期更新!)

  • Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

为什么JVM不直接将源码解析成机器码去执行

  • 准备工作:每次执行都需要各种检查
  • 兼容性:也可以将其它的语言解析成字节码

三、JVM如何加载.class文件

java虚拟机:
剑指Java面试-JVM整理(不定期更新!)

  • Class Loader:依据特定格式,加载class文件到内存
  • Execution Engine:对命令进行解析
  • Native Interface:融合不同开发语言的原生库为Java所用
  • Runtime Data Area:JVM内存空间结构模型

JVM主要由Class Loader 、Runtime Data Area、Execution Engine、Native Interface这四个部分组成,它主要通过Class Loader将符合其格式要求的class文件加载到内存里,并通过Execution Engine去解析文件里面的字节码并提交给操作系统去执行。

四、反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

1.一个反射的例子

首先定义一个类:

public class Robot {
    private String name;

    public void sayHello(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }

    private String returnHello(String tag){
        return "Hello" + tag;
    }
}

然后我们使用反射来调用它:

public class ReflectSample {
    public static void main(String[] args) throws Exception{
        Class rc = Class.forName("otherTest.date0312.Robot");
        Robot r = (Robot) rc.newInstance();
        System.out.println("Class name is" + rc.getName());
        //getDeclaredMethod 获取该类的所有方法,但是不能获取继承、实现的方法
        Method getHello = rc.getDeclaredMethod("returnHello", String.class);
        //setAccessible()方法可以取消Java的权限控制检查
        //私有属性与方法的该属性默认为false
        getHello.setAccessible(true);
        Object bob = getHello.invoke(r, "Bob");
        System.out.println("getHello result is" + bob);
        //getMethod 只能获取public方法,但是可以获取继承和实现的方法
        Method sayHello = rc.getMethod("sayHello",String.class);
        sayHello.invoke(r,"Wlecome");
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r,"Alice");
        sayHello.invoke(r, "Welcome");
    }
}

五、ClassLoader

1.类从编译到执行的过程

  1. 编译器将xxx.java源文件编译为xxx.class字节码文件
  2. ClassLoader将字节码转换为JVM中的Class<xxx>对象
  3. JVM利用Class<xxx>对象实例化xxx对象

2.谈谈ClassLoader

ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作。

ClassLoader的种类

  • BootStrapClassLoader:C++编写,加载核心库 java.*
  • ExtClassLoader:Java编写,加载扩展库javax.*
  • AppClassLoader:Java编写,加载程序所在目录
  • 自定义ClassLoader:Java编写,定制化加载

自定义ClassLoader的实现

关键函数

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

protected final Class<?> defineClass(byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(null, b, off, len, null);
}

首先编写一个Java类(其它路径下,用记事本编写),并用javac指令生成.class文件

public class TestClassLoader{
	static{
		System.out.println("This is My ClassLoader");
	}
}

然后我们在IDE中编写我们的ClassLoader

public class MyClassLoader extends ClassLoader {
    private String path;
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName) {
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        return defineClass(name,b,0,b.length);
    }

    private byte[] loadClassData(String name){
        name = path + name + ".class";
        try(InputStream in = new FileInputStream(new File(name));
             ByteArrayOutputStream out = new ByteArrayOutputStream();) {
            int i = 0;
            while ((i = in.read()) != -1){
                out.write(i);
            }
            return out.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

测试我们自己编写的ClassLoader

public class ClassLoaderChecker {
    public static void main(String[] args) throws Exception{
        MyClassLoader myClassLoader = new MyClassLoader("C:\\Users\\hp\\Desktop\\","myClassLoader");
        Class clazz = myClassLoader.loadClass("TestClassLoader");
        System.out.println(clazz.getClassLoader());
        clazz.newInstance();
    }
}

执行结果

aaa@qq.com
This is My ClassLoader

六、类加载器的双亲委派机制

剑指Java面试-JVM整理(不定期更新!)

上图种所展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。

1.双亲委派模型工作工程

剑指Java面试-JVM整理(不定期更新!)

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

2.双亲委派模型的实现

实现双亲委派的代码都集中在java.lang.ClassLoader 的loadClass()方法之中,如下所示:

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    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;
        }
    }

上述代码逻辑:

先检查是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

3.为什么要使用双亲委派机制去加载类

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

  • 避免多份同样字节码的加载

七、类的加载方式

  • 隐式加载:new
  • 显示加载:loadClass,forName等

1.类的加载过程

剑指Java面试-JVM整理(不定期更新!)

2.loadClass和forName的区别

  • Class.forName得到的class是已经初始化完成的
  • Classloader.loadClass得到的class是还没有链接的

八、Java内存模型

1.内存简介

剑指Java面试-JVM整理(不定期更新!)

  • 32位处理器:2^32的可寻址范围
  • 64位处理器:2^64的可寻址范围

2.地址空间的划分

  • 内核空间
  • 用户空间

剑指Java面试-JVM整理(不定期更新!)

3.JVM内存模型—JDK8

剑指Java面试-JVM整理(不定期更新!)

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:MetaSpce、Java堆

程序计数器(Program Counter Register)

  • 当前线程所执行的字节码行号指示器(逻辑)
  • 改变计数器的值来选取下一条需要执行的字节码指令
  • 和线程是一对一的关系即“线程私有”
  • 对Java方法技术,如果是Native方法则计数器的值为Undefined
  • 不会发生内存泄漏

Java虚拟机栈(Stack)

  • Java方法执行的内存模型
  • 包含多个栈帧

剑指Java面试-JVM整理(不定期更新!)

  • 局部变量表:包含方法执行过程中的所有变量
  • 操作数栈:入栈、出栈、赋值、交换、产生消费变量

本地方法栈

  • 与虚拟机栈相似,主要作用于标注了native的方法

元空间(MetaSpace)与永久代(PermGen)的区别

元空间和永久代都是用来存储class的相关信息,包括class对象的method、filed,元空间和永久代都是方法区的实现,只是实现有所不同,方法区只是一种JVM的规范,在JDK1.7之后原先位于方法区的字符串常量池,已经被移动到Java 堆中并且在JDK8之后使用元空间替代了永久代。

  • 元空间使用本地内存,而永久代使用的是jvm的内存

MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
  • 永久代会为GC带来不必要的复杂性
  • 方便HotSpot与其他JVM如Jrockit的集成

Java 堆

  • 对象实例分配区域
  • GC管理的主要区域

九、JVM常考题型解析

1. JVm三大性能调优参数 -Xms -Xmx -Xss的含义

java -Xms128m -Xmx128m -Xss256k -jar xxx.jar

  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小,此配置会影响并发线程数的大小
  • -Xms:堆的初始值
  • -Xmx:堆能达到的最大值

在通常情况下我们将-Xms -Xmx的值设置成一样大,因为当内存不够用而发生扩容时,容易发生内存抖动影响程序运行时的稳定性

2. Java内存模型中堆和栈的区别

内存分配策略

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
  • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
  • 堆式存储:编译时或者运行时模块入口都无法确定,动态分配

堆和栈的联系

  • 引用对象、数组时,栈里定义变量保存堆中目标的首地址

剑指Java面试-JVM整理(不定期更新!)

Java内存模型中堆和栈的区别

  • 管理方式:栈自动释放,堆需要GC
  • 空间大小:栈比堆小
  • 碎片相关:栈产生的碎片远小于堆
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高

3.元空间、堆线程独占部分间的联系—内存角度

通过HelloWorld这段代码来看一下:

public class HelloWorld {
    private String name;

    public void sayHello() {
        System.out.println("Hello" + " " + name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        int a = 1;
        HelloWorld hw = new HelloWorld();
        hw.setName("test");
        hw.sayHello();
    }
}

剑指Java面试-JVM整理(不定期更新!)

4. 不同版本之间的intern()方法的区别----JDK6 VS JDK6+

String s = new String("a");
s.intern();
  • JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用
  • JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。

下面我们用代码测试一下:

public class InternDifference {
    public static void main(String[] args) {
        String s = new String("a");
        s.intern();
        String s2 = "a";
        System.out.println(s == s2);

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 ==s4);
    }
}

JDK1.6输出

false
false

剑指Java面试-JVM整理(不定期更新!)

**JDK1.6+**输出
JDK1.6+是可以将字符串的引用传入到常量池里面去的,所以第二个为true;

false
true

剑指Java面试-JVM整理(不定期更新!)