安卓面试必备的JVM虚拟机制详解,看完之后简历上多一个技能( 四 )


类加载流程分为五个阶段 , 分别是加载、验证、准备、解析和初始化 。
加载阶段 , 就是通过一个类的全限定名来获取定义此类的二进制字节流 , 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 。 加载阶段是开发人员可控性最强的阶段 , 因为开发人员可以自定义类加载器 。 对于数组而言 , 情况有所不同 , 数组类本身不通过类加载器创建 , 它是由 Java 虚拟机直接创建 。
验证是链接阶段的第一步 , 这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求 , 并且不会危害虚拟机自身的安全 。 它包括文件格式校验、元数据校验、字节码校验等 。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段 , 这些变量所使用的内存都将在方法区中进行分配 。 需要注意的是 , 这时候进行内存分配的仅仅包含类变量 , 不包括实例变量 , 实例变量将会在对象实例化时随着对象一起分配在 Java 堆上 。 其次 , 这里所说的变量初始值是该数据类型的零值 。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 。 符号引用以一组符号来描述所引用的目标 , 直接引用可以是直接指向目标的指针 。
初始化阶段是执行类构造器 () 方法的过程 。 () 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的 , 编译器收集的顺序是由语句在源文件中出现的顺序所决定的 。 虚拟机会保证一个类的 () 方法在多线程环境中被正确的加锁同步 , 如果多个线程同时去初始化一个类 , 那么只会有一个线程去执行这个类的 () 方法 , 其他线程都需要阻塞等待 , 这也是静态内部类能实现单例的主要原因之一 。
安卓面试必备的JVM虚拟机制详解,看完之后简历上多一个技能文章插图
双亲委派模型双亲委派模型的工作过程是:如果一个类加载器收到类加载的请求 , 它首先不会自己去尝试加载这个类 , 而是把这个请求委派给父类加载器去完成 , 每一层次的类加载器都是如此 , 因此所有的类加载请求最终都应该传送给顶层的启动类加载器中 , 只有当父类加载器反馈自己无法完成这个加载请求时 , 子加载器才会尝试自己去加载 。
使用双亲委派模型来组织类加载器之间的关系 , 有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系 。 比如 Object 类 , 无论哪个类加载器去加载 , 应用程序各种加载器环境中都是同一个类 , 同时也避免了重复加载 。 而且 , 双亲委派模型也保证了 Java 程序的稳定运作 。 比如在应用程序中你是不能直接使用 UnSafe 这一不安全操作的类的 。
双亲委派模型的实现相对简单 , 代码都集中在 ClassLoader 的 loadClass 方法中先检查是否已经被加载过了 , 如果没加载则先调用父加载器的 loadClass 方法 , 若父加载器为空则使用默认的启动类加载器作为父加载器 。 如果父加载器加载失败 , 抛出 ClassNotFoundException 异常 , 然后调用自己的 findClass 方法进行加载 。
编译器优化在公司内部 , 我是分享过一次关于编译优化的相关知识 。 课题是 “从 final '能够' 提升性能 , 谈编译优化” 。
对于 Java 代码的编译 , 分为前端编译和后端编译 。 前端编译是指通过 javac 工具 , 将 Java 代码转化为字节码的过程 。 既然 javac 负责字节码的生成 , 那肯定就会有一些通用的优化手段 。 比如常量折叠、自动装拆箱、条件编译等 , 其次还有 JDK9 使用 StringContactFactory 对 "+" 的重载提供的统一入口等 。 后端编译则指 JVM 内置的解释器和即时编译器(C1、C2) 。 JVM 在对代码执行的优化可以分为运行时优化和即时编译器(JIT)优化 。 运行时优化主要是解释执行和动态编译通用的一些机制 , 比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等 。 除此之外 , 还有一些专门用于优化解释执行效率的 , 比如说模版解释器、内联缓存(inline cache , 用于优化虚方法调用的动态绑定) 。 JVM 的即时编译器优化是指将热点代码以方法为单位转化成机器码 , 直接运行在底层硬件之上 。 它采用了多种优化方式 , 包括静态编译器可以使用的如方法内联、逃逸分析 , 也包括基于程序的运行 profile 的投机性优化 。
下面我就主要讲一下方法内联和逃逸分析 。
方法内联 , 它指的是在编译的过程中遇到方法调用时 , 将目标方法的方法体纳入编译范围之中 , 并取代原方法调用的优化手段 。 方法内联不仅可以消除调用本身带来的性能开销 , 还可以进一步触发更多的优化 。 因此 , 它可以算是编译优化里最为重要的一环 。 以 getter/setter 为例 , 如果没有方法内联 , 在调用 getter/setter 时 , 程序需要保存当前方法的执行位置 , 创建并压入用于 getter/setter 的栈桢、访问字段、弹出栈桢 , 最后再恢复当前方法的执行 。 而当内联了对 getter/setter 的方法调用后 , 上述操作就只剩下字段访问了 。 但是即时编译器不会无限制的进行方法内联 , 它会根据方法的调用次数、方法体大小、Code cache 的空间等去决定是否要进行内联 。 比如即使是热点代码 , 如果方法体太大 , 也不会进行内联 , 因为会占用更多内存空间 。 所以平时编码中 , 尽可能使用小方法体 。 对于需要动态绑定的虚方法调用来说 , 即时编译器则需要先对虚方法调用进行去虚化 , 即转化为一个或多个直接调用 , 然后才能进行方法内联 。 说到这 , 你应该就明白 final/static 的好处了 。 所以尽量使用 final、private、static 关键字修饰方法 , 虚方法因为继承 , 会需要额外的类型检查才能知道实际上调用的是哪个方法 。