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

10 篇文章 0 订阅
6 篇文章 0 订阅

一、谈谈你对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语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

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

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

三、JVM如何加载.class文件

java虚拟机:
在这里插入图片描述

  • 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();
    }
}

执行结果

otherTest.date0312.MyClassLoader@4554617c
This is My ClassLoader

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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.类的加载过程

在这里插入图片描述

2.loadClass和forName的区别

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

八、Java内存模型

1.内存简介

在这里插入图片描述

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

2.地址空间的划分

  • 内核空间
  • 用户空间

在这里插入图片描述

3.JVM内存模型—JDK8

在这里插入图片描述

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

程序计数器(Program Counter Register)

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

Java虚拟机栈(Stack)

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

在这里插入图片描述

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

本地方法栈

  • 与虚拟机栈相似,主要作用于标注了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内存模型中堆和栈的区别

  • 管理方式:栈自动释放,堆需要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();
    }
}

在这里插入图片描述

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

在这里插入图片描述

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

false
true

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值