JVM概论
引子
Java虚拟机是Java应用程序的执行环境。通常而言,JVM是由一组严格的指令集和一个复杂的内存模型来具体实现的虚拟机,它用来解释编译好的java字节码文件,将字节码转换为特定机器可以执行的本机代码(native code)。它也可以指代某一软件运行时的进程实例。这里,我们以hotspot实现的JVM为例。
JVM的规则保证任何一款具体实现的JVM都要以完全相同的方式去解释java字节码文件,无论是一个进程,一个独立的java操作系统,抑或是一个直接执行字节码命令的处理器芯片。一般情况下,我们通常讨论的JVM是一个运行在操作系统上的进程。
JVM的架构设计使得它可以精细的控制JAVA应用程序的每一个动作,在没有权限的情况下,应用程序无法去访问本地文件系统,处理器,网络等。例如,在远程操作的情况下,代码需要有签名证书。
除了去解释java字节码,许多软件实现的JVM都有一个JIT编译器用于生成频繁执行的方法机器代码。机器代码是可以直接被cpu解析执行的,所以比字节码速度更快。
你无需去理解JVM的内部,就能编写并运行一个JAVA应用程序。但是,如果你知道了其中的一些原理,就能避免一些性能上的问题。本文以sunspot为例子来说明。
架构
JVM主要有两大子系统:
- 类加载器:负责读取java源文件,并把类加载进内存。
- 执行引擎:负责执行指令。
这里的内存是底层操作系统分配给JVM的,如下所示:
类加载器
JVM应用不同类型的类加载器构造了层次结构:
- bootstrap类加载器,它是其他类加载器的父母,负责加载核心java类库,并且是唯一的一个使用本机代码进行编写的类加载器。
- 扩展类加载器,它是bootstrap类加载器的孩子,负责加载扩展类库。
- 系统类加载器,它是扩展类加载器的的孩子, 负责从classpath中加载类文件。
- 用户自定义类加载器,它是系统类加载器或其他用户自定义类加载器的孩子。
当类加载器收到去加载一个类的请求时,会去检查cache中该类是否已经被加载,然后向其父加载器发出加载请求,如果其父加载器加载失败,那么它就自己进行加载。一个子类加载器可以检查其父类加载器的cache中是否加载了某个类,但是父类加载器无法查看子类cache中的缓存。这样设计的原因是为了防止子类加载器加载那些已经被父类加载器加载过的类。(呼,好绕口。。。)
java文件经过编译后会生成.class字节码文件,它定义了JVM中的一个类型,包括域,方法,继承信息,注解和其他元数据。我们知道,类是JVM能加载的最小程序代码单元,将一个新的类加入到当前运行中的JVM中,首先要对类文件进行加载和连接,然后将一个代表该类的Class对象交给JVM,才可以创建新的实例。
加载与连接
JVM要执行.class文件中的字节码,首先必须以字节流的方式将文件读入,然后转化为可以使用的格式加入到运行的JVM中。这两步被称为加载与连接。
加载
这个过程首先会创建一个字节数组,然后从文件系统中读取构成类文件的字节流,最后产生与所加载类对应的Class对象。这个过程中会对类做一些基本检查,加载结束后,Class对象还不完整,所以类是不可用的。
连接
加载工作完成后,类需要被连接起来,这里分为3个阶段:
- 验证:正式类文件符合预期,不会引起运行时错误或其他问题。主要包括完整性检查,常量池检查,字节码检查。
- 准备:在类文件中引用的其他类型需要全部定位到,分配内存确保该类准备就绪。此时并不会初始化变量,也不会执行任何字节码。
- 解析:解析会促使JVM检查类文件中引用到的类型是否都是已知类型,如果此时有未被加载进来的类,则会触发新的加载过程。
- 初始化:初始化静态变量,静态初始化代码块运行,类可以使用了。
连接与加载的最终产物是一个Class对象,它可以表示加载并连接起来的新类型,可以用它来创建新实例。
执行引擎
执行引擎负责执行被加载进内存的字节码指令,为了使计算机能够识别字节码,执行引擎采用了两种方式:
- 解释:执行引擎将字节码解释成机器语言。
- JIT编译:如果一个方法被频繁的执行,执行引擎会把该方法的字节码编译成本机代码存于cache中,于是,所有与该方法相关的指令都无需解释,直接执行。
尽管JIT的编译过程比普通的解释过程要耗时,但是它只需编译一次,对于那些上千次调用的方法来说,直接执行机器代码就比每次都要转换字节码再执行要划算了。
JIT编译器对于JVM而言并非是必须的组件,同时,也不是提升JVM性能的唯一手段。JVM规范只是定义了字节码与机器代码的对应关系,至于如何具体实现,就是不同版本JVM的事情了。
内存模型
JAVA内存模型是建立在内存自动管理机制之上的。当一个对象不在被应用程序引用,垃圾收集器GC就丢弃它并释放内存。这与其他编程语言需要手动释放对象的方式不同。
JVM从操作系统中申请来内存,并分割成如下几个区域:
- 堆:存放对象实例的共享区,GC会来进行扫描。
- 方法区:用于存放类加载器加载进来的字节码,静态变量,常量等等。最近,它被JVM移除,类被当做元数据加载进本机操作系统的内存。
- 栈:存放基本类型变量和类对象实例的引用,线程私有。
垃圾回收
内存自动管理是JAVA平台最重要的组成部分。一个java进程既有栈又有堆,其中,栈保存了基本类型的局部变量,以及自定义类型变量在堆中存放的地址。堆中保存了要创建的对象。java对堆内存回收和再利用的基本算法被称为标记和清除。
最简单的标记和清除算法首先会暂停所有正在运行的线程,然后堆中遍历引用树,标记出“活”的对象,遍历完成后则清除回收所有未被标记的对象。其中,“活”的对象是指在任意用户线程的栈帧中存在引用的对象。被清除的内存并不会还给OS,而是交给JVM。
JAVA对标记清除算法做了改进,采用“分代式垃圾收集”方法,因为对象的生存期或者很短或者很长,所以根据对象的生命周期将堆内存划分为不同区域,充分利用对象生命周期的特点。因此,同一个对象在其不同生命周期中,对它的引用可能指向了不同的内存区域。
将堆根据类实例的生存周期划分为不同区域使得内存管理更加有效,GC无需遍历整个堆。绝大多数对象的生命周期都很短,而那些略长一些的对象所占内存在程序结束之前不大可能被全部回收。
内存区域划分
- Eden区:Eden区中存放刚创建的对象,大多数对象都仅仅存在过这里。
- Survivor Space:这个区域通常被划分为两个区域S0和S1,从Eden中幸存下来未被清除的对象会被挪到其中一个区域中。
- tenured区:这个区域中存放从Survivor Space区域中挪过来的对象。
收集方式
对不同区域的内存回收方式是不同的,具体来讲主要分为年轻代收集和完全收集。
年轻代收集
我们将Eden区和Survivor Space称为年轻代,对这部分内存的清理与收集的过程很简单:
- 标记:当一个类对象被创建,它会被存于堆的eden池中,当Eden池满时就会触发young GC。在遍历年轻代区域的过程中,将发现的“活”对象都标记出来并挪走。
- GC遍历Eden区和Survivor Space区,标记出“活”对象并增加那些仍然存活的对象的"寿命值"(使用经历过垃圾回收次数来表示)。然后进行Eden区+有对象的Survivor区(设为S0区)垃圾回收,把存活的对象用复制算法拷贝到一个空的Survivor(S1)中,此时Eden区被清空,另外一个Survivor S0也为空。下次触发Young GC回收Eden+S1,将存活对象拷贝到S0中。新生代垃圾回收简单、粗暴、高效。(每次Young GC都会使Survivor区存活对象值+1,直到阈值)。
- 将Survivor Space区中S1足够老(被标记次数足够多)的对象挪进tenured区。
- 清除:最后,清空Eden区和S1区就可以重用了。
完全收集
当tenured区满了,年轻代收集就无法把对象放入tenured区了,这时候会触发一次完全收集。根据老年代所用的垃圾收集器,对老年代对象进行内部迁移。
发生一次 Major GC 至少伴随一次Young GC,一般比JVM在tenured区申请不到内存,会进行Full GC。tenured区使用一般采用Concurrent-Mark–Sweep策略回收内存。
当一个GC运行时,应用程序所有的进程都将停止。Young GC很频繁,但是会很快清理Eden池中的对象。而Major GC由于涉及到大量仍存活的对象,所以比Young GC慢很多。
堆内存是动态的。当堆内存满时,JVM会重新分配内存给它直到最大限度,同时也停止应用程序进程来完成内存分配。
线程
JVM是一个单进程,但是它可以并发多个线程,不同线程执行自己的方法。所有的线程共享着JVM分配到的资源。JVM进程在程序入口(main方法)新开一个线程,其余的线程都来自与此线程,并独立执行。多个线程可以并发地在不同处理器中执行,或者共享同一个处理器。
JVM概论的更多相关文章
- JVM调优总结
堆大小设置JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制:系统的可用虚拟内存限制:系统的可用物理内存限制.32位系统下,一般限制在1.5G~2G:64为操作 ...
- JVM调优总结 -Xms -Xmx -Xmn -Xss
http://blog.csdn.net/ye1992/article/details/9344807 堆大小设置JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit) ...
- JVM调优
堆大小设置JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制.32位系统 下,一般限制在1.5G~2G;64为操 ...
- JVM配置
1.堆设置 JVM中最大堆大小有三方面限制:操作系统位数(32-bt还是64-bit)限制:可用虚拟内存限制:系统的可用物理内存限制. java -Xmx3550m -Xms3550m -Xmn2g ...
- Atitit 异常机制与异常处理的原理与概论
Atitit 异常机制与异常处理的原理与概论 1. 异常vs 返回码1 1.1. 返回码模式的处理 (瀑布if 跳到失败1 1.2. 终止模式 vs 恢复模式(asp2 1.3. 异常机制的设计原理 ...
- JVM内存配置详解
前段时间在一个项目的性能测试中又发生了一次OOM(Out of swap sapce),情形和以前网店版的那次差不多,比上次更奇怪的是,此次搞了几天之后啥都没调整系统就自动好了,死活没法再重现之前的O ...
- JVM 基础知识
JVM 基础知识(GC) 2013-12-10 00:16 3190人阅读 评论(1) 收藏 举报 分类: Java(49) 目录(?)[+] 几年前写过一篇关于JVM调优的文章,前段时间拿出来看了看 ...
- jvm笔记
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M 1. 各个参数的含义什么? 参数中-vmargs的意思是设置JVM参数, ...
- 设置JVM参数,查看堆大小
1.在eclipse设置JVM参数 打开eclipse-窗口-首选项-Java-已安装的JRE(对在当前开发环境中运行的java程序皆生效,也就是在eclipse中运行的java程序)编辑当前 ...
随机推荐
- Django逻辑关系
title: Django学习笔记 subtitle: 1. Django逻辑关系 date: 2018-12-14 10:17:28 --- Django逻辑关系 本文档主要基于Django2.2官 ...
- [Java]链表的打印,反转与删除
class Node{ public int value; public Node next=null; public Node(int value) { this.value=value; } }p ...
- ORACLE 查询不走索引的原因分析,解决办法通过强制索引或动态执行SQL语句提高查询速度
(一)索引失效的原因分析: <>或者单独的>,<,(有时会用到,有时不会) 有时间范围查询:oracle 时间条件值范围越大就不走索引 like "%_" ...
- 【剑指Offer】39、平衡二叉树
题目描述: 输入一棵二叉树,判断该二叉树是否是平衡二叉树.这里的定义是:如果某二叉树中任意结点的左.右子树的深度相差不超过1,那么它就是一棵平衡二叉树. 解题思路: 首先对于本题我们要 ...
- 【剑指Offer】31、从1到n整数中1出现的次数
题目描述: 求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1.10.11.12.13因此共出现6次,但是对于后面问题他 ...
- code runner运行终端的目录设置
我的github:swarz,欢迎给老弟我++星星 该设置属性为 "code-runner.fileDirectoryAsCwd": true 设置为 true后,终端默认目录为运 ...
- Monkey日志中如何找错误
无响应问题可以在日志中搜索 “ANR” ,崩溃问题搜索 “CRASH” ,内存泄露问题搜索"GC"(需进一步分析),异常问题搜索 “Exception” monkey执行时未 ...
- vue 注册全局组件
注册全局组件有啥好处呢? 提高代码的复用性:哪里需要写哪里,贼方便,就写一个标签:减少代码量:可以再配合slot一起使用,咦~~,舒服 为了让整个项目的可读性,我创建一个文件统一存放全局组件 1.创建 ...
- Orcale用户管理
类 ------表 对象----行 属性----列 软件开发流程: 需求调研 需求分析 概要分析 详细分析 编码 测试 上线 维护 论坛: 1.注册和登录 2.发帖,回帖(关注,浏览数) 用户:(昵称 ...
- 洛谷 P2805 BZOJ 1565 植物大战僵尸
题目描述 Plants vs. Zombies(PVZ)是最近十分风靡的一款小游戏.Plants(植物)和Zombies(僵尸)是游戏的主角,其中Plants防守,而Zombies进攻.该款游戏包含多 ...