JVM 面试必备(上)


JVM的组成

  1. 类加载器 在jvm启动时或者类运行将需要的Class加载到JVM中

  2. 执行引擎 负责执行Class文件中包含的字节码指令

  3. 内存区 (运行时数据区) 是JVM运行的时候操作所分配的内存区,运行时内存区主要分为

    1. 方法区: 用于存储类结构信息 包含常量池 静态常量 构造函数 运行时常量池

    2. java堆 存储java实例或者对象的地方,GC的主要区域

    3. java栈 和线程相关 每创建一个线程,jvm就会为这个线程创建一个对应的java栈,其中包含多个栈帧,每运行一个方法就是建一个栈帧,用于存储局部变量表, 操作栈 方法返回等,

    4. 程序计数器 用于保存当前线程执行的内存地址 由于JVM程序是多线程,所以保证线程切换回来,就需要单独的程序计数器记录之前中断的地方,

    5. 本地方法栈 和java栈差不多,不过为jvm的native方法服务

  4. 本地方法接口 主要调用C/C++ 实现本地方法以及回调结果

GC的原理和回收策略

java四种引用类型:

  • 强引用: 代码普遍存在, 只要强引用存在 GC就不会被回收掉引用的对象

  • 弱引用: 用来描述非必须对象 ,弱引用当GC发生时 无论内存是否足够都会回收

  • 软引用: 用来描述还有用但是非必须的对象,当内存你不足时候会被回收这类对象

  • 虚引用: 一哥对象是否有虚引用存在,完全不会对生存时间产生影响,无法通过虚引用获取对象,它的存在的目的就是对象呗回收时可以收到一个系统通知

可达性算法

  • 用来判断对象的引用是否存在

可达性算法通过一系列称为GCRoots的对象作为起始点,这些节点向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链存与GCRoots链接时说明这个对象不可达,(不可用)

可达性算法流程
  • 第一次标记 对象经过可达性分析后发现没有与GC Roots引用链 进行第一次标记并进行一次筛选 筛选条件: 该对象是否有必要执行finalize()没有覆盖finalize()或者finalize()已经执行过都会被认为没必要执行 如果没有必要执行,则会将该对象放在一个F-Queue队列,并稍后由虚拟机建立的低优先级Finalizer线程中触发这个对象的finalize方法 ,但是不保证一定会等待它执行结束,如果finalize发生死循环等,,

  • 第二次标记 GC对F-Queue队列的对象进行第二次标记 ,如果第二次标记的对象又被引用,则会被移除即将回收的集合

  • 总之 jvm在做垃圾回收时候,会检查堆中所有对象是否被这些根手机对象引用,不能够被引用的对象就会被垃圾收集器回收,

垃圾回收算法
  • 标记-清除 : 采用根集合进行扫描,对活的对象进行标记,再扫描整个空间中未被标记的对象,进行回收, 这个算法不需要对对象进行移动,并且仅对不存活的对象进行处理,

    • CMS 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • 标记-整理 : 和清除算法一样进行标记 但在清除时不同,在回收不存活的对象占用空间后,会将所有存活的对象往做单空闲位置移动,并更新对应的指针, 该回收算法使用于对象存活率高的场景(老年代)

    • Serial Old收集器 老年代单线程收集器,Serial收集器的老年代版本

    • Parallel Old收集器 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本

    • G1 Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片 ,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

  • 复制 : 将内存按容量划分为大小相等的二块,每次只使用其中一块,当这一块内存使用完毕,就将还活着的对象复制到另一块上,再把已经使用过的内存一次清理掉 适用于对象存活率较低的(新生代) 分半区回收

    • Serial收集器 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

    • ParNew收集器 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现

    • Parallel Scavenge收集器 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景

  • 分代收集算法 : 不同的对象的生命周期存活情况不一样,而不同的生命周期的对象位于堆中不同区域,因此对堆内存采用不用区域采用不同的策略进行回收可以提高jvm的执行效率

Java堆内存
  • 新生代 : (复制算法)

    • 所有新生成的对象首先放在新生代 新生代的目标就是尽可能快速收集掉那些生命周期短的对象

    • .新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区 大部分对象在Eden区中生成

    • 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高

  • 老年代

    • 新生代中经历N次垃圾回收任然存在的对象 就会被放到老年代,

    • 内存区域比新生代大很多, 当老年代存满,发生full GC 发生频率比较低 老年代的存活时间比较长

    • Full GC 就是新生代、老年代都进行回收

  • 永久代

    • 永久代主要存放静态文件 如java类 方法 ,永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些Class,反射 动态代理 GCLib 等bytecode 在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类

内存分配和回收策略

  • 对象优先在新生代(Eden)分配 当(Eden)区没有足够空间分配时,虚拟机发生一次 minor GC

  • 大对象直接进入老年代 如很长的字符串和数组

  • 长期存活的对象进入老年代 当对象在新生代经历一定次数(默认15) 的minor GC 就会进入老年代

  • 动态对象的判断, 为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

类加载器

类加载器
  • 一个Java 程序都是由若干个.class文件组织而成的一个完整的java 应用程序,当应用程序运行时,即会调用改程序的一个入口函数调用相关的系统功能,而这些功能都被封装到不同的class文件种, 所以经常从这class文件中调用另一个class文件的方法,如果另一个文件不存在,就会发生异常
  • 而程序在启动时候,并不会一次性加载程序所用的class文件 而是根据程序需要,通过java的类加载机制(ClassLoder)来动态加载某个class文件
双亲委托机制
  • 类的加载就是虚拟机通过类的全限定名来获取此类的二进制字节流,而完成这个加载动作就是类加载器
  • 类和类加载器息息相关 判断而各类是否相等,只有在这二个类被同一个类加载器加载的情况下才有意义.否则即便是二个类来自同一个Class文件,被不同的类加载器加载,他们也是不相等的

类加载器可以分为三类

  1. 启动类加载器: 负责加载\bin目录下或者被-XbootClassPath参数指定的路径.并且是被虚拟机识别的库到内存中

  2. 拓展类加载器: 负责加载\bin\ext 目录下或者被java.ext.dirs 系统变量所指定的路径的所有内加载到内存中

  3. 应用类加载器 : 负责加载用户类路径上的指定类库,如果应用程序中没有实现自己的加载类,一般就是这个类去加载应用程序中的类库

原理

ClassLoader 使用的是双亲委托模式来搜索类.每个CLassLoder实例都有一个父类加载器的引用,(不是继承关系 是包含的关系) 虚拟机内置的类加载器,(Bottstrap classLoder) 本身没有父类加载器,但是可以作用其它ClassLoder实例的父类加载器

当一个ClassLoder实例需要加载某个类的时候,它会试图搜索某个之前,它这个任务委托给它的父类加载器,这个过程由上至下一次检查,首先由最顶层的bootStrapClassLoder尝试加载,如果没有加载成功,则会把任务转交给ExtensionClassLoder试图加载,吐过没有加载到,则转交给AppClassLoder进行加载,如果它也没有加载到的话,就会转交给委托的发起者,由它指定的问价系统或者网络等待URL中加载该类 如果他们都没有加载到这个类,就会抛出ClassNotFoundException异常

类加载机制

类的加载值得是将类的.class文件中的二进制数据读取到内存中,将其放在运行时数据区的方法区内,然后在堆中创建创建一个java.lang.class对象,用来封装在方法区内的数据结构,类的加载最终是在堆区的Class对象,Class对象封装了类在方法区内的数据结构,并且向外界提供访问方法区内数据结构的接口

类加载有三种方式

  1. 命令行启动应用时候由JVM初始化加载

  2. 通过Class.forName()方法动态加载

  3. 通过ClassLoder.loadClass()动态加载

如果一个类加载器收到加载类的请求,它不会自己去加载这个类,它会先请求父类加载器,每层的加载器都是如此,层层传递,直到传递到最高层的类加载器,只有父类加载器反馈无法加载此类时才会让子类加载器去加载该类

双亲委托模式的优点

避免重复加载 当父类加载该类,就没有必要让子类再加载一次

jvm搜索类的时候 如何判断二个Class是相同的呢?

JVM在判定二个类是否相同 ,不仅要判断二个类名相同,而且要判断是否同一个类加载器实例加载,只有二者满足时JVM才会认为这二个Class是相同的

Android 类加载器

在Android系统中 最终的APK文件包含的dex类型的文件,dex文件是将Class文件重新打包,打包的规则是将Class文件内部的各种函数表进行优化,产生一个新文件,即是Dex文件,因此加载某种特殊的Class文件就需要特殊的类加载器DexClassLoder

可以动态加载Jar通过URLClassLoder

  1. ClassLoder 隔离问题 JVM识别一个类是由 ClassLoderId +packageName+ClassName

  2. 加载不同Jar包的公共类

    1. 让父类ClassLoder加载公共的jar 子类ClassLoder加载包含公共jar的jar 此时子类ClassLoder在加载jar时会先去父类加载ClassLoder中

    2. 重写加载包含jar的jar的classLoder 在Class中找到已经加载过公共jar的classLoder 是吧父类classLoder替换掉

    3. 生成包含公共jar的jar时候把公共jar去掉

java虚拟机的栈内存和堆内存

  • 栈内存 在函数中定义的一些基本类型变量和引用对象变量都在函数的栈内存中 当一段代码定义一个变量时 java就在栈中认为这个变量分配内存空间,当超过变量的作用域 java会自动释放掉为该变量分配的内存空间 ,

  • 堆内存 存放由new 创建的对象和数组 在堆内存分配的内存 有jvm的自动垃圾回收机制来管理

你可能感兴趣的:(JVM 面试必备(上))