JVM工作原理和工作流程简述

JAVA之所以跨平台,是因为有JVM这么一个编译和运行机器,它令对于系统的操作对于用户而言是黑盒的,使得开发人员更快速和更注重软件功能的实现。然而,也因为jvm是黑盒,所以内部和底层具有不确定性,如果用状态机来表示jvm,那么jvm就是一种现役复制不确定的状态机,因为它的状态和表现跟系统、底层、硬件等等都有关系,从而状态是不确定,如果在分布式应用中,jvm一直以来兼容性都不是很好,这就是主要原因。尽管如此,就单一的系统而言,弄清楚jvm运行的来龙去脉,对于系统的运行至关重要。

理解jvm的运行原理具有以下几点充分作用:

1、针对系统进行内存和垃圾回收监控

2、解决因内存溢出和泄露的问题

3、对系统进行优化

4、提升jvm和系统性能

jvm的运行原理有主要有三方面,其实这也是jvm的主要工作:

1、内存管理

2、执行流程

3、垃圾回收

在开始之前,有一些知识需要知道,广义来讲,jvm并不是指sun的hotspot,而是一个规范,因此不同厂商会根据规范实现不同的jvm,因此这些jvm的表现都不是一致甚至相差甚远。在jvm规范中,通常我们所能接触的就是命令行参数了。

命令行参数

命令行参数分为三种,标准、非标准、非稳定

标准命令行参数会在jvm规范中明确列出,强制实现的选项,并且具有版本控制废弃的管理通知。非标准的命令行参数不是规范强制并且可能没有对应的通知,非稳定参数是特定调校的选项,同时也是非标准的。标准的选项可通过help命令查看,非标准的选项通过-X为前缀访问,非稳定的前缀是-XX,通常对于布尔类型的选项,用+或-来设置true或者false,如-XX:+UseTLAB开启线程内存缓冲分配。

内存划分

jvm是具有内存自动分配和管理的架构,而内存管理自动化是解放劳动力的重要工具,可以对比C/C++,开发人员不需要管理内存,开发效率会比较高。

在jvm中,使用的内存分为两类,线程共享内存和线程私有内存。

结合我们平时的代码可以看出,线程共享的内容包括方法、实例对象、常量,分别对应共享内存中的方法区、堆区、常量池。

堆区

堆区通常是共享内存中最大的一块,因此它也是GC重点关注区域。堆区可能是连续的也可能是不连续的,以及堆区的大小都会对GC造成相应的影响。-Xms和-Xmx设置堆的最小和最大值,如果堆内存大小超过最大值,则抛出OutOfMemoryError异常。

方法区

方法区存储的是方法、类的结构信息,而常量池也包含在内,除了我们代码中所看到的静态常量,这些常量还包括一些字节码内容和类初始化所需的特殊内容。通常情况下,jvm不会对方法区GC直到方法区大小不够,即使GC也只是针对常量池和类型,所以也被称为永久区Permanent Generation,除了可以设置大小以外,还可以设置是否进行GC,如果超过大小,抛出OutOfMemoryError异常。

常量池

这里说的常量池是运行时的,通常是字节码中的类的版本、描述,以及常量池表,这个表是一种符号表,在运行的时候将这些符号的引用变为直接符号。由此可以看出,加载类会使用常量池和方法区,如果类过多或常量过多,也会抛出OutOfMemoryError异常。

线程私有内存区

线程私有内存是被某一独立线程独占的,包括PC寄存器、java栈、本地方法栈

PC寄存器

这个寄存器是jvm内部的,而非物理寄存器,因此也可以看出,jvm的指令执行是基于栈架构的,所有的操作都是经过入栈出栈完成,为了确保线程安全,它被设定为线程私有的。通常,栈中存储字节码指令地址,如果调用的是本地方法,即native方法,则是空值。会不会抛出OutOfMemoryError异常,jvm目前没有明确规定。

java栈

java栈的颗粒度比PC寄存器大,存储方法的局部变量、操作计数、方法返回\方法出口等信息。局部变量除了我们代码所接触的类型,还包括一种叫做returnAddress返回地址类型,也是一种jvm规范的原始类型,但是开发人员并不能使用,实际上这种类型标识一条字节码指令的操作吗。java栈也会OutOfMemoryError异常,不过他也是可以动态扩展的。

本地方法栈

用于支持本地方法调用时使用,但是jvm没有强制实现,和java栈类似。

执行流程

我们的代码在IDE中或者通过CMD来执行即可看到执行结果,而实际上每次执行都会启动和关闭jvm,这个过程是相当复杂的,下面罗列一下主要步骤。

对于sun的hotspot,launcher负责维护jvm的生命周期,包括启动和结束关闭。就是我们在java目录下看到的java.exe和javaw.exe,一个有控制台输出,另一个没有,用于执行GUI程序。

jvm的启动初始化

1、解析命令行参数,设置内存大小和JIT编译器,并且加载系统环境变量。

2、查找主类,并且调用本地方法JNI_CreateJavaVM创建jvm主线程。

3、当jvm初始化完成,就会加载主类,如果加载成功,则调用本地方法传入参数,然后开始执行java的程序了。

其中调用本地方法JNI_CreateJavaVM创建jvm主线程,是jvm的启动过程,实际上启动器并非直接调用该本地方法,而是先用main()函数创建主线程,然后通过主线程调用javamain()函数调用该JNI_CreateJavaVM方法创建子线程来完成初始化并执行java程序。因为创建的主线程是操作系统分配的初始线程,为了更好的定制线程,通过在该线程上创建再初始线程来初始化jvm。

进一步细化JNI_CreateJavaVM函数的执行内容,主要流程如下:

1、检查是否线程安全,也就是是否只有一个线程调用此方法,一个线程只能创建一个jvm实例。

2、初始化各个模块,如日志、计数器、内存页等。

3、加载核心库并初始化线程库

4、初始化全局数据,这步完成后就可以创建java子线程了

5、初始化类加载器、解析器、编译器、GC等模块。其中重要一点就是初始化universe类型,这种类型是java种一切类型的类型,是一种数据结构,所有java的存储对象都用该类型类存储。

6、加载并初始化基础类库,如Lang、System、reflect等包。

7、返回给调用者。

通过上面的步骤,可以发现基础类库是在初始化阶段完成加载的,这跟开发人员编写的类库加载顺序是不同的。

jvm的关闭

当java程序结束,jvm会先检查有无未处理的异常以及清理这些异常,然后调用本地方法断开主线程跟本地方法接口的连接,如果可以断开,说明已经没有线程在运行了,则可以安全的关闭jvm。

和JNI_CreateJavaVM方法对应的是DestroyJavaVM方法,当jvm在启动和运行时发生错误,根据严重程度会调用该方法关闭jvm,而在理想状态下,即正常运行直到退出,也是调用DestroyJavaVM方法关闭并销毁jvm。停止jvm按照以下主要步骤进行:

1、守护线程一致等待,直到只有一个非守护线程执行。

2、停止监控、计数器等线程。

3、移除当前线程,释放保护页。

4、释放所有资源,返回到调用者。

我们可以看出,当需要关闭jvm时,如果jvm中仍有线程在运行,是无法强制关闭的,这就是为什么我们很多代码的运行出现异常后,重复的调试导致有多个后台jvm在运行却不能自动结束而要手动关闭。

类加载机制

在前面说到,开发人员使用的类和基础类库并非同一时间加载的,这是有原因的。类的加载由类加载器来完成,包括加载、连接、初始化三个阶段。完成加载后就可以通过new来创建类的实例对象了。类的加载可以理解为根据类的字节码文件全路径名读取后转换为与目标类型一致的Class类型,并且是可以动态加载的。

加载类由类加载器完成,加载器分为两种,一种是Bootstrap Classloader引导加载器,另一种是User-defined Classloader用户自定义加载器,用户自定义加载器默认又分为ExtClassloader和AppClassloader。

引导加载器是C++编写的,负责完成lib目录里的类加载,也就是前面所说的基础类库,而ExtClassloader和AppClassloader是java编写的,分别负责加载lib/ext目录和ClassPath系统路径中的类型。他们都是Classloader的子类,我们也可以通过继承父类来实现自己的类加载器。

父类委托模式

通过查阅类关系树可以发现,AppClassloader是ExtClassloader的子类,而ExtClassloader则是Classloader的子类,java规范要求自定义的类加载器都派生与父类,并且在进行类加载的时候,都要委托给直接上级父类执行加载,这就是父类委托模式(parents delegation model),国内很多翻译为双亲委托模式,但是你会发现是多亲模式,所以我认为父类委托更为合适。

父类委托模式在执行时,子类始终会委托父类加载,一级一级的向上请求,知道最后唯一的超类来进行加载,如果父类无法加载,再一级一级的退回到子类进行加载,这样就不会重复加载相同的类了。

为什么要使用父类委托模式?因为类的加载必须是一次性不可重复的,试想一下,如果基础类库中的类可以重复加另一个类来替换原来的类,那是多么严重的安全隐患,为了避免这一点,基础类库都是由C++编写的启动加载器来加载,但是为了兼顾扩展性,所以除了基础类库,其他的类都可以通过用户加载器来加载,那么为了避免但不强制要求避免重复加载的情况发生,java规范就采取并建议我们按照父类委托的方式实现类加载器。

类的加载过程

前面说到,类先经过类加载器将字节码文件转换为Class对象,但是这个时候并不能使用它,此时的类结构信息存储在方法区内,还需要对其进行验证,结构信息是否有效合法,一旦通过验证,就会为类中的静态变量分配内存空间并初始化值,这些准备工作完成后,还需将类结构中的符号和常量表的符号进行解析转为直接引用,这时候的类才具有执行能力。最后的工作就是初始化了,也就是我们代码中在new一个对象之前会执行的static代码块。

垃圾回收机制

jvm的垃圾回收包括内存动态分配和内存回收两大块。内存的分配和垃圾回收是息息相关的,内存分配的方式一定程度上决定采取何种垃圾收集器和收集算法。

前面说到,堆内存可以是连续也可以是不连续的,也是GC的重点区域,但正由于这种分布的不确定性,该GC带来很大麻烦。首先针对连续的情况。

指针碰撞

通过前面讲述的jvm启动过程,我们知道创建对象就需要在堆内存中划分出一部分来存储对象,如果此时的内存是规整的,那么将空闲的和已使用的各放置一边,两部分的边界处用一个指针标记,当新增对象内存分配,就将指针偏移相应的位置,下一次分配内存只需要知道最后指针偏移的位置开始分配内存并更新指针偏移量即可,这种方式就是指针碰撞(bump the pointer)。

空闲列表

然而,需要面临的一个问题首先不是规整问题,而是线程安全,如果对指针的操作加锁,必然会降低性能。并且如果堆不是连续的,指针碰撞就变得很棘手,此时还有一种解决办法,就是通过一张表记录下所有空闲的内存,每当分配内存就更新表上的记录,这种方式就是空闲列表(free list)。

不管呢种方式,都必须解决线程安全,对于指针碰撞,为了满足规整的先决条件,这就要求GC收集器具有压缩规整功能,如serial、par等收集器,而采用mark-sweep的cms这种收集器则不支持规整,因为他就是通过空闲列表方式来整理的内存的。分配内存就需要对内存指针进行操作,如何确保指针的使用是线程安全的?一种做法是用过CAS原子操作来实现,也就是所谓的失败重试保证更新原子性。还有一种做法就是TLAB(本地线程缓冲),即在堆内存中事先划分一块线程独占的私有内存,这样线程就可以互不干涉的创建对象了,如果TLAB不够用,再已加锁的方式分配TLAB,并且对象的初始化还可以提前进行。

分代划分收集机制

目前大部分的GC都是采用分代收集算法的,换而言之,也就是内存是分代划分的。这当中的设计有很多复杂和严格的要求,首先对算法绝对精确,不能造成误删和误读,还要保证没用的对象及时回收,以及如何处理产生的碎片和系统停顿开销等。涉及的指标和算法,就在另一篇中单独阐述了。

待续......

到此这篇关于关于JVM工作原理简述的文章就介绍到这了,更多相关JVM工作原理简述内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java程序员必须知道的5个JVM命令行标志

    本文是Neward & Associates的总裁Ted Neward为developerworks独家撰稿"你不知道5个--"系列中的一篇,JVM是多数开发人员视为理所当然的Java功能和性能背后的重负荷机器.然而,我们很少有人能理解JVM是如何进行工作的-像任务分配和垃圾收集.转动线程.打开和关闭文件.中断和/或JIT编译Java字节码,等等. 不熟悉JVM将不仅会影响应用程序性能,而且当JVM出问题时,尝试修复也会很困难. 本文将介绍一些命令行标志,您可以使用它们来诊断和

  • 基于JVM 调优的技巧总结分析

    这篇是技巧性的文章,如果要找关于GC或者调整内纯的文章,看我其他几篇文章.因为是JVM 调优总结,所以废话少说.从各方面一共收集到以下几个方法:1.升级 JVM 版本.如果能使用64-bit,使用64-bit JVM.    基本上没什么好解释的,很简单将JVM升级到最新的版本.如果你还是使用JDK1.4甚至是更早的JVM,那你首先要做的就是升级.因为JVM从1.4- >1.5->1.6可不是仅仅的版本号升级,或者仅仅往里面加了一堆新的语言特性,这么简单.而是真正在JVM做了重大的改进,每次版

  • Java JVM程序指令码实例解析

    这篇文章主要介绍了Java JVM程序指令码实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java程序转化为JVM指令码分析 1.编写java文件(简易示例) /** * @author yew * @date on 2019/12/9 - 15:53 */ public class MainTest { public static void main(String[] args) { int a =1; int b=2; int c

  • JVM的垃圾回收算法工作原理详解

    怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

  • JVM核心教程之JVM运行与类加载全过程详解

    为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性.例如: 1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类: 2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分:(这个是Android插件化,动态安装更新apk的基础) 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程

  • JVM运行时数据区划分原理详解

    Java内存空间 内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了JAVA在运行过程中内存申请.分配.管理的策略,保证了JVM的高效稳定运行.不同的jvm对于内存的划分方式和管理机制存在着部分差异(对于Hotspot主要指方法区) (图源阿里)JDK8的元数据区+JIT编译产物 就是JDK8以前的方法区 JavaAPI中的Runtime public class Runtime extends Object Every Java

  • JVM对象创建和内存分配原理解析

    创建对象 当 JVM 收到一个 new 指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,如果没有的话则要进行一次类加载. 接着就是分配内存了,通常有两种方式: 指针碰撞 空闲列表 使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可. 当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式.虚拟机会维护一个空闲的列表,用于记

  • JVM常用指令速查表

    JVM 基本指令 基本指令集是最常用的,总结如下: 指令 释义 iconst_1 int型常量值1进栈 bipush 将一个byte型常量值推送至栈顶 iload_1 第二个int型局部变量进栈,从0开始计数 istore_1 将栈顶int型数值存入第二个局部变量,从0开始计数 iadd 栈顶两int型数值相加,并且结果进栈 return 当前方法返回void getstatic 获取指定类的静态域,并将其值压入栈顶 putstatic 为指定的类的静态域赋值 invokevirtual 调用实

  • JVM工作原理和工作流程简述

    JAVA之所以跨平台,是因为有JVM这么一个编译和运行机器,它令对于系统的操作对于用户而言是黑盒的,使得开发人员更快速和更注重软件功能的实现.然而,也因为jvm是黑盒,所以内部和底层具有不确定性,如果用状态机来表示jvm,那么jvm就是一种现役复制不确定的状态机,因为它的状态和表现跟系统.底层.硬件等等都有关系,从而状态是不确定,如果在分布式应用中,jvm一直以来兼容性都不是很好,这就是主要原因.尽管如此,就单一的系统而言,弄清楚jvm运行的来龙去脉,对于系统的运行至关重要. 理解jvm的运行原

  • 深入解析Session工作原理及运行流程

    一.session的概念及特点 session概念:在计算机中,尤其是在网络应用中,称为"会话控制".Session 对象存储特定用户会话所需的属性及配置信息.说白了session就是一种可以维持服务器端的数据存储技术.session主要有以下的这些特点: session保存的位置是在服务端 session一般来说要配合cookie使用,如果用户浏览器禁用了cookie,那么只能使用URL重写来实现session的存储功能 单纯的使用session来存储用户回话信息,那么当用户量较多时

  • DDNS 的工作原理及其在 Linux 上的实现

    DDNS 工作原理的分析 DDNS 的实现最根本的一点是当主机的 IP 地址发生变化的时候,实现 DNS 映射信息的及时更新,应用程序需要及时地获得这一信息,主要的方法可分为两大类: 一类是轮询机制,即:应用程序每隔一定的时间,去从查询主机当前的 IP 地址,并与之前的进行比较,从而判断网络地址是否发生了变化.显然,这种方法不仅效率低下,而且对每次查询 IP 地址的时间间隔很难得到一个折中的数值. 第二类方法是异步实现方式,即:每当主机的 IP 地址发生变化的时候,应用程序能够被及时地通知到.这

  • 深度剖析Java中的内存原型及工作原理

    本文主要通过分析Java内存分配的栈.堆以以及常量池详细的讲解了其的工作原理. 一.java虚拟机内存原型 寄存器:我们在程序中无法控制栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中堆:存放用new产生的数据静态域:存放在对象中用static定义的静态成员常量池:存放常量非RAM存储:硬盘等永久存储空间. 二.常量池(constant pool) 常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据.除了包含代码中所定义的各种基本类型(如int.

  • Java中GC的工作原理详细介绍

    Java中GC的工作原理 引子:面试时被问到垃圾回收机制,只是粗略的讲'程序员不能直接对内存操作,jvm负责对已经超过作用域的对象回收处理',面官表情呆滞,也就没再继续深入. 转文: 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能提高整个应用程序的性能.本文将从GC的工作原理.GC的几个关键问题进行探讨,最后提出一些Java程序设计建议,如何从GC角度提高Ja

  • 深入了解Java GC的工作原理

    JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代下载地址  和旧生代采用不同的垃圾回收机制. 首先来看一下JVM内存结构,它是由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示. JVM学习笔记 JVM内存管理和JVM垃圾回收 JVM内存组成结构 JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆

  • java的JIT 工作原理简单介绍

    1.JIT的工作原理图 工作原理 当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器.JIT编译器将字节码编译成本机机器代码. 通常javac将程序源代码编译,转换成java字节码,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译.很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢.为了提高执行速度,引入了JIT技术. 在运行时JIT会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,采用该JIT技术可以,可以接

  • 浅析ASP.NET路由模型工作原理

    ps:这是针对ASP.NET4.5版本的,好像在最新的5.0版本中加入了OWIN,彻底解耦了和Web服务器的耦合,我还没有研究过,不敢妄言4.5的模型适用5.0. action*0x1:大话ASP.NET模型 首先我们先来了解下一个请求的悲欢离合的命运,看看它的一生中所走过的蜿蜒曲折的道路.如下图所示: 在如上所示的风光旖旎的画卷中,我们可以看到一个"请求"从客户端浏览器出发,经历千山万水到达服务器,服务器的内核模块的HTTP.SYS热情款待了它,对它进行简单的修饰之后,就和它依依惜别

  • Ajax工作原理深入理解

    1.ajax技术的背景 不可否认,ajax技术的流行得益于google的大力推广,正是由于google earth.google suggest以及gmail等对ajax技术的广泛应用,催生了ajax的流行.而这也让微软感到无比的尴尬,因为早在97年,微软便已经发明了ajax中的关键技术,并且在99年IE5推出之时,它便开始支持XmlHttpRequest对象,并且微软之前已经开始在它的一些产品中应用ajax,比如说MSDN网站菜单中的一些应用.遗憾的是,不知道出于什么想法,当时微软发明了aja

  • Java虚拟机工作原理

    首先我想从宏观上介绍一下Java虚拟机的工作原理.从最初的我们编写的Java源文件(.java文件)是如何一步步执行的,如下图所示,首先Java源文件经过前端编译器(javac或ECJ)将.java文件编译为Java字节码文件,然后JRE加载Java字节码文件,载入系统分配给JVM的内存区,然后执行引擎解释或编译类文件,再由即时编译器将字节码转化为机器码.主要介绍下图中的类加载器和运行时数据区两个部分. 类加载 类加载指将类的字节码文件(.class)中的二进制数据读入内存,将其放在运行时数据区

随机推荐