JVM深入理解
JVM深入理解
一.JVM介绍
JVM应用百度百科的原话是:
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JAVA语言的一个非常重要的特点就是与平台的无关性。
而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了
与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够
"一次编译,到处运行"的原因。
这句话能够很清楚的明白一件事情:之所以JAVA能够流行起来,JVM是一个非常关键的因数.所以学习JVM的原理和运转流程是很有必要的,对优化JAVA性能也是非常有必要的一个基础条件.
二.运行时区域简单介绍
JAVA虚拟机所管理的内存将会包含了有以下几个运行时数据区域,下图可看:

1.程序计数器
是记录当前线程所执行器的行号指示器.字节码解释器就是改变计数器的值来选取吓一条字节码指令来执行的.因为程序计数器是在私有区域,对于线程来说,每个线程都将拥有一个程序计数器.各个线程计数器都是互不影响,独立存储的.
程序计数器主要记录的是虚拟机的字节码地址,如果当前执行本地方法,那么程序计数器的值为空.
2.JAVA虚拟机栈
虚拟机栈也是线程私有的,是为了描述java方法执行的内存模型:但方法执行时,虚拟机栈会创建一个栈帧,是用来存储局部变量表,操作数栈,动态链接,和方法的返回地址等信息的.方法在执行的过程,就是一个栈帧的入栈到出栈的过程.
1.局部变量表
是存储8大基本类型(boolean,int,byte,long,double,short,float,char),和一个叫做对象引用的reference(只是一个存储的对象指针,指向此对象的位置).
3.本地方法栈
类似于虚拟机栈的功能,但是本地方法栈是虚拟机是使用到了NATIVE方法.
4.JAVA堆
java堆是管理内存的最大区域,这一区域是线程共享的,几乎所有的对象创建后都会保存在堆里面,并且堆也是垃圾回收的重点关注对象,因为垃圾回收对java对象回收的收益是非常高的,在后面我会详细讲解垃圾回收机制有关的知识.
java堆按照年龄段分为:新生代和老年代.
5.方法区
方法区也是线程共享的,它主要存放关于类的信息,常量,静态变量和编译后的代码等.方法区也有人称其为'永久代',因为垃圾回收虽然也会回收方法区的数据,但是回收的效益很低,几乎不会被回收,所以称为'永久代'.
运行时常量池:用于存放通过编译期编译过后的的字面量和符号引用,相当于存放数据结构的的地方.对于class文件,java虚拟机都对其每一个部分有着非常严格的说明与限制.不管是什么语言,在java虚拟机编译后都会统一
编译成class文件,并且在虚拟机上运行.
6.对象的创建与定位
对于java语言,大家都知道是一门对象语言,所以对java来说,对象的创建是非常平常的,让我们来一探究竟.
对代码而言,可能创建对象仅仅就是new这样一个关键字而已,但是java底层一定是做了很多操作的:
1.当虚拟机检查到new关键字时,会检查常量池是否有这个类的符号引用.
2.如果有这个类的符号引用,会检查这个类是否被加载,解析和初始化了,就是常常人们说的'类加载'.
3.如果没有,就会去类加载,如果已经加载了,jvm就会为新对象分配内存空间.新对象的内存大小会在类加载的时候就确定.
4.分配完对象的内存大小,会对这个类进行一些初始化设置,比如:元数据信息,对象的hash码,GC信息等,这些都会被分配到对象的对象头里面.下图为对象的对象头信息图:

三.垃圾收集
1.判断对象是否已经死去
在垃圾收集器,在收集时,会对这个对象判断是否已经不用了,不用了就代表已经死了,需要进行垃圾收集了.所以垃圾收集器时怎么判定对象是否已经死了呢?
有两种方式:计数算法和可达性算法
计数算法:相当于给每一个对象的引用都加1,当引用失效的时候就减1,然后判断此对象的计数器是否为0,如果是就说明此对象已经没有引用了,需要垃圾收集了.
虽然这个方式简单,但是有一个问题,如果对象之间存在相互调用的情况,那么它们的计数器都不为0,导致无法收集.
可达性算法:
基本思路时一个链路式的判定方法,把'GC ROOTS'作为根节点,从根节点往下寻找,搜索这个对象的引用链路,当这个对象没法达到GC Roots的话,说明此
对象不可达了,需要垃圾收集.
2.垃圾收集算法
到目前为此,垃圾收集算法有非常多种,但是我也不能说哪一种时完美的,没有完美的垃圾收集器,只有合适的垃圾收集器.所以,在不同场合,需要选择不同的垃圾收集器
来完成运行中的垃圾收集任务.但是底层的算法大概能分为以下几种:
1.标记-清除算法
看名字就应该知道,此算法有2个阶段,'标记'和'清除'.当发现对象已经死了,就标记,到时候垃圾收集器就会统一'清除'对象.

2.复制算法
复制算法是为了解决效率问题产生的。大多数场景都可以定义为:Survivor和Eden,比例为2:8,而Survivor又可以分为:from Survivor和to Survivor,比例为:1:1.
对于新生代的对象来说,基本90%以上的对象都是‘朝生夕死’的,所以当回收时,就将from Survivor和Eden区还存活的对象复制到to Survivor,其余的空间全部
清除。这样就可以提升性能。
3.标记-整理算法
使用复制算法效率高,但是会浪费一部分的空间,如果不想浪费空间,就可以采用标记-整理算法。它和‘标记-清除’算法很类似,但是,当标记完需要回收的对象时,
‘标记-整理’算法会把存活下来的对象整体向一个方向移动,就可以直接清除掉需要回收的对象。

3.垃圾收集器
垃圾收集器,就是根据各种垃圾回收算法,运用到不同的场合的一种实现。大致的垃圾收集器可以用如下图表示:

每一种垃圾收集器都会运用在不同的场合,或者多个垃圾收集器共用,使得新生代和老年代都可以高效率的运行,目前最前沿的垃圾收集器是G1收集器。
号称最低延迟的'stop the world'。关于每一种垃圾收集器详细的意义,可以自行在网上查阅资料,在这里我就不过多说明了。
四.类文件结构
类文件就是通过编译器编译过后,生成的.class文件,之所以java语言是跨平台的,其实关键就是在类文件class身上。
1.类文件结构
class文件其实就是一组由8位字节为基本单元的二进制流,文件的排序必须严格按照规定顺序排列在class文件中,并且没有任何空隙。class文件的结构大概就是:
2种:无符号数和表。
无符号数:是一种基本数据结构,以:u1,u2,u4,u8分别代表1字节,2字节,4字节和8字节的无符号数。无符号数可以用来描述数字,数量值,字符串等。
表:就是由多个无符号数组合而成的一种复合结构。其实整个class文件就是一张表罢了。
1.魔数
class文件的前4个字节被称为“魔数”,它被当做唯一确定文件是否是class文件的标识。魔术的值为:0xCAFFEBABE.
2.版本号
第5,6和第7,8都是版本号,第5,6个字符是此版本号,第7,8个字符是主版本号。
3.常量池
它是整个class文件中和其他项目关联最多的结构了。也是最大的一块结构。它是存储class文件中的资源仓库。常量池的常量并不是固定的,所以在常量池的入口会放置一个u2的
数据类型,代表当前常量池的计量值。常量池主要存放2大类常量:字面量和符号引用。
字面量:文本符号,final的常量值等
符号引用:类,接口的全限定名,字段名,方法名
常量池的基本类型有如下几十种:

所以说常量池是最繁琐的结构,数据,每一种的结构数据都不一样。
2.字节码指令
字节码指令都是由一个字节长度以及后面跟随的多个操作参数组成的。字节码的操作是通过在虚拟机栈的操作数栈实现的。
由于一开始就已经限制了字节码指令长度为一个字节,所以所有的指令就不能超过256个。我这里就不罗列字节码的指令了。
五.类加载机制
上一章我们讲了class文件结构,但是jvm是怎么加载类文件的呢?下面我们来一起探讨下这个问题
1.类加载时机
类加载的整个生命周期大致分为下列7个阶段:

在什么时候会触发第一个阶段‘加载’呢?有如下5种情况:
1.遇到new,getstatic,putstatic,invokestatic字节码时,如果没有类加载,那么就需要进行类加载操作。
2.使用refect反射时,如果类没有初始化。
3.当初始化一个类,发现其父类没有初始化时。
4.当虚拟机启动,执行的主类没有初始化时。
5.当jdk为1.7以上,如果MethodHandle的解析结果为REF_getStatic,REF_putstatic,REF_invokestatic的类没有初始化时。
2.类加载过程
1.加载
这个阶段需要完成:
1.通过全限定名获取类的二进制流。
2.将这个流所代表的静态结构转换为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.class对象。作为这个类的访问入口。
2.验证
验证当前字节流中的信息是否符合虚拟机的要求,包含:
1.文件格式:
1.是否是以‘魔数’开头。
2.主,副版本号是否符合当前虚拟机。
3.常量池中的常量是否有不被支持的类型。
4.其他等等要求。
2.元数据验证:
1.验证当前类的父类,类的字段等信息是否符合规范
3.字节码验证:
对每一个字节码都进行验证,检查字节码是否符合规范,逻辑。
4.符号引用:
对常量池中的符号引用做验证。
3.准备
为类的变量分配初始值的阶段。但是这个类的变量限于:被static修饰的变量,并且当前赋值和java代码没哟关系,仅仅是赋变量的初始值,比如int类型的初始值为0.
4.解析
就是把常量池的符号引用解析为直接引用的过程。
5.初始化
这一阶段,才开始定义java代码。在准备阶段已经赋了一次初始值,这个阶段是第二次赋值。它会把java代码的初始值给赋上。
3.类加载器
类加载器顾名思义就是用来加载类的。比较两个类是否相等,其实最底层是比较他们是否为同一个加载器加载的。类加载器有一个很重要的词语:双亲委派。
类加载器分大类就2类:启动类加载器(C语言实现),其他类加载器(java语言实现)。下图为加载器的继承图:

这个模式就是双亲委派模型。意思是:当一个类加载器收到类加载的信息时,一般是先委派给父类完成,一般就会传递到顶层加载,当父类无法加载时,子加载器才会自己尝试加载。
双亲委派对于java的稳定来说至关重要。
这次基本就说到这,强烈建议大家去看<<深入理解Java虚拟机:JVM高级特性与最佳实践>>这本书。
JVM深入理解的更多相关文章
- JVM深入理解<二>
以下内容来自: http://www.jianshu.com/p/ac7760655d9d JVM相关知识详解 一.Java虚拟机指令集 Java虚拟机指令由一个字节长度的.代表某种特定含义的操作码( ...
- java之jvm学习笔记二(类装载器的体系结构)
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...
- JVM的理解
1.Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分.在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程: 也相当与 注:JVM(ja ...
- JVM如何理解Java泛型类(转)
一个很典型的泛型(generic)代码.T是类型变量,可以是任何引用类型: public class Pair<T>{ private T first=null; private T se ...
- JVM如何理解Java泛型类
//泛型代码 public class Pair<T>{ private T first=null; private T second=null; public Pair(T fir,T ...
- 谈谈对JVM的理解
JVM可谓是学习JAVA基础中的基础了,但仍有不少同学对JVM概念还是比较模糊,甚至没有听说过,对java的理解也只是在基础语法 层面,本文就将对JVM进行初步介绍,因篇幅所限,只能介 ...
- JVM 系列(二)内存模型
02 JVM 系列(二)内存模型 一.JVM 内存区域 JVM 会将 Java 进程所管理的内存划分为若干不同的数据区域.这些区域有各自的用途.创建/销毁时间: 一. 线程私有区域 线程私有数据区域生 ...
- [转帖]JVM—深入理解内存模型与垃圾收集机制
JVM—深入理解内存模型与垃圾收集机制 https://juejin.im/post/5d68dc9ee51d4561ad6548f7 前言 Java是一种跨平台的语言,当初其设计初衷也是为了解决各个 ...
- Java 中级 学习笔记 1 JVM的理解以及新生代GC处理流程
写在最前 从毕业到现在已经过去了差不多一年的时间,工作还算顺利,但总是离不开CRUD ,我觉得这样下去肯定是不行的,温水煮青蛙,势必有一天,会昏昏沉沉的迷失在温水里.所以,需要将之前学习JAVA 当中 ...
随机推荐
- linux日志logger命令详解
通过logger命令记录日志 logger是一个shell命令接口,可以通过该接口使用Syslog的系统日志模块,还可以从命令行直接向系统日志文件写入一行信息. ------------------- ...
- Linux下Oracle开机启动
参考:http://blog.csdn.net/huangyanlong/article/details/36942155 一.保证dbstart能用:vi $ORACLE_HOME/bin/dbst ...
- C语言博客作业4--数组
C语言博客作业4--数组 1.本章学习总结 1.1思维导图 请以思维导图总结本周的学习内容,如下图所示: 1.2本章学习体会及代码量学习体会 1.2.1学习体会 描述本周学习感受,也可以在这里提出你不 ...
- mysql 多行(GROUP_CONCAT)和多列(CONCAT)的合并函数
1,多行合并:把查询的一行或者多行进行合并. SELECT GROUP_CONCAT(md.data1) FROM DATA md,contacts cc WHERE md.conskey=cc.id ...
- spring boot apollo demo
controller 监听器,监听配置实时变化 src/main/resources---->META-INF---->app.properties apollo 界面 测试访问 : 实时 ...
- matlab-调用摄像头人脸识别
----------------------------边学边写边学习------------------------------------- 版本:2014a 调用摄像头 a = imaqhwin ...
- MySQL(索引)
索引 索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构.类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取即可. MySQL中常见索引有: 普通索引 唯一索引 ...
- MySQL(Python+ORM)
本篇对于Python操作MySQL主要使用两种方式: 原生模块 pymsql ORM框架 SQLAchemy pymsql pymsql是Python中操作MySQL的模块,其使用方法和MySQLdb ...
- makefile中打印变量名字,方便调试
$(warning $(DVD_SERVICE)) // DVD_SerVICE是Makefile中的变量 $(warning ST40_IMPORTS is $(ST40_IMPORTS)) 变 ...
- rownum用法
对于rownum来说它是oracle系统顺序分配为从查询返回的行的编号,返回的第一行分配的是1,第二行是2,依此类推,这个伪字段可以用于限制查询返回的总行数,且rownum不能以任何表的名称作为前缀. ...