JVM面试复习
先上图,妈的说个题外话,工作真的难找,吐血!!!!
首先JVM 是可运行 Java 代码的假想计算机 , 括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
之所以说可以跨平台就是因为JVM的存在。
通过这个路径就可以实现跨平台
这是一张经典图,里面记录了JVM中的内存基础模型,同时也画出了基本的流程。
当我们的class文件进入到JVM中的时候,首先会有一个初始化的过程,当线程本地存储、缓冲区分配、同步对象、Java虚拟机栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时,会释放原生线程和 Java 线程的所有资源。
- 虚拟机线程:这个线程等待 JVM 到达安全点(前面的文章有说过)操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
-
周期性任务线程:这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
-
GC 线程:这些线程支持 JVM 中不同的垃圾回收活动
-
编译器线程:这些线程在运行时将字节码动态编译成本地平台相关的机器码。
-
信号分发线程:这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。
上图为JVM内存的详细模型图,具体的含义可以看我之前的文章《自动内存管理机制》这边要注意的是方法区其实真正意义上的永久代,另外栈一般指的是虚拟机栈,堆指的是数据区
详细说一下之前没写到的。
本地变量表:是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。最简单的例子就是方法内定义的数据。一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
操作数栈:也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
JVM运行时的内存
其实按照严格的意义上讲,GC的算法不同对于区的分配是不同的,就像远古时期的时候,直接一个计数器就可以分出新生和老年代。但是随着算法的进步以及JVM的完善,现在目前我们使用的JDK1.8使用的GC算法,把新生代再一次划分成了三个不同的区用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
这一张是GC需要复习到的图。
引用计数法:这是一个简单而高效的方法,就像“大道至简”,其实我个人对这个的看法是不一样的,包括python目前也是使用这一种的方法来进行回收。我只需要找这个内存有没有被引用就可以知道有没有被使用,如果没有被使用那么自然可以被回收。其实这我怀疑也是最早JVM设计时候的想法。JVM那时候提出的期望就是:无需程序员去操作内存并且JVM会是一个通用的语言虚拟机,事实证明JVM目前已经可以支持多种语言了,例如Jython。
可达性分析算法:从字面其实我们就可以看得出,可达=>可以达到。将GC roots(a.虚拟机栈栈桢中的本地变量表b.方法区中的类静态属性引用的对象c.方法区中的常量引用的对象d.本地方法栈中JNI的引用的对象)作为根,从上到下的找,这个GC roots所需要的东西中的引用的对象.不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
JAVA 四中引用类型
阻塞型和非阻塞型IO模型和多复用型IO
非阻塞型:当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。说白了就是乐观锁,我先做,做的不对就继续做,知道成功。
多复用型IO:在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。
1)文件格式的验证,验证字节流是否符合Class文件的规范,是否能被当前版本的虚拟机处理
2)元数据验证,对字节码描述的信息进行语义分析,确保符合java语言规范
3)字节码验证 通过数据流和控制流分析,确定语义是合法的,符合逻辑的
4)符号引用验证 这个校验在解析阶段发生
准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念。例如public static int v = 8080;在这一步其实是为0,但是如果是public static final int v = 8080;则就是8080
初始化阶段是类加载最后一个阶段,前面的都加载完才会进行到这一步,在之前的加载中基本都是JVM在自己进行,和我们书写的部分没有关系(除了写过自定义类加载器),但是之后的基本都是我们书写的java代码开始运行了。
- 类构造器<client> 是由编译器自动收集类中的类变量的赋值操作和静态代码块中的语句合并而成的,但是他是有顺序的,必须先父类的<client>完成才会进行到子类的<client>操作。同时如果一个类是没静态变量或者静态代码块不会进行到这一步。就不会生产<client>这个操作
注意以下几种方式不会进行初始化的工程:
1.子类引用父类的静态变量,这样的话就只会对父类进行代码初始化工作
2.数组对象不会有初始化工作(数组是存放在堆当中的,然后返回的是一个地址,在对类的使用中,其实是一个被动的操作,所以被动引动都不会促发初始化操作)
3.常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4.通过类名获取 Class 对象,不会触发类的初始化。
5.通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
类加载器:java是将类加载器放置在JVM外部来进行实现的,JVM一共提供了三种类加载器。
双亲委派模型:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
说白就是所有的加载器都交给最顶层,这样的话出来的OBJ就都是由一个生产出来的了 。
包括图中可以 看出123都是交给启动类加载器的,然后启动类加载器再去一层一层去实现。