Java面试第一篇 关于JVM的那些事情

Java面试点

1. JVM

1.1 谈谈java的特性

  1. 平台无关性:使用javap先将java文件编译成class文件
  2. 面向对象
  3. GC
  4. 语言特性
  5. 类库
  6. 异常处理

1.2 JVM如何加载.class文件

Java虚拟机

  1. Class Loader:依据特定格式,加载class到内存
  2. Execution Engine:对命令进行解析
  3. Native Interface:融合不同语言的原生库为java所用
  4. Runtiime Data Area:JVM内存空间结构模型(重要)

1.3 谈谈反射

// 有一个Robot类如下
public class Robot {
    private String name;
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }
    private String throwHello(String tag){
        return "Hello " + tag;
    }
    static {
        System.out.println("Hello Robot");
    }
}
// 如何使用反射调用私有方法以及属性
public class ReflectSample {
    public static void main(String[] args) throws Exception {
      // 获得当前类对象
      Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
      // 新创建一个当前类的实例
      Robot r = (Robot) rc.newInstance();
      // Class name is com.interview.javabasic.reflect.Robot
      System.out.println("Class name is " + rc.getName());
      // 定义一个方法类
      Method getHello = rc.getDeclaredMethod("throwHello", String.class);
      // 设置调用私有方法为true
      getHello.setAccessible(true);
      Object str = getHello.invoke(r, "Bob");
      // getHello result is Hello Bob
      System.out.println("getHello result is " + str);
      // 正常调用公共方法
      Method sayHi = rc.getMethod("sayHi", String.class);
      sayHi.invoke(r, "Welcome");
      Field name = rc.getDeclaredField("name");
      name.setAccessible(true);
      name.set(r, "Alice");
      sayHi.invoke(r, "Welcome");
      System.out.println(System.getProperty("java.ext.dirs"));
      System.out.println(System.getProperty("java.class.path"));
    }
}

1.4 类从编译到执行的过程

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

1. 谈谈ClassLoader

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

2. ClassLoader的种类

  1. BootStrapClassLoader:C++编写,加载核心库java.*
  2. ExtClassLoader:Java编写,加载扩展库 javax.*
  3. AppClassLoader:Java编写,加载程序所在目录

3. 自定义ClassLoader的实现

  1. 关键函数
    • findClass
    • defineClass
public class MyClassLoader extends ClassLoader {
  	// 需要加载的类的路径
    private String path;
  	// class的名字
    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();
    }
}

###1.5 双亲委派机制

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

1.6 类的加载方式

  1. 隐式加载:new
  2. 显示加载:loadClass,forName等

1. loadClass和forName的区别

  1. 类的装载过程

    • 加载
      1. 通过ClassLoader加载class文件字节码,生成class对象
    • 链接
      1. 校验:检查加载的class的正确性和安全性
      2. 准备:为类变量分配存储空间并设置类变量初始化
      3. 解析:JVM将常量池内的符号引用转换为直接引用
    • 初始化
      1. 执行类变量赋值和静态代码块
  2. 两者区别

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

1.7 Java的内存模型

1. 内存简介

2.地址空间的划分

  1. 内核空间
  2. 用户空间

3. JVM架构

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

4. JVM内存模型——JDK8

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:MetaSpace、Java堆
  • 程序计数器(Program Counter Register):
    • 当前线程所执行的字节码行号指示器(逻辑计数器)
    • 改变计数器的值来选取下一条需要执行的字节码指令
    • 和线程是一对一的关系即”线程私有“
    • 对Java方法计数,如果是Native方法则计数器值为Undefined
    • 不会发生内存泄漏
  • Java虚拟机栈(Stack)
    • Java方法执行的内存模型
    • 包含多个栈帧
  • 局部变量表和操作数栈
    • 局部变量表:包含方法执行过程中的所有变量
    • 操作数栈:入栈、出栈、复制、交换、产生消费变量

5. 线程共享

  1. 元空间(MetaSpace)和永久代(PermGen)的区别

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

      java.lang.OutOfMemoryError: PermGen Space
      
  2. MetaSpace相比PermGem的优势

    • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
    • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
    • 永久代会为GC带来不必要的复杂性
    • 方便HotSpot与其他JVMJrockit的继承
  3. Java堆(Heap)

    • 对象实例的分配区域

    • GC管理的主要区域

1.8 JVM调优参数

  • -Xms:堆的初始大小
  • -Xmx:堆能达到的最大值
  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小
// 示例代码
java -Xms128m -Xmx128m -Xss256k -jar xxxxx.jar

1.9 内存分配策略

1. 内存分配策略

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

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

  • 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
  • 管理方式:栈自动释放,堆需要GC
  • 空间大小:栈比堆小
  • 碎片相关:栈产生的碎片远小于堆
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高
  • 详解一个普通的类在加载的时候(元空间、堆、线程独占部分)之间的联系
// 有如下一个类
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();
  }
}