JAVA底层知识:JVM
一 谈谈你对Java的理解
- 平台无关性,即一次编译,到处运行
- GC,垃圾回收机制,不必像C++那样手动释放内存了
- 语言特性:泛型、反射、Lambda表达式
- 面向对象:封装、继承、多态
- 类库、Java本身自带的一些集合和一些并发库,网络库、IO/NIO
- 异常处理
二 Compile Once,Run Anywhere如何实现
javac编译,生成字节码文件,JVM解析,转换成特定平台的执行指令
Java源码首先被变异成字节码文件,再有不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
如何查看java字节码: javap
2.1 为什么JVM不直接将源码解析成机器码去执行
- 准备工作: 每次执行都需要各种检查
- 兼容性:也可以将别的语言解析成字节码
- 首先这样做有两点好处:可以节省大量的准备工作,提升效率,其次可以提升兼容性;
三 JVM如何加载.class文件
首先这个问题我们需要弄明白Java虚拟机,虚拟机是跑在内存中的,虚拟机核心的有两点:
①内存模型,②GC;
先来看一下JVM的架构图
- Class Loader : 负责加载符合格式的.class文件;
- Execution Engine : 对命令进行解析;
- Native Interface : 融合不同开发语言的原生库为Java所用;
- Runtime Data Area : Java内存空间模型;
四 谈谈反射
4.1 反射的概念
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象的功能称为Java语言的反射机制;
4.2 写一个反射的例子
public class Robot {
//私有name getDeclaredField()
private String name;
//公用方法 getMethod()
public void sayHi(String helloSentence){
System.out.println(helloSentence+" "+name);
}
//私有方法 getDeclaredMethod()
private String throwHello(String tag){
return "hello " + tag;
}
}
public class ReflectSample {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class<?> rc = Class.forName("com.interview.javabasic.reflect.Robot");
Robot r =(Robot) rc.newInstance();
System.out.println("Class name is: " + rc.getName());
//private方法
//getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。
//需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
getHello.setAccessible(true);//setAccessible(true)取消了Java的权限控制检查
Object str = getHello.invoke(r,"zpf");
System.out.println("getHello result is: " + str);
//public方法
//getMethod:获取当前类和父类的所有public的方法。
// 这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。
Method sayHi = rc.getMethod("sayHi", String.class);
sayHi.invoke(r,"Welcome");
//getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
//getField 仅能获取类(及其父类可以自己测试) public属性成员
Field name = rc.getDeclaredField("name");
name.setAccessible(true);
name.set(r,"Alice");
sayHi.invoke(r,"Welcome");
}
}
4.3 类从编译到执行的工程
- 编译器将.ava源文件编译成Class文件;
- ClassLoader将字节码转换成JVM中的Class< T >对象;
- JVM利用Class< T >对象实例化为T对象;
五 谈谈ClassLoader类加载器
5.1 ClassLoader类加载器
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流.它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接/初始化等操作.
5.2 ClassLoader的种类
BootStrapClassLoader : C++编写, 加载核心库Java.*;
ExtClassLoader : Java编写,加载扩展库javax.*;
AppClassLoader : Java编写,加载程序所在目录;
自定义ClassLoader : Java编写,定制化加载;
5.3 自定义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); }
-
自定义ClassLoader
public class MyClassLoader extends ClassLoader{ private String path; private String classLoadName; public MyClassLoader(String path, String classLoadName){ this.path = path; this.classLoadName = classLoadName; } //用于寻找类文件 @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 (IOException e) { e.printStackTrace(); } } return out.toByteArray(); } }
-
测试自定义MyClassLoader
public class ClassLoaderChecker { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { MyClassLoader m = new MyClassLoader("F:/","myClassLoader"); Class<?> c = m.loadClass("Zpf"); System.out.println(c.getClassLoader()); c.newInstance(); } }
六 为什么使用双亲委派机制去加载类
6.1 什么是双亲委派机制?
当某个类加载器需要加载某个
.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。6.2 为什么使用双亲委派机制去加载类?
- 避免重复字节码的加载;
- 提供了JDK核心类加载的沙箱环境;
6.3 双亲委派机制的作用
1、防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
七 类的加载方式
7.1 类的加载方式
- 隐式加载 : new;
- 显式加载 : LoadClass —> forName进行加载;
7.2 loadClass和forName的区别
LoadClass源码
// loadClass
// 第一步
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
-------------------------------------------------------------------------------
// 第二步
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;
}
}
---------------------------------------------------------------------------------
// 第三步
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
由于resolve传的值默认为false,所以只会进行加载不会执行后续两步的装载过程;接下来看一下forName();
// forName
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
从上边的源码可以看出默认传入的 initialize 为 true.说明使用 Class.forName() 方法获得 Class 对象是已经执行完初始化的了(注意这里指的是类加载过程中的最后一步:初始化,而非是实例化对象操作的初始化),所以他们的区别是
- 使用 loadClass() 方法获得的 Class 对象只完成了类加载过程中的第一步:加载,后续的操作均未进行。
- 使用 Class.forName() 方法获得 Class 对象是已经执行完初始化的了
7.3 两者的使用场景
-
对于Class.forName
加载 MySQL 的驱动:
Class.forName("com.mysql.jdbc.Driver");
在驱动的源码会发现在类 Driver 中有一个静态代码块,静态代码块会在类加载过程中的初始化阶段执行。
static { try { //往DriverManager中注册自身驱动 DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }
-
对于loadClass
举个小例子,在 Spring IOC 中,在资源加载器获取要读入的字节的时候,即读取一些 Bean 的配置的时候,如果是以 classpath 的方式来加载,就需要使用 ClassLoader 的 loadClass() 方法来加载。之所以这样做,是和 Spring IOC 的 Lazy Loading 有关,即延迟加载。Spring IOC 为了加快初始化的速度,大量的使用了延迟加载技术,而使用 ClassLoader 的 loadClass() 方法不需要执行类加载过程中的链接和初始化的步骤,这样做能有效的加快加载速度,把类的初始化工作留到实际使用到这个类的时候才去执行
八 Java内存模型😛
8.1 你了解Java的内存模型吗
-
内存简介
32位 : 2^32 的可寻址范围;
64位 : 2^64 的可寻址范围 -
地址空间的划分
内核空间 : 指操作系统程序和C运行时的空间,包括调度程序等;
用户空间 : Java程序运行时实际使用的空间;
8.2 Java内存模型JDK1.8
8.3 程序计数器(Program Counter Register)
- 当前线程所执行的字节码的行号指示器;
- 改变计数器的值来选取下一条需要执行的字节码指令;
- 和线程是一对一的关系也就是线程私有的;
- 对Java方法技术,如果使用的Native方法则计数器值为Undefined;
- 没有内存泄露的问题;
8.4 Java虚拟机栈(Stack)
- Java方法执行的内存模型;
- 包含多个栈帧;
8.5 局部变量表和操作数栈
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、赋值、交换、产生消费变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOnGn3oQ-1582124488351)(https://i.loli.net/2019/01/27/5c4da5087e34c.png)]
8.6 递归为什么会引发java.lang.StackOverflowError异常
名词解释:StackOverflowError:栈溢出错误
-
栈溢出原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况
如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出StackOverflowError
-
递归过深,栈帧数超过虚拟栈深度
-
解决办法 : 限制递归深度或者使用循环;
-
**原理:**StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
8.7 虚拟机栈过多会引发java.lang.OutOfMemoryError异常
- 概念:OutOfMemoryError:内存不足错误
- 内存溢出原因:
如果一个线程可以动态地扩展本机方法栈,并且尝试本地方法栈扩展(没有大于配置允许最大的栈大小),但是内存不足可以提供, 或者如果不能提供足够的内存来为新线程创建初始的堆(如new Object),那么Java虚拟机将抛出OutOfMemoryError。
8.8 本地方法栈(线程共享😛)
- 与虚拟机栈相似,主要作用于标注了Native的方法;
8.9 元空间与永久代
-
两者区别
元空间使用的是本地内存,永久代使用的是JVM的内存;
-
使用元空间的好处
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出;
- 类和方法的信息大小难以确认,给永久代的大小指定带来困难;
- 永久代会为GC带来复杂度;
- 方便与其他JVM继承;
8.10 堆(heap)
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。
- 对象实例的分配区域
- GC管理的主要区域
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
九 JVM内存模型—常考题解析
9.1 JVM三大性能调优参数 -Xms -Xmx -Xss
- -Xss:规定了每个线程虚拟机栈(堆栈)的大小,默认JDK1.4中是256K,JDK1.5+中是1M
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值(一般设置和-Xms相同大小的值,防止heap不够用发生内存抖动)
9.2 Java内存模型中堆和栈的区别—内存分配策略
-
内存分配策略
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.
- 静态存储:编译时驱动每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
-
Java内存模型中堆和栈的区别
- 堆内存用来存放由new创建的对象实例和数组。(重点)
- 在栈内存中保存的是堆内存空间的访问地址,或者说栈中的变量指向堆内存中的变量(Java中的指针)
**堆和栈的联系:**引用对象,数组时,栈里定义变量保存堆中目标的首地址
堆与栈区别:
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小**(堆存的是对象数据,栈存的是指针)**
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高(栈的灵活度较低,堆的灵活度较高,堆操作的复杂度要高)
9.3 元空间、堆、线程独占部分的联系—内存角度
9.4 不同JDK版本之间的intern()方法的区别——JDK6 VS JDK6+
String s = new String("a");
s.intern();
JDK6:当调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。
本质上就是后来改善版的 intern 方法把字符串存到了堆中,寻找的时候也会到堆中去寻找,原因是为了避免由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen;(元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。)
-
JDK6的例子
解析:首先我们用引号声明的常量(“a”)都会在字符串常量池中创建出来,而new出来的string对象(new String(“a”))都会在Java heap中创建出来,intern的时候会试图把"a"字符串的副本放进常量池,但是由于常量池已有"a",所以放不进去,所有s1和s2引用不同的地址;"aa"则是首先在Java heap中生成,然后 intern 把"aa"的副本放进常量池,成功放进去;所以s4和s3不同。
注:此处虽然都是false,但是常量池和Java heap中字符串的生成顺序不一样 -
JDK6+的例子
解析:解析:首先我们用引号声明的常量(“a”)都会在字符串常量池中创建出来,而new出来的string对象(new String(“a”))都会在Java heap中创建出来,intern的时候会试图把"a"字符串引用放进常量池,但是由于常量池已有"a",所以放不进去,所以s1和s2引用不同的地址;"aa"则是首先在Java heap中生成,然后 intern 把"aa"的引用放进常量池,成功放进去;所以s4和s3相同。
注:新版本的intern与旧版本不同的是:旧版本放进常量池的是字符串的副本,也就是字符串的内容;而新版本则是把字符串的引用放进常量池;所以当常量池不存在字符串的时候,新版本的 intern 之后的引用就和之前的一样