Java平台无关性如何实现
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台 上运行的时候不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
为什么JVM不直接将源码解析成机器码去执行
- 准备工作:每次执行都需要各种检查;
- 兼容性:可以将别的语言解析成字节码;
JVM如何加载class文件
- ClassLoader:依照特定格式,加载class文件到内存;
- Execution Engine:对命令进行解析;
- Native Interface:融合不同开发语言的原生库为Java所用;
什么是反射?
Java的反射机制就是在运行状态中,对于任意一个类,都能够知道这个的所有属性和方法;对于任意一个对象都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能成为Java 的反射机制
;
写一个反射的例子
//对象类
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");
}
}
mport 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, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
//获取类对象
Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
//
Robot r = (Robot) rc.newInstance();
System.out.println("Class name is " + rc.getName());
Method getHello = rc.getDeclaredMethod("throwHello", String.class);
getHello.setAccessible(true);
Object str = getHello.invoke(r, "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"));
}
}
谈谈ClassLoader
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class的二进制工作流,它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流加载进系统,然后交给Java虚拟机进行连接、初始化等操作。
- BootStrapClassLoader: C++编写,加载核心库java.*;
- ExtClassLoader: Java编写,加载扩展库 javax.*;
- AppClassLoader: Java编写,加载程序所在目录;
- 自定义ClassLoader: Java编写,定制化加载;
类加载器的双亲委派机制
- 避免多份同样的字节码的加载;
类的加载方式
- **隐式加载: ** new
- 显式加载: laadClass、forName;
类加载过程:
- 加载: 通过ClassLoader加载class文件字节码,生成Class对象;
- 链接 :
- 校验:检查加载的class的正确性和安全性;
- 准备:为
类变量
分配存储空间并设置类变量的初始值(static修饰的变量); - 解析:JVM将常量池内的符号引用转化为直接引用;
- 初始化: 执行类变量赋值和静态代码块;
loadClass和forName的区别:
Class.forName()
得到的class是已经得到初始化完成的,Classloader.loadClass
得到的class 是还没有链接的;Classloader.loadClass
在实现Spring的延迟加载时作用很大;
谈谈Java的内存模型
内存简介
地址空间的划分
- 内核空间
- 用户空间
JVM架构
- ClassLoader:依据特定格式,加载class文件到内存;
- ExecutionEngine:对命令进行解析;
- NativeInterface:融合不同开发语言的原生库为Java所用;
- Runtime Data Area: JVM内存空间结构模型;
JVM内存模型–JDK8
- 线程私有的:程序计数器、虚拟机栈、本地方法栈;
- 线程共享:MetaSpace、Java堆;
线程私有–程序计数器
- 当前线程所执行的字节码行号指示器(逻辑),
逻辑计数器
; - 改变计数器的值来选取下一条需要执行的字节码指令;
- 和线程是一对一的关系,即"线程私有";
- 对Java方法计数,如果是Native方法计数器值为Undefined;
- 不会发生内存泄漏;
线程私有–Java虚拟机栈(Stack)
- Java方法执行的内存模型,每次方法的调用都是通过虚拟机栈传递的;
- 包括多个栈帧(局部变量表、操作栈、动态链接、返回地址);
- 局部变量表: 包含方法执行过程中的所有变量;
- 操作数栈: 入栈、出栈、复制、交换、产生消费变量;
线程私有–Java本地方法栈
- 与虚拟机栈相似,主要作用于native方法栈;
线程共享–MetaSpace(元空间)
- 元空间使用本地内存,而永久代使用的是jvm内存;
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出;
- 类和方法的信息大小难易确定,给永久代的大小指定带来困难;
- 永久代会给GC带来不必要的复杂性;
线程共享–堆(Heap)
- 堆区域的唯一目的就是存放对象实例的区域,是Java虚拟机管理内存中最大的一块区域;
- GC管理的主要区域;
说一下JVM三大性能调优参数 -Xms、-Xmx、-Xss的含义
- -Xss: 规定了每个线程虚拟机栈(堆栈)的大小;
- -Xms: 堆的初始值;
- -Xmx: 堆能达到的最大值,一般Xms和Xmx的值一样;
Java内存模型堆和栈的区别–内存分配策略;
1、存储区别
- 静态存储: 编译时就能确定每个数据目标在运行时的存储空间需求;
- 栈式存储: 数据区需求在编译时未知,运行时模块入口确定;
- 堆式存储: 编译时或运行时模块入口都无法确定,动态分配;
2、联系
- 引用对象、数组时候,栈里会定义变量保存堆中目标的首地址;
- 管理方式:栈自动释放,堆需要GC;
- 空间大小:栈比堆小;
- 碎片相关:栈产生的碎片远小于堆;
- 分配方式:栈支持静态和动态分配,堆仅支持动态分配
- 效率:栈的效率比堆高;
垃圾回收
对象被判定为垃圾的标准
- 没有被对象引用
- 引用计数法:通过判断的引用数量来决定镀锡是否可以被回收,每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1,任何引用计数为0的对象实例可以被当做垃圾收集;
- 可达性分析算法:通过判断对象的引用链是否可达来决定对象是否可以被回收;
- 虚拟机栈中引用的对象(栈帧中的本地变量表);
- 方法区中常量引用的对象;
- 方法区中类静态属性引用的对象;
- 本地方法栈中引用对象;
- 活跃线程的引用对象;
垃圾回收算法
标记-清除算法
- 标记:从 根集合进行扫描,对存活的对象进行标记;
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存;
复制算法(适合年轻代中的对象)
- 可用内存分为对象面和空闲面;
- 对象在对象面上创建,存活的对象被从对象面复制到空闲面;
- 将对象面所有对象面内存清除;
标记-清除算法(适合老年代中的对象)
- 标记:从根集合进行扫描,对存货的对象进行标记;
- 清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收;
- 优点:
1、避免内存的不连续;
2、不用设置两块内存互换;
3、适用于存活率高的场景;
分代收集算法
- 垃圾回收的集大成者,按照对象生命周期的不同划分区域以采用不同的垃圾回收算法,提高垃圾回收效率;
- 年轻代对象存活率低,使用复制算法;老年代对象存活率高,使用标记-整理或者标记-清除算法;
年轻代:尽可能快速收集掉那些生命周期短的对象
- Eden区(8):新创建的对象在Eden去内存足够的情况下存在;
- 两个Survivor区(1:1):
对象如何晋升到老年代 - 经历一定Minor次数依然存活的对象;
- Survivor区中存放不下的对象;
- 新生成的大对象(-XX:+PretenuerSizeThreshold)
常用的调优参数 - -XX:SurvivorRatio: Eden区和Survivor区的比值,默认8:1;
- -XX:NewRatio: 老年代和你年轻代内存大小的比例;
- -XX:MaxTenuringThreshold: 对象从年轻代晋升到老年代经过GC次数的阈值;
老年代:存放生命周期较长的对象
- 使用标记清理或标记整理算法进行垃圾回收;
- 触发老年代的垃圾回收指的是Full GC或Major GC;
- Full GC比Minor GC慢;
触发Full GC的条件: - 老年代空间不足;
- CMS GC时出现promotion failed和concurrent mode failure;
- MinorGC晋升到老年代的平均大小大于老年代的剩余空间;
- 调用System.gc()可能会触发Full GC;
Stop-the-World
- JVM要执行垃圾回收而停止了应用程序的执行;
- 任何一种GC算法中都会发生;
- 多数GC优化通过减少Stop-the-world发生的时间来提高性能;
GC的分类
- Minor GC:发生在年轻代中;
- Full GC:发生在老年代中;
常见的垃圾收集器
JVM的运行模式
- Server
- Client
垃圾收集器之间的联系
年轻代收集器
Serial收集器(-XX:UserSerial,复制算法)
- 单线程收集:进行垃圾收集时候,必须暂停所有工作线程;
- 简单高效:Client默认下默认的年轻代收集器;
ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样;
- 单核执行效率不如Serial,在多核下执行才有优势;
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 更关注系统的吞吐量;
- 在多核下执行才有优势,Server模式下默认的年轻代收集器;
老年代收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程;
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
- 单线程。吞吐量优先;
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿;
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象;
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余 对象;
- 并发清理:清理垃圾对象,程序不会停顿;
- 并发重置:重置CMS收集器的数据结构;
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
- 并行和并发;
- 分代收集;
- 空间整合;
- 可预测的停顿;
GC相关面试题
Java中的强引用、软引用、弱引用、虚引用
- 强引用:
- 最普遍的引用:Object object = new Object();
- 抛出OOM异常终止程序也不会回收具有强引用的对象;
- 通过将对象设置为null来弱化引用,使其被回收;
- 软引用
- 对象处在有用但非必须的状态;
- 只有当内存空间不足时,GC会回收该引用的对象的内存;
- 可以用来实现高速缓存;
- 弱引用
- 非必须的对象,比软引用更弱一些;
- 虚引用
- 不会决定对象的生命周期;
- 不会决定对象的生命周期;