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栈、本地方法栈
这个寄存器是jvm内部的,而非物理寄存器,因此也可以看出,jvm的指令执行是基于栈架构的,所有的操作都是经过入栈出栈完成,为了确保线程安全,它被设定为线程私有的。通常,栈中存储字节码指令地址,如果调用的是本地方法,即native方法,则是空值。会不会抛出OutOfMemoryError异常,jvm目前没有明确规定。
java栈的颗粒度比PC寄存器大,存储方法的局部变量、操作计数、方法返回/方法出口等信息。局部变量除了我们代码所接触的类型,还包括一种叫做returnAddress返回地址类型,也是一种jvm规范的原始类型,但是开发人员并不能使用,实际上这种类型标识一条字节码指令的操作吗。java栈也会OutOfMemoryError异常,不过他也是可以动态扩展的。
用于支持本地方法调用时使用,但是jvm没有强制实现,和java栈类似。
我们的代码在IDE中或者通过CMD来执行即可看到执行结果,而实际上每次执行都会启动和关闭jvm,这个过程是相当复杂的,下面罗列一下主要步骤。
对于sun的hotspot,launcher负责维护jvm的生命周期,包括启动和结束关闭。就是我们在java目录下看到的java.exe和javaw.exe,一个有控制台输出,另一个没有,用于执行GUI程序。
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、返回给调用者。
通过上面的步骤,可以发现基础类库是在初始化阶段完成加载的,这跟开发人员编写的类库加载顺序是不同的。
当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工作原理简述内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
时间:2020-07-13
这篇文章主要介绍了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
Java内存空间 内存是非常重要的系统资源,是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了JAVA在运行过程中内存申请.分配.管理的策略,保证了JVM的高效稳定运行.不同的jvm对于内存的划分方式和管理机制存在着部分差异(对于Hotspot主要指方法区) (图源阿里)JDK8的元数据区+JIT编译产物 就是JDK8以前的方法区 JavaAPI中的Runtime public class Runtime extends Object Every Java
本文是Neward & Associates的总裁Ted Neward为developerworks独家撰稿"你不知道5个--"系列中的一篇,JVM是多数开发人员视为理所当然的Java功能和性能背后的重负荷机器.然而,我们很少有人能理解JVM是如何进行工作的-像任务分配和垃圾收集.转动线程.打开和关闭文件.中断和/或JIT编译Java字节码,等等. 不熟悉JVM将不仅会影响应用程序性能,而且当JVM出问题时,尝试修复也会很困难. 本文将介绍一些命令行标志,您可以使用它们来诊断和
为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性.例如: 1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类: 2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分:(这个是Android插件化,动态安装更新apk的基础) 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程
JVM 基本指令 基本指令集是最常用的,总结如下: 指令 释义 iconst_1 int型常量值1进栈 bipush 将一个byte型常量值推送至栈顶 iload_1 第二个int型局部变量进栈,从0开始计数 istore_1 将栈顶int型数值存入第二个局部变量,从0开始计数 iadd 栈顶两int型数值相加,并且结果进栈 return 当前方法返回void getstatic 获取指定类的静态域,并将其值压入栈顶 putstatic 为指定的类的静态域赋值 invokevirtual 调用实
怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法
创建对象 当 JVM 收到一个 new 指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,如果没有的话则要进行一次类加载. 接着就是分配内存了,通常有两种方式: 指针碰撞 空闲列表 使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可. 当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式.虚拟机会维护一个空闲的列表,用于记
这篇是技巧性的文章,如果要找关于GC或者调整内纯的文章,看我其他几篇文章.因为是JVM 调优总结,所以废话少说.从各方面一共收集到以下几个方法:1.升级 JVM 版本.如果能使用64-bit,使用64-bit JVM. 基本上没什么好解释的,很简单将JVM升级到最新的版本.如果你还是使用JDK1.4甚至是更早的JVM,那你首先要做的就是升级.因为JVM从1.4- >1.5->1.6可不是仅仅的版本号升级,或者仅仅往里面加了一堆新的语言特性,这么简单.而是真正在JVM做了重大的改进,每次版
作为Android开发,日常的开发工作中或多或少要接触到性能问题,比如我的Android程序运行缓慢卡顿,并且常常出现ANR对话框等等问题.既然有性能问题,就需要进行性能优化.正所谓工欲善其事,必先利其器.一个好的工具,可以帮助我们发现并定位问题,进而有的放矢进行解决.本文主要介绍StrictMode 在Android 应用开发中的应用和一些问题. 什么是StrictMode StrictMode意思为严格模式,是用来检测程序中违例情况的开发者工具.最常用的场景就是检测主线程中本地磁盘和网络读写
堆设置 -Xmx3550m:设置JVM最大堆内存为3550M. -Xms3550m:设置JVM初始堆内存为3550M.此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存. -Xss128k:设置每个线程的栈大小.JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K.应当根据应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右. -Xmn2g:设置堆
JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.是运行Java应用最底层部分. JDK(Java Development kit) 整个Java的核心,包括了Java运行环境(Java Runtime E
JavaScript 是一个比较完善的前端开发语言,在现今的 web 开发中应用非常广泛,尤其是对 Web 2.0 的应用.随着 Web 2.0 越来越流行的今天,我们会发现:在我们的 web 应用项目中,会有大量的 JavaScript 代码,并且以后会越来越多.JavaScript 作为一个解释执行的语言,以及它的单线程机制,决定了性能问题是 JavaScript 的软肋,也是 web 软件工程师们在写 JavaScript 需要高度重视的一个问题,尤其是针对 Web 2.0 的应用.绝大多
一.需求和初步实现很简单的一个windows服务:客户端连接邮件服务器,下载邮件(含附件)并保存为.eml格式,保存成功后删除服务器上的邮件.实现的伪代码大致如下: 复制代码 代码如下: public void Process() { var recordCount = 1000;//每次取出邮件记录数 while (true) { using (var client = new Pop
程序性能的主要表现点: 执行速度:程序的反映是否迅速,响应时间是否足够短 内存分配:内存分配是否合理,是否过多地消耗内存或者存在内存泄漏 启动时间:程序从运行到可以正常处理业务需要花费多少时间 负载承受能力:当系统压力上升时,系统的执行速度.响应时间的上升曲线是否平缓 衡量程序性能的主要指标: 执行时间:程序从运行到结束所使用的时间 CPU时间:函数或者线程占用CPU的时间 内存分配:程序在运行时占用内容的空间 磁盘吞吐量:描述I/O的使用情况 网络吞吐量:描述网络的使用情况 响应时间:系统对用
大多数开发人员理所当然地以为性能优化很复杂,需要大量的经验和知识.好吧,不能说这是完全错误的.优化应用程序以获得最佳性能不是一件容易的事情.但是,这并不意味着如果你不具备这些知识,就不能做任何事情.这里有11个易于遵循的建议和最佳实践可以帮助你创建一个性能良好的应用程序. 大部分建议是针对Java的.但也有若干建议是与语言无关的,可以应用于所有应用程序和编程语言.在讨论专门针对Java的性能调优技巧之前,让我们先来看看通用技巧. 1.在你知道必要之前不要优化 这可能是最重要的性能调整技巧之一.你
大多数开发人员认为性能优化是个比较复杂的问题,需要大量的经验和知识.是的,这并不没有错.诚然,优化应用程序以获得最好的性能并不是一件容易的事情,但这并不意味着你在没有获得这些经验和知识之前就不能做任何事.下面有几个很容易遵循的建议和最佳实践能够帮你创建一个性能良好的应用程序. 这些建议中的大多数都是基于Java的,但是也不一定,也有一些是可以应用于所有的应用程序和编程语言的.在我们分享基于Java的性能调优技巧之前,让我们先讨论一下这些通用的性能调优技巧. 1.在必要之前,先不要优化 这可能是最
一. 简介 MySQL自带复制方案,带来好处有: 数据备份. 负载均衡. 分布式数据. 概念介绍: 主机(master):被复制的数据库. 从机(slave):复制主机数据的数据库. 复制步骤: (1). master记录更改的明细,存入到二进制日志(binary log). (2). master发送同步消息给slave. (3). slave收到消息后,将master的二进制日志复制到本地的中继日志(relay log). (4). slave重现中继日志中的消息,从而改变数据库的数据. 下