面试问道:谈谈你对java的理解
一般我们要简单的从 平台无关性、GC、语言特性、面向对象、类库、异常处理等来简要回答。
平台无关性如何实现
Compile Once,Run Anywhere如何实现
提供了不同平台的虚拟机,所以可以通过下图可以实现
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台上运行时是不需要进行重新编译的,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
public class ByteCodeSample {
public static void main(String[] args) {
int i=1,j=5;
i++;
++j;
System.out.println(i);
System.out.println(j);
}
}
编译时
利用命令 javac test/java/com/kun/ByteCodeSample.java 进行编译生成字节码.class文件
运行时
利用命令 java com/kun/ByteCodeSample 进行运行字节码文件
反汇编
利用命令 javap -c com/kun/ByteCodeSample 进行反汇编;这里反汇编编译的时.class字节码文件
反汇编查看字节码内容
Compiled from "ByteCodeSample.java" //从ByteCodeSample.java编译而来
public class com.kun.ByteCodeSample {
public com.kun.ByteCodeSample(); //无参构造函数
Code: //无参构造函数执行内容
0: aload_0
1: invokespecial #1 //调用父类构造方法 super Method java/lang/Object."<init>":()V
4: return //退出方法
public static void main(java.lang.String[]);
Code: //main函数执行内容 这里只有局部变量 涉及到栈操作
0: iconst_1 //常量1放入栈顶
1: istore_1 //取出栈顶的值 放入局部变量1中
2: iconst_5 //常量5放入栈顶
3: istore_2 //取出栈顶的值 放入局部变量2中
4: iinc 1, 1 //调用函数 将变量1加上1
7: iinc 2, 1 //调用函数 将变量2加上1
10: getstatic #2 // 获取PrintStream静态域 Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_1 //将变量1计算得到的结果值压入栈顶
14: invokevirtual #3 //打印出栈顶的值 Method java/io/PrintStream.println:(I)V
17: getstatic #2 // 获取PrintStream静态域 Field java/lang/System.out:Ljava/io/PrintStream;
20: iload_2 //将变量2计算得到的结果值压入栈顶
21: invokevirtual #3 //打印出栈顶的值 Method java/io/PrintStream.println:(I)V
24: return //退出方法
}
为什么JVM不直接将源码解析成机器码去执行
准备工作:每次执行都需要各种检查(浪费时间,降低效率)
兼容性:也可以将别的语言解析成字节码(符合软件的中庸之道,如scala也是字节码转机器码)
JVM如何加载.class文件
主要是通过 Class Loader依据特定格式,加载class文件到内存,之后通过Execution Engine对命令进行解析,最后提交给操作系统去执行。
Java虚拟机
上图主要分为四部分:Class Loader、Runtime Data Area、Execution Engine、Native Interface
- Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析
- Native Interface:融合不同开发语言的原生库为Java所用 (比如class.forName的实现就接用了native方法)
- Runtime Data Area:后续有详细介绍
如果要看native类,请参考openjdk 中的jdk8里的share里的java.lang
谈谈反射
JAVA反射机制是运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
列举或写一个反射的例子
实体类
package com.kun;
public class People {
private String name;
public void say(String sentence){
System.out.println(sentence + " " + name);
}
private String throwHello(String tag){
return "Hello "+ tag;
}
}
反射拿到调用方法返回结果
package com.kun;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class aClass = Class.forName("com.kun.People");
People people = (People) aClass.newInstance();
System.out.println("类名: "+ aClass.getName());
//获取私有方法(可以获取除了继承的所有方法)
Method throwHello = aClass.getDeclaredMethod("throwHello", String.class);
throwHello.setAccessible(true);
Object jek = throwHello.invoke(people, "jek");
System.out.println("throwHello返回值 "+ jek);
//获取公共方法(可以获取除了私有的所有方法包括继承抽象)
Method say = aClass.getMethod("say", String.class);
say.invoke(people, "welcome");
//获取私有变量
Field name = aClass.getDeclaredField("name");
name.setAccessible(true);
name.set(people,"Alice");
say.invoke(people, "welcome");
}
}
//运行结果
类名: com.kun.People
throwHello返回值 Hello jek
welcome null
welcome Alice
类从编译到执行的过程
- 编译器将People.java源文件编译为People.class字节码文件
- ClassLoader将字节码转换为JVM中的Class<People>对象
- JVM利用Class<People>对象实例化为People对象
谈谈ClassLoader
ClassLoader 在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
它是一个抽象类
public abstract class ClassLoader {
其中最重要的方法:加载类
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
ClassLoader的种类
- BootStrapClassLoader:C++编写,加载核心类java.*。(parent是null)
- ExtClassLoader:Java编写,加载扩展库javax.*
当用到相关类库的时候会自动到指定路径加载class文件
String var0 = System.getProperty(“java.ext.dirs”)。(parent是BootStrapClassLoader) - AppClassLoader:Java编写,加载程序所在目录
final String var1 = System.getProperty(“java.class.path”); 我们程序所在路径。(parent是ExtClassLoader) - 自定义ClassLoader :Java编写,定制化加载 。 (parent是AppClassLoader)
自定义ClassLoader的实现
关键函数
findClass 寻找Class文件
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
先在任意地方创建一个类,运行生成class字节码文件
这个java文件路径为\Users\K\Desktop\
public class Kun{
static{
System.out.println("Hello Kun");
}
}
编写自定义ClassLoader类
package com.kun;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String path;
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();
}
}
package com.kun;
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader("\\Users\\K\\Desktop\\","test");
Class kun = myClassLoader.loadClass("Kun");//寻找Kun类
System.out.println(kun.getClassLoader());
kun.newInstance();
}
}
//运行结果
com.kun.MyClassLoader@14ae5a5
Hello Kun
谈谈类加载器(ClassLoader)的双亲委派机制
核心源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//自定义找类 If still not found, then invoke findClass in order to find the class.
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;
}
}
为什么使用双亲委派机制去加载类
- 避免多份同样字节码的加载
类的加载方式
- 隐式加载:new
- 显示加载:loadClass、forName等
loadClass和forName的区别
类的装载过程
- 加载
- 通过ClassLoader加载class文件字节码,生成class对象
- 链接
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配存储空间并设置类变量初始值
- 解析:JVM将常量池内的符号引用转换为直接引用
- 初始化
- 执行类变量赋值和静态代码块
区别
- Class.forName得到的class是已经初始化完成的(初始化MySQL数据库连接)
- Classloder.loadClass得到的class是还没有链接的(spring ioc运用到延迟加载)
你了解Java的内存模型吗
内存简介
- 32位处理器:2^32的可寻址范围
- 64位处理器:2^64的可寻址范围
地址空间的划分
- 内核空间
- 用户空间(java内存)
JVM内存模型 - JDK8
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:MetaSpace、Java堆
程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即线程私有
- 对Java方法计数,如果是Native方法则计数器值位Undefined
- 不会发生内存泄漏
Java虚拟机栈(Stack)
- Java方法执行的内存模型
- 包含多个栈帧
局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、生产消费变量
源代码
package com.kun;
public class ByteCodeSample {
public static int add(int a,int b){
int c=0;
c=a+b;
return c;
}
}
javac反编译后
C:\Users\K\Documents\IDEA\java_train\src\test\java>javap -verbose com\kun\ByteCodeSample.class
//文件信息
Classfile /C:/Users/K/Documents/IDEA/java_train/src/test/java/com/kun/ByteCodeSample.class
Last modified 2019-11-5; size 278 bytes
MD5 checksum 27fa20ed9289f599a73ba2cdf75b8036
Compiled from "ByteCodeSample.java"
//描述类信息
public class com.kun.ByteCodeSample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池信息
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/kun/ByteCodeSample
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 add
#9 = Utf8 (II)I
#10 = Utf8 SourceFile
#11 = Utf8 ByteCodeSample.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/kun/ByteCodeSample
#14 = Utf8 java/lang/Object
{
//初始化过程
public com.kun.ByteCodeSample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
//自定义方法
public static int add(int, int);
descriptor: (II)I //接收了两个int的变量(II);返回值也是int(I)
flags: ACC_PUBLIC, ACC_STATIC //方法信息-public和static
Code:
stack=2, locals=3, args_size=2 //操作数栈2个,本地变量3个,方法参数2个
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn
LineNumberTable:
line 5: 0 //代码的第五行对于字节码的第0行
line 6: 2
line 7: 6
}
SourceFile: "ByteCodeSample.java"
执行add(1,2)
局部变量表为操作数栈提供一定的数据支撑
递归为什么会引发java.lang.StackOverflowError异常
public class Fibonacci {
//F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
//F(2)=F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1+1 = 2
//F(0)-F(N) 依次为 0,1,1,2,3,5,8,13,21,34...
public static int fibonacci(int n){
if(n == 0) {return 0;}
if(n == 1) {return 1;}
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
System.out.println(fibonacci(1000000));
}
}
上诉代码就会报错;不停的递归调用一个方法,会使单个线程的栈帧急速增多,操作数栈会不断压栈进入数据,导致StackOverflow。
虚拟机栈过多会引发java.lang.OutOfMenoryError异常
由于windwos平台虚拟机java平台映射到操作系统的内核线程上;上诉代码会有极大可能导致系统假死,在windows上运行会死机。
本地方法栈
- 与虚拟机栈相似,主要作用于标注了native的方法
元空间(MetaSpace)与永久代(PermGen)的区别
MetaSpace在1.8及之前是属于永久代的,都是用来存储class的相关信息,包括class的相关对象的method、 field 等;元空间和永久代都是方法区的实现,只不过实现有所不同,方法区只是jvm的一种规范;在1.7及之后方法区的字符串常量池已经被移动到java堆中,并且在1.8及之后使用元空间替代了永久代。
- 元空间使用本地内存,而永久代使用的是jvm的内存
1.8之后java.lang.OutOfMemoryError:PermGen space 错误将不复存在 - MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot与其他JVM如Jrockit的集成
Java堆
- 对象实例的分配区域
- GC管理的主要区域
常考面试题
JVM三大性能调优参数-Xms -Xmx -Xss的含义
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
通常情况下-Xms -Xmx会设置成一样的,因为当k不够用自动扩容时会发生内存抖动,影响程序的运行。
Java内存模型中堆和栈的区别
先回答 内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
不允许有可变数据结构的存在,也不允许有嵌套递归出现;因为这些会导致编译程序无法计算准确的存储空间 - 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
Java内存模型中堆和栈的区别
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
- 区别
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态分配和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
元空间、堆、线程独占部分间的联系-内存角度
- 元空间
- Class:HelloWorld - Method:sayHello\setName\main - Field :name
- Class:System
- Java堆
- Object:String(“test”)
- Object:HelloWorld
- 线程独占
- Parameter reference:“test” to String object
- Variable reference:“hw” to HelloWorld object
- Local Variables:a with 1,lineNo
不同JDK版本之间的intern()方法的区别-JDK6 VS JDK+
String s = new String("a")
s.intern();
JDK6 :当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
引爆jdk6的常量池的代码 java.lang.OutOfMemoryError:PermGen space
jdk6测试报错java.lang.OutOfMemoryError:PermGen space
jdk7测试不会报错
jdk8测试会报错永久代参数无效 -XX:MaxPermSize=6M -XX:PermSize=6M参数被舍弃
import java.util.Random;
public class PermGenErrTest {
public static void main(String[] args) {
for(int i=0; i <= 1000; i++){
//将返回的随机字符串添加到字符串常量池中
getRandomString(1000000).intern();
}
System.out.println("Mission Complete!");
}
//返回指定长度的随机字符串
private static String getRandomString(int length) {
//字符串源
String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for ( int i = 0; i < length; i++){
int number = random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
}
在VM options运行参数里填写永久代大小 -XX:MaxPermSize=6M -XX:PermSize=6M
以下代码1.6的执行结果为false false; 1.6+为false true
package com.interview.javabasic.jvm.model;
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);
}
}
JDK6解释图
JDK6+解释图