共计 4607 个字符,预计需要花费 12 分钟才能阅读完成。
Java 是一种跨平台的语言,这意味着 Java 开发进去的程序通过编译后,能够在 Linux 上运行,也能够在 Windows 上运行;能够在 PC、服务器上运行,也能够在手机上运行;能够在 X86 的 CPU 上运行,也能够在 ARM 的 CPU 上运行。
因为不同操作系统,特地是不同 CPU 架构,是不可能执行雷同的指令的。而 Java 之所以有这种神奇的个性,就是因为 Java 编译的字节码文件不是间接在底层的零碎平台上运行的,而是在 Java 虚拟机 JVM 上运行,JVM 屏蔽了底层零碎的不同,为 Java 字节码文件结构了一个对立的运行环境。JVM 实质上也是一个应用程序,启动当前加载执行 Java 字节码文件。JVM 的全称是 Java Virtual Machine,你有没有想过,这样一个程序为什么被称为机器(Machine)呢?
其实,如果答复了这个问题,也就理解了 JVM 的底层结构了。这样在进行 Java 开发的时候,如果遇到各种问题,都能够思考一下在 JVM 层面是如何的?而后进一步查找材料、剖析问题,直至真正地解决问题。
JVM 的组成结构
要想晓得这个问题的答案,咱们首先须要理解 JVM 的结构。JVM 次要由类加载器、运行时数据区、执行引擎三个局部组成。
运行时数据区次要包含办法区、堆、Java 栈、程序计数寄存器。
办法区次要寄存从磁盘加载进来的类字节码,而在程序运行过程中创立的类实例则寄存在堆里。程序运行的时候,实际上是以线程为单位运行的,当 JVM 进入启动类的 main 办法的时候,就会为应用程序创立一个主线程,main 办法里的代码就会被这个主线程执行,每个线程有本人的 Java 栈,栈里寄存着办法运行期的局部变量。而以后线程执行到哪一行字节码指令,这个信息则被寄存在程序计数寄存器。
一个典型的 Java 程序运行过程是上面这样的。
通过 Java 命令启动 JVM,JVM 的类加载器依据 Java 命令的参数到指定的门路加载.class 类文件,类文件被加载到内存后,寄存在专门的办法区。而后 JVM 创立一个主线程执行这个类文件的 main 办法,main 办法的输出参数和办法内定义的变量被压入 Java 栈。如果在办法内创立了一个对象实例,这个对象实例信息将会被寄存到堆里,而对象实例的援用,也就是对象实例在堆中的地址信息则会被记录在栈里。堆中记录的对象实例信息次要是成员变量信息,因为类办法内的可执行代码寄存在办法区,而办法内的局部变量寄存在线程的栈里。
程序计数寄存器一开始寄存的是 main 办法的第一行代码地位,JVM 的执行引擎依据这个地位去办法区的对应地位加载这行代码指令,将其解释为本身所在平台的 CPU 指令后交给 CPU 执行。如果在 main 办法里调用了其余办法,那么在进入其余办法的时候,会在 Java 栈中为这个办法创立一个新的栈帧,当线程在这个办法内执行的时候,办法内的局部变量都寄存在这个栈帧里。当这个办法执行结束退出的时候,就把这个栈帧从 Java 栈中出栈,这样以后栈帧,也就是堆栈的栈顶就又回到了 main 办法的栈帧,应用这个栈帧里的变量,继续执行 main 办法。这样,即便 main 办法和 f 办法都定义雷同的变量,JVM 也不会弄错。这部分内容咱们在第一篇曾经探讨过,JVM 作为一个 machine,和操作系统的解决线程栈的的办法是一样的。
Java 的线程平安经常让人困惑,你能够试着从 Java 栈的角度去了解,所有在办法内定义的根本类型变量,都会被每个运行这个办法的线程放入本人的栈中,线程的栈彼此隔离,所以这些变量肯定是线程平安的。如果在办法里创立了一个对象实例,这个对象实例如果没有被办法返回或者放入某些内部的对象容器中的话,也就是说这个对象的援用没有来到这个办法,尽管这个对象被搁置在堆中,然而这个对象不会被其余线程拜访到,也是线程平安的。
相同,像 Servlet 这样的类,在 Web 容器中创立当前,会被传递给每个拜访 Web 利用的用户线程执行,这个类就不是线程平安的。但这并不意味着肯定会引发线程平安问题,如果 Servlet 类里没有成员变量,即便多线程同时执行这个 Servlet 实例的办法,也不会造成成员变量抵触。这种对象被称作无状态对象,也就是说对象不记录状态,执行这个对象的任何办法都不会扭转对象的状态,也就不会有线程平安问题了。事实上,Web 开发实际中,常见的 Service 类、DAO 类,都被设计成无状态对象,所以尽管咱们开发的 Web 利用都是多线程的利用,因为 Web 容器肯定会创立多线程来执行咱们的代码,然而咱们开发中却能够很少思考线程平安的问题。
咱们再回过头看 JVM,它封装了一组自定义的字节码指令集,有本人的程序计数器和执行引擎,像 CPU 一样,能够执行运算指令。它还像操作系统一样有本人的程序装载与运行机制,内存管理机制,线程及栈管理机制,看起来就像是一台残缺的计算机,这就是 JVM 被称作 machine(机器)的起因。
JVM 的垃圾回收
事实上,JVM 比操作系统更进一步,它岂但能够治理内存,还能够对内存进行主动垃圾回收。所谓主动垃圾回收就是将 JVM 堆中的曾经不再被应用的对象清理掉,开释贵重的内存资源。那么要想进行垃圾回收,首先一个问题就是如何晓得哪些对象是不再被应用的,能够清理的呢?
JVM 通过一种可达性剖析算法进行垃圾对象的辨认,具体过程是:从线程栈帧中的局部变量,或者是办法区的动态变量登程,将这些变量援用的对象进行标记,而后看这些被标记的对象是否援用了其余对象,持续进行标记,所有被标记过的对象都是被应用的对象,而那些没有被标记的对象就是可回收的垃圾对象了。所以你能够看进去,可达性剖析算法其实是一个援用标记算法。
进行完标记当前,JVM 就会对垃圾对象占用的内存进行回收,回收次要有三种办法。
第一种形式是清理 :将垃圾对象占据的内存清理掉,其实 JVM 并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为闲暇,记录在一个闲暇列表里,当应用程序须要创立新对象的时候,就从闲暇列表中找一段闲暇内存调配给这个新对象。
但这样做有一个很显著的缺点,因为垃圾对象是散落在内存空间各处的,所以标记进去的闲暇空间也是不间断的,当应用程序创立一个数组须要申请一段间断的大内存空间时,即便堆空间中有足够的闲暇空间,也无奈为应用程序分配内存。
第二种形式是压缩 :从堆空间的头部开始,将存活的对象拷贝放在一段间断的内存空间中,那么其余的空间就是间断的闲暇空间。
第三种办法是复制 :将堆空间分成两局部,只在其中一部分创建对象,当这个局部空间用完的时候,将标记过的可用对象复制到另一个空间中。JVM 将这两个空间别离命名为 from 区域和 to 区域。当对象从 from 区域复制到 to 区域后,两个区域替换名称援用,持续在 from 区域创建对象,直到 from 区域满。
上面这系列图能够让你直观地理解 JVM 三种不同的垃圾回收机制。
回收前:
清理:
压缩:
复制:
JVM 在具体进行垃圾回收的时候,会进行分代回收。绝大多数的 Java 对象存活工夫都十分短,很多时候就是在一个办法内创建对象,对象援用放在栈中,当办法调用完结,栈帧出栈的时候,这个对象就失去援用了,成为垃圾。针对这种状况,JVM 将堆空间分成新生代(young)和老年代(old)两个区域,创建对象的时候,只在新生代创立,当新生代空间有余的时候,只对新生代进行垃圾回收,这样须要解决的内存空间就比拟小,垃圾回收速度就比拟快。
新生代又分为 Eden 区、From 区和 To 区三个区域,每次垃圾回收都是扫描 Eden 区和 From 区,将存活对象复制到 To 区,而后替换 From 区和 To 区的名称援用,下次垃圾回收的时候持续将存活对象从 From 区复制到 To 区。当一个对象通过几次新生代垃圾回收,也就是几次从 From 区复制到 To 区当前,仍然存活,那么这个对象就会被复制到老年代区域。
当老年代空间已满,也就是无奈将新生代中屡次复制后仍然存活的对象复制进去的时候,就会对新生代和老年代的内存空间进行一次全量垃圾回收,即 Full GC。所以依据应用程序的对象存活工夫,正当设置老年代和新生代的空间比例对 JVM 垃圾回收的性能有很大影响,JVM 设置老年代新生代比例的参数是 -XX:NewRatio。
JVM 中,具体执行垃圾回收的垃圾回收器有四种。
第一种是 Serial 串行垃圾回收器 ,这是 JVM 晚期的垃圾回收器,只有一个线程执行垃圾回收。
第二种是 Parallel 并行垃圾回收器 ,它启动多线程执行垃圾回收。如果 JVM 运行在多核 CPU 上,那么显然并行垃圾回收要比串行垃圾回收效率高。
在串行和并行垃圾回收过程中,当垃圾回收线程工作的时候,必须要进行用户线程的工作,否则可能会导致对象的援用标记错乱,因而垃圾回收过程也被称为 stop the world,在用户视角看来,所有的程序都不再执行,整个世界都进行了。
第三种 CMS 并发垃圾回收器 ,在垃圾回收的某些阶段,垃圾回收线程和用户线程能够并发运行,因而对用户线程的影响较小。Web 利用这类对用户响应工夫比拟敏感的场景,实用 CMS 垃圾回收器。
最初一种是 G1 垃圾回收器 ,它将整个堆空间分成多个子区域,而后在这些子区域上各自独立进行垃圾回收,在回收过程中垃圾回收线程和用户线程也是并发运行。G1 综合了以前几种垃圾回收器的劣势,实用于各种场景,是将来次要的垃圾回收器。
总结
JVM 有很多配置参数,Java 开发过程中也可能会遇到各种问题,理解了 JVM 的根本结构,能够帮忙咱们从原理下来解决问题。
比方遇到 OutOfMemoryError,咱们就晓得是堆空间有余了,可能是 JVM 调配的内存空间不足以让程序失常运行,这时候咱们须要通过调整 -Xmx 参数减少内存空间。也可能是程序存在内存透露,比方一些对象被放入 List 或者 Map 等容器对象中,尽管这些对象程序曾经不再应用了,然而这些对象仍然被容器对象援用,无奈进行垃圾回收,导致内存溢出,这时候能够通过 jmap 命令查看堆中的对象状况,剖析是否有内存透露。
如果遇到 StackOverflowError,咱们就晓得是线程栈空间有余,栈空间有余通常是因为办法调用的档次太多,导致栈帧太多。咱们能够先通过栈异样信息察看是否存在谬误的递归调用,因为每次递归都会使嵌套办法调用更深刻一层。如果调用是失常的,能够尝试调整 -Xss 参数减少栈空间大小。
如果程序运行卡顿,局部申请响应提早比拟厉害,那么能够通过 jstat 命令查看垃圾回收器的运行状况,是否存在较长时间的 FullGC,而后调整垃圾回收器的相干参数,使垃圾回收对程序运行的影响尽可能小。
执行引擎在执行字节码指令的时候,是解释执行的,也就是每个字节码指令都会被解释成一个底层的 CPU 指令,然而这样的解释执行效率比拟差,JVM 对此进行了优化,将频繁执行的代码编译为底层 CPU 指令存储起来,前面再执行的时候,间接执行编译好的指令,不再解释执行,这就是 JVM 的即时编译 JIT。Web 应用程序通常是长时间运行的,应用 JIT 会有很好的优化成果,能够通过 -server 参数关上 JIT 的 C2 编译器进行优化。
总之,如果你了解了 JVM 的结构,在进行 Java 开发的时候,遇到各种问题,都能够思考一下,这在 JVM 层面是如何的?而后进一步查找材料、剖析问题,这样就会真正解决问题,而且通过这样一直地思考剖析,你对 Java,对 JVM,甚至对整个计算机的原理体系以及设计理念都会有更多意识和领悟。
本文由 mdnice 多平台公布