了解JVM,不只是在面试时增长概率,更是在开发过程进行优化调整时了解内存溢出等,所以须要知道这个是什么东东,有什么卵用?html
朋友,看20分钟的呗,了解下?(有木有发传单的赶脚?嘻嘻~~)java
正好前段时间看了《深刻理解Java虚拟机:JVM高级特性与最佳实践(第2版)》这本书,并且在腾讯课堂听了某位大大的讲解,因此本篇记录下,一是本身了解并笔记,二能帮助他人就更好了。(就是下面这本书...看的本身有点怀疑人生...)面试
java发展? 算法
学习java的朋友都清楚java的特性,”write Once,Run Anywhere”,而支持其此特性最大的一点就是java虚拟机(JVM)。编程
1. 从1996年 Sun 公司发布JDK1.0时,其中内置的虚拟机是Classic VM,它是第一款商用的虚拟机,这款虚拟机只能使用纯解释器方式来执行java代码,每一方法,每一行代码都必须进行编译,编译耗时很是高,这就形成了业界对Java语言运行很慢的印象。可是其可使用外挂JIT编译器(Just In Time),但此时的即时编译与纯解释编译不能够同时工做。缓存
2. 1998年,JDK1.2版本中的虚拟机是Exact VM,通过java团队的优化,采用精准式的内存管理,其执行系统已经具有现代高性能虚拟机的雏形,如:两级即时编译器、编译器与解释器混合工做模式等。安全
PS:《深刻理解java虚拟机》一书中说起,JDK1.2中曾并存过三种虚拟机,Classic VM、Exact VM、Hotspot VM数据结构
Classic VM只能之外挂的形式使用JIT编译器,Exact VM、Hotspot VM内置了JIT编译器。多线程
3. 1999年,Hotspot VM发布,实质上该虚拟机是由名为“Longview Technologies”的小公司设计的,因为其优异表现于1997年被Sun公司收购,Hotspot VM发布时是做为JDK1.2的附加程序提供的,但其后来成为了JDK1.3及之后的其余版本的默认虚拟机。ide
PS:HotSpot VM如其名称,热点代码探测能力,一开始就是准确式GC(Garbar Collection)。
若是一个方法被频繁调用,或方法中有效循环次数不少,将会分别触发标准编译和OSR(栈上替换)编译动做。经过编译器与解释器恰当地协同工做,能够在最优化的程序响应时间与最佳执行性能中取得平衡,并且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减少,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
4. 自JDK 1.3始,Sun公司维持一个习惯,每隔两年发布一个JDK版本,以动物命名,期间发布的修正版本则以昆虫为工程名称。
5. 大事发生了!!!2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。Oracle公司宣布在不久的未来(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工做,使之优点互补。整合的方式大体上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务,使用HotSpot的JIT编译器与混合的运行时系统。
6. 2014年,Oracle发布了JDK1.8,此版本是JDK近几年的稳定版本,也是自JDK1.5以来改动最大的一个版本,各公司一直在用该版本。
7. 2017年、2018年分别发布了Java九、Java10,这两个版本属短时间版本,本博主已处在跟不上的阶段了,并且上个版本还没彻底熟悉...惆怅...
java虚拟机发展?
能够参考本篇:https://201610042321.iteye.com/blog/2337901
1.Sun 公司 --- Classic VM、Exact VM、Hotpost VM、KVM、Squawk VM、Maxine VM...
2.BEA公司 --- JRockit VM
3.IBM公司 --- J9 VM、K8 VM
4.还有Apache Harmoney、Google Android Dalvik VM、Microsoft JVM等等好多的虚拟机...
JVM是什么?
如上图:
JDK:Java Development Kit,是整个java的核心,包含了JRE,Java开发工具,Java编译器等;
JRE:Java Runtime Enviromennt,运行Java程序的必须环境,包含JVM与java程序须要的核心类库;
JVM:Java Virtual Machine,是Java实现跨平台的最核心部分,将字节码文件转换为系统机器指令,并解释执行,JVM是运行在操做系统之上的,没有直接与硬件交互。
Java源程序编译为字节码文件?
平常编程的文件都是 *.java文件,而在JVM中运行的文件则须要是 *.class文件,这其中的过程是如何进行编译的呢?
其中具体的编译过程能够阅读【龙书】《编译原理》,这本书里面讲解了程序是如何进行编译的,是很是牛掰的一本著做。
亦可参考这两篇:https://www.jianshu.com/p/b9d3f20a880e
http://m.elecfans.com/article/668346.html
https://blog.csdn.net/wy727764020/article/details/80411751
编译是将一种抽象程度更高的语言转换为更贴近计算机可以识别的语言的过程。
Java属高级语言,一是其更贴近人类语言,更易理解(语法);二是平台无关性,无需兼容不一样系统(跨平台);三是无需管理内存,更关注业务实现(内存管理)。
一般命令行窗口在对应的目录下执行javac *.java命令,编译成功会在当前目录下生成对应的class字节码文件,执行java *获得执行后结果。
java代码执行过程:Java源程序通过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,而后在特定的机器上运行。
通用代码编译器的步骤:
Java代码编译器的步骤:
实质上,JVM在JIT编译阶段就作了相关的优化,如逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除等等。
有兴趣的朋友能够搜索一下,顺便了解一下此处相关的具体优化点。
JVM加载class文件?
加载,实质上是将字节码(类的二进制字节流)文件由磁盘读取至虚拟机内存中,再经过类加载器完成,整个过程包含了7个阶段:
具体过程是按以上顺序开始的,但不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段。
装载
该阶段须要完成如下3步操做:
1.经过ClassLoader在classPath中获取*.class文件,将其以二进制流形式读入JVM内存中;
2.将此字节流表明的静态存储结构转换为方法区中的运行时数据结构;
3.在(堆)内存中,生成表明该类的Class对象,做为对方法区中的数据访问入口。
装载阶段是可控性最强的阶段,开发人员可使用系统的类加载器,也可以使用自定义的类加载器来完成此阶段(继承ClassLoader
)。
类加载器分为3大类:
1.启动类加载器:Bootstrap ClassLoader,是虚拟机的一部分,负责加载JDK/jre/lib下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是没法被Java程序直接引用的。
2.扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器。
3.应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
这三种类加载器的关系以下图,这几者之间的关系并不是继承,而是组合的关系。
关于加载时,JVM的加载机制有3种:
1.全盘负责:加载某一class时,会将其相关的其余class也由该类加载器负责加载;
2.双亲委派:先告知其父类,父类再告知其祖类,直至最顶层类加载器,最顶层加载成功,则返回,不然依次向下进行加载,若都没有,则抛ClassNotFoundError;
3.缓存机制:宝成全部加载过的class都被缓存,当程序中使用某个class时,类加载器会先从缓存中寻找该class,若不存在,则读取该类二进制文件,转换为class对象后,再次放入缓存区,若修改了class,则不需重启JVM,修改才会再次生效。
java虚拟机采用的加载机制是 - 双亲委派模型。
采用该机制的优势是 - 采用双亲委派模式的是好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载。
提问:按照上述解释,JVM明明是父类委派或者是单亲委派,但为何一直称为’双亲委派’呢???
验证
主要是确保class文件的字节流符合JVM规范,且不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
对虚拟机自身安全的一种保证机制,保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。
准备
为类的静态变量分配内存,并设置默认初始值,这些内存都将在方法区中存在。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程, 解析动做主要针对类或接口、字段、类方法、接口方法四类符号引用进行。
符号引用:以一组符号来描述所引用的目标。符号引用能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可,符号引用和虚拟机的布局无关。
直接引用:能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄,直接引用和虚拟机的布局是相关的,若是有了直接引用,那么直接引用的目标必定被加载到了内存中。
初始化
真正开始执行类中定义的Java程序代码,主要是根据程序中的赋值语句主动为类变量赋指定值。
执行
程序中方法之间的相互调用。
卸载
销毁一个对象,通常状况下中有JVM垃圾回收器完成。代码层面的销毁只是将引用置为null。
整个类加载过程当中,除了在装载阶段,可以使用自定义的类加载器参与外,其他阶段所有都由虚拟机主导控制,在初始化阶段才真正执行字节码文件,
若你能看到此处,那接下来的东西才是真正的JVM的区域划分介绍。 -.-
JVM运行时数据区域划分?
在介绍JVM数据区域划分时,先简单的运行一个简单地代码,便于分析java文件 -> class文件 在 JVM之中的存在及执行关系。
先用一个test来查看一下源文件编译后的class文件之间的对应关系吧,以下图:这种的二进制文件,反正本人(想起来不良人中的本人了)是看的费劲,能理解一点点的一点点...
将左边的 Test.java 进行 javac Test.java 编译后,生成右边的 Test.class 字节码文件,若是须要进行查看,须要使用 javap -v Test.class 反解析字节码文件,生成可看懂的文件。
关于Javap命令反编译后的文件具体信息能够参考:https://www.jianshu.com/p/6a8997560b05
当反编译以后的字节码文件,能够参考JVM字节码指令进行查询相应的跳转处理逻辑,其中关于锁,i++ / ++i 等等操做原理均可以剖析下,上面的反编译代码至截取了当中的 main() 和 addAndTest() 这两个方法,其余的类元信息、主次版本号、常量信息,属性信息等都没有截取,有兴趣的朋友能够本身试一下,参照JVM指令查看一下反编译后的文件。
你们看到反编译后的文件中有bipush、astore_一、aload_1等等指令,这些都是在JVM中进行操做的,该代码是引出接下来的JVM模型的....
看到这儿的朋友,10分钟应该是过去了,也就是喝杯水休息的功夫,不过注意了,由于接下来就要变形了,呃,不对,是真正的JVM模型的讲解了。
先看一张JVM的自己内存结构图,以下:
(画了好一下子呐)能够参考下这篇博文,讲解的仍是很是清楚的:https://www.nowcoder.com/discuss/151138?type=1
线程计数器:
程序计数器是一块较小的内存空间,可看做当前线程正在执行的字节码的行号指示器。若当前线程正在执行的是Java方法,计数器记录的就是当前线程正在执行的字节码指令的地址(行号);如果本地Native方法,那么程序计数器值为undefined。
程序计数器有两个做用:字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理;在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被来回切换时清楚该线程运行位置,即线程切换后能恢复到正确的执行位置。
线程私有。每条线程都有一个独立的程序计数器,并且是JVM中惟一一个不会出现OOM的内存区域。生命周期随着线程的建立而建立,随着线程的结束而死亡。
思考:线程计数器指的是当前线程的字节码行号指示器,那为何能够根据它来选取下一条须要执行的字节码指令?
答:记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。在每次指令执行后自增, 维护下一个将要执行指令的地址,若当前为最后的行号,则其会根据最后的return语句进入下一个要执行的线程位置,再获取当前线程此位置的行号。
Java 栈:
虚拟机栈描述的是Java方法执行的内存区域,也是线程私有的: 每一个方法被执行时会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。
栈中的元素用于支持虚拟机进行方法调用,每一个方法从开始调用到执行完成的过程,就是栈帧在虚拟机栈中从入栈到出栈的过程。
在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧,在执行引擎运行时,全部指令都只能针对当前栈帧进行操做;正在执行的方法称为当前方法;栈帧是方法运行的基本结构,其压栈弹栈方式是FILO(First In Last Out)。
关于运行时的栈帧结构,能够参考一下这篇博文:http://www.javashuo.com/article/p-wmhvmenl-du.html
Java虚拟机栈会出现两种异常:
StackOverFlowError:若Java虚拟机栈的内存大小不容许动态扩展,那么当线程请求的栈深度大于虚拟机容许的最大深度时(但内存空间可能还有不少),就抛出此异常;
OutOfMemoryError:若Java虚拟机栈的内存大小容许动态扩展,且当线程请求栈时内存用完了,没法再动态扩展了,此时抛出OutOfMemoryError异常;
Java虚拟机栈也是线程私有的,每一个线程都有各自的Java虚拟机栈,并且随着线程的建立而建立,随着线程的死亡而死亡。
本地方法栈:
本地方法栈与虚拟机栈所发挥的做用是很是类似的,其区别不过是Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并无强制规定,所以具体的虚拟机能够自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈同样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
该区域也是线程私有的,对JVM而言,java虚拟机栈“主内”,而本地方法栈“主外”,本地方法能够经过JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至能够调用寄存器,具备和JVM相同的能力和权限,当大量本地方法出现时,势必会削弱JVM对系统的控制力,大量使用其余语言来实现JNI,就会丧失跨平台特性,威胁到程序运行的稳定性,由于它的出错信息都比较黑盒。
java 堆:
Java虚拟机所管理的内存中最大且被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,因为如今收集器基本都采用分代收集算法,因此Java堆还能够细分为:新生代和老年代;新生代又能够分为:Eden 空间、From Survivor空间、To Survivor空间。若是从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的便可,就像咱们的磁盘空间同样。在实现时,既能够实现成固定大小的,也能够是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(经过-Xms和-Xmx控制)。若是在堆中没有内存完成实例的分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError异常。
此处关于堆的空间就少介绍一点吧,等到后面再开一篇博客详细讲解关于堆中的具体信息,本篇主要关注的是堆中的东西。也就是对象在堆中是如何存在的?
那对象是如何建立的呢?以下图:
在HotspotVM中,对象在内存中存储的布局能够分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1.对象头
第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称之为MarkWord;
另一部分是类型指针,即对象指向它的类元数据的指针, 虚拟机经过这个指针来肯定这个对象是哪一个类的实例实例数据。
2.实例数据
对象真正存储的有效信息,也是在程序代码中所定义的各类 类型的字段内容。 不管是从父类继承下来的,仍是在子类中 定义的,都须要记录起来对齐填充。
3.对齐填充
非必要存在, 仅起到占位符的做用, 缘由是HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍, 即对象的大小必须是8字节的整数倍.
对象的访问定位:
JVM 规范中并无详细规定引用类型的实现细节,好比引用应该经过何种方式去定位、访问堆中的对象,具体的对象访问方式取决于虚拟机的具体实现,好比 HotSpot 有其本身的实现方案。目前主流的访问方式有使用句柄和直接指针两种,以下图:
方法区:非堆 / 永久代
方法区是堆空间的一个外延部分,但并不是属于堆,所以还可称其为 ' Non-Heap(非堆) ',方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区中的信息通常须要长期存在,并且它又是堆的逻辑分区,所以用堆的划分方法,咱们把方法区称为‘ 永久代 (Permanent Generation)’。这部分空间大小可扩展,其中数据是线程共享的。
HotSpot VM虚拟机把GC分代收集扩展至方法区, 即便用Java堆的永久代来实现方法区,这样HotSpot的垃圾收集器就能够像管理Java堆同样管理这部份内存, 而没必要为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 所以收益通常很小)。只有Hotspot才有Perm区(永久代),它在启动时固定大小,很难进行调优,而且Full GC时会移动类元信息。而对于其余虚拟机(如BEA JRockit、IBM J9 等)来讲是不存在永久代的概念的。
运行时常量池:
运行时常量池是方法区的一部分。其中方法区中的常量存储在运行时常量池中。
常量池( Constant pool table)中存放编译时期产生的各类字面量和符号引用,.class文件中的常量池中的全部的内容在类被加载后存放到方法区的运行时常量池中。运行时常量池相对于class文件常量池的另一个特性是具有动态性,java语言并不要求常量必定只有编译器才产生,也就是并不是预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。如String类中的intern()方法就是采用了运行时常量池的动态性。当调用 intern 方法时,若是池已经包含一个等于此 String 对象的字符串,则返回池中的字符串。不然,将此 String 对象添加到池中,并返回此 String 对象的引用,就是在运行期间向常量池中添加字符串常量。
在近三个JDK版本(六、七、8)中, 运行时常量池的所处区域一直在不断的变化,在JDK6时它是方法区的一部分,7又把他放到了堆内存中,8以后出现了元空间,它又回到了元空间。
运行时常量池是方法区的一部分,因此会受到方法区内存的限制,所以当常量池没法再申请到内存时就会抛出OutOfMemoryError异常。当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就须要垃圾收集器回收。
元空间:JDK1.8以后代替方法区的区域。
在JDK8以后,永久代被移除,本来存储在永久代的数据将存放在一个叫作元空间(Metaspace)的本地内存区域。
在Java虚拟机(如下简称JVM)中,类包含其对应的元数据,好比类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已肯定的符号引用和虚方法表。在过去(当自定义类加载器使用不广泛的时候),类几乎是“静态的”而且不多被卸载和回收,所以类也能够被当作“永久的”。另外因为类做为JVM实现的一部分,它们不禁程序来建立,由于它们也被认为是“非堆”的内存。
若是在JDK8以前碰到java.lang.OutOfMemoryError: PermGenspace
,为解决该问题,须要设置永久代的运行参数:-XX:MaxPermSize= l280m。
若将此部署到新机器上,也须要再次调参数信息,因此在JDK8中使用元空间替换永久代。区别于永久代,元空间在本地内存中分配,即:只要本地内存足够,不会出现像永久代中
PermGenspace
。一样的,对永久代的设置参数PermSize和MaxPermSize也会失效。
默认状况下,“元空间”的大小能够动态调整,或者使用新参数MaxMetaspaceSize来限制本地内存分配给类元数据的大小。
在JDK8里,Perm 区全部内容中,字符串常量移至堆内存,其余内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间。
元空间的特色:
此区域没有GC扫描,Full GC时,指向元数据指针都不用再扫描(CMS不会扫描元空间),减小了Full GC的时间,同时减小了内存碎片;
元空间的对象不会进行转移,也不进行压缩,减小数据转移压缩的开销;
除非某个类加载器死亡,不然不会对此区域进行回收;
JVM关闭?
正常状况下,当随后的非守护线程结束,或调用退出指令(如System.exit(); )JVM正常关闭;
若系统运行遇到异常信息,抛出RuntimeException时,也会退出,属于异常关闭;
还有强制执行kill命令,杀死进程,或者是调用Runtime.halt(),这种属于强制关闭。
JVM如何调优?
啥?你问为何须要JVM优化?这我TM...唉...
确定是程序出问题了呀,否则还能怎么呐?如程序运行卡停、无反应、CPU负载忽然太高... ...
一般进行JVM调优时,都会先清楚是哪部分代码的问题,这是就须要使用命令或者使用工具来进行排查了。
命令行工具,这个没有具体使用过;可使用Jconsole、JVisualVM等来查看java线程执行(如热点代码分析、内存泄漏检查等),此处就不介绍了...嘿嘿,由于工具嘛,使用也不难...
JVM相关调优连接:https://pengjiaheng.iteye.com/blog/552456
http://baijiahao.baidu.com/s?id=1601240621385314413&wfr=spider&for=pc
具体如何调整呢?
最终的结果是使用现有的硬件消耗承载最大的系统请求吞吐量。
主要是达到两个目的:一使GC次数减小,二使GC时间减小。与其说减小,不如说过度的少。不过这二者之间的关系是相互矛盾的,所以须要在二者之间进行性能权衡,固然也能够根据不一样的GC算法来选择不一样的GC收集器。具体调整是根据当前线程的执行状况来决定。
本节中有部分未完善,会在后续博文中更新...烦请稍做等待...
(愿你的每一行代码,都有让世界进步的力量 ------ fn)