剑指Java面试-JVM整理(不定期更新!)
一、谈谈你对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.类从编译到执行的过程
- 编译器将xxx.java源文件编译为xxx.class字节码文件
- ClassLoader将字节码转换为JVM中的
Class<xxx>
对象 - 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
六、类加载器的双亲委派机制
上图种所展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(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