Java面试第一篇 关于JVM的那些事情
Java面试点
1. JVM
1.1 谈谈java的特性
- 平台无关性:使用
javap
先将java
文件编译成class
文件 - 面向对象
- GC
- 语言特性
- 类库
- 异常处理
1.2 JVM如何加载.class文件
Java虚拟机
- Class Loader:依据特定格式,加载class到内存
- Execution Engine:对命令进行解析
- Native Interface:融合不同语言的原生库为java所用
- 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 类从编译到执行的过程
- 编译器将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转换为JVM中的
Class<Robot>
对象 - JVM利用
Class<Robot>
对象实例化为Robot对象
1. 谈谈ClassLoader
- ClassLoader主要工作在Class装载的加载阶段
- 其主要作用是从系统外部获得Class二进制数据流。
- 他是Java的核心组件,所有的Class都是由ClassLoader进行加载的
- ClassLoader负责通过将Class文件里的二进制数据流装载进系统
- 然后交给Java虚拟机进行连接、初始化等操作
2. ClassLoader的种类
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库 javax.*
- AppClassLoader:Java编写,加载程序所在目录
3. 自定义ClassLoader的实现
- 关键函数
- 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 类的加载方式
- 隐式加载:new
- 显示加载:loadClass,forName等
1. loadClass和forName的区别
-
类的装载过程
- 加载
- 通过ClassLoader加载class文件字节码,生成class对象
- 链接
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配存储空间并设置类变量初始化
- 解析:JVM将常量池内的符号引用转换为直接引用
- 初始化
- 执行类变量赋值和静态代码块
- 加载
-
两者区别
- Class.forName得到的class是已经初始化完成的
- ClassLoader.loadClass得到的class是还没有完成链接的
1.7 Java的内存模型
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. 线程共享
-
元空间(MetaSpace)和永久代(PermGen)的区别
-
元空间使用本地内存,而永久代使用的是jvm的内存
java.lang.OutOfMemoryError: PermGen Space
-
-
MetaSpace
相比PermGem
的优势- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便
HotSpot
与其他JVM
如Jrockit
的继承
-
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();
}
}