自己建网站卖鞋,网页版游戏大全,内江企业网站建设公司,广州今天新闻一、简介
1.1 概述
JVM是Java Virtual Machine#xff08;Java虚拟机#xff09;的缩写#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关…一、简介
1.1 概述
JVM是Java Virtual MachineJava虚拟机的缩写是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息使得Java程序只需要生成在Java虚拟机上运行的目标代码字节码就可在多种平台上不加修改的运行这也是Java能够“一次编译到处运行的”原因。
所谓java能实现跨平台是由在不同平台上运行不同的虚拟机决定的因此java文件的执行不直接在操作系统上执行而是通过jvm虚拟机执行我们可以从这张图看到JVM并没有直接与硬件打交道而是与操作系统交互用以执行java程序。 1.2 JRE、JDK和JVM的关系
JREJava Runtime Environment Java运行环境是Java平台所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。
JDKJava Development KitJava开发工具包是用来编译、调试Java程序的开发工具包。包括Java工具javac/java/jdb等和Java基础的类库java API 。
JVMJava Virtual Machine Java虚拟机是JRE的一部分。JVM主要工作是解释自己的指令集即字节码并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的不同的操作系统会有不同的JVM映射规则使之与操作系统无关完成跨平台性。 使用JDK调用JAVA API开发JAVA程序后通过JDK中的编译程序javac将Java程序编译为Java字节码在JRE上运行这些字节码JVM会解析并映射到真实操作系统的CPU指令集和OS的系统调用。 二、JVM架构
JVM的架构分为多个子系统主要包括类加载子系统、运行时数据区、垃圾回收机制和执行引擎等。 2.1 类加载子系统
JVM 将 class 字节码文件加载到内存中 并将这些静态数据转换成方法区中的运行时数据结构在堆并不一定在堆中HotSpot在方法区中中生成一个代表这个类的 java.lang.Class 对象作为方法区类数据的访问入口。 2.1.1 类加载器
把类加载阶段的 “ 通过一个类的全限定名来获取描述此类的二进制字节流 ” 这个动作交给虚拟机之外的类加载器来完成。这样的好处在于我们可以自行实现类加载器来加载其他格式的类只要是二进制字节流就行这就大大增强了加载器灵活性。系统自带的类加载器分为三种
启动类加载器 》 Bootstrap ClassLoader扩展类加载器 》 Extension ClassLoader应用程序类加载器 》Application ClassLoader 2.1.1.1 双亲委派机制
如果一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父加载器去完成每个层次的类加载器都是如此。因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时子加载器才会尝试自己去加载。
层级结构如下图所示 双亲委派模型的优点 java类随着它的加载器一起具备了一种带有优先级的层次关系。 例如类 java.lang.Object 它存放在 rt.jar 之中无论哪一个类加载器都要加载这个类最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载。因此 Object 类在程序的各种类加载器环境中都是同一个类。相反如果没有使用双亲委派模型由各个类加载器自行去加载的话如果用户编写了一个称为 “java.lang.Object” 的类并存放在程序的ClassPath中那系统中将会出现多个不同的 Object 类java 类型体系中最基础的行为也就无法保证应用程序也将会一片混乱。 2.1.1.2 自定义类加载器
继承 ClassLoader 类重写 findClass() 方法 2.1.2 类加载过程
类的加载过程如下 JVM类加载机制分为五个部分加载校验准备解析初始化下面我们就分别来看一下这五个过程。其中加载、校验、准备、初始化和卸载这个五个阶段的顺序是固定的而解析则未必。为了支持动态绑定解析这个过程可以发生在初始化阶段之后。
加载Loading类加载器从字节流中读取类的二进制数据并将其加载到内存中。 验证Verification验证类的字节码是否符合JVM规范防止恶意代码的侵入。 准备Preparation为类的静态变量分配内存并设置默认值。 解析Resolution将类中的符号引用解析为直接引用。 初始化Initialization执行类的构造方法初始化类的静态变量和静态代码块。 2.1.2.1 加载
类加载器从字节流中读取类的二进制数据并将其加载到内存中。
加载过程主要完成三件事情
通过类的全限定名来获取定义此类的二进制字节流将这个类字节流代表的静态存储结构转为方法区的运行时数据结构在堆中生成一个代表此类的 java.lang.Class 对象作为访问方法区这些数据结构的入口。
这个过程主要由类加载器完成。 2.1.2.2 校验
验证类的字节码是否符合JVM规范防止恶意代码的侵入。
文件格式验证基于字节流验证。 是否以魔数0xCAFEBABE开头。主、次版本号是否在当前虚拟机处理范围之内。常量池的常量中是否有不被支持的常量类型检查常量tag标志。指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
元数据验证基于方法区的存储结构验证。 这个类是否有父类除了java.lang.Object之外所有类都应当有父类。这个类是否继承了不允许被继承的类被final修饰的类。如果这个类不是抽象类是否实现了其父类或接口之中所要求实现的所有方法。类中的字段、方法是否与父类产生矛盾例如覆盖了父类的final字段或者出现不符合规则的方法重载例如方法参数都一致但返回值类型却不同等等。
字节码验证基于方法区的存储结构验证。主要目的是通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作例如不会出现类似这样的情况在操作数栈放置了一个int类型的数据使用时却按long类型来加载入本地变量表中。保证跳转指令不会跳转到方法体以外的字节码指令上。保证方法体中的类型转换是有效的例如可以把一个子类对象赋值给父类数据类型但是把父类对象赋值给子类数据类型甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型则是危险不合法的。
符号引用验证基于方法区的存储结构验证。可以看作是类对自身以外常量池中的各种符号引用的信息进行匹配性校验。 符号引用中通过字符串描述的全限定名是否能够找到对应的类。在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。符号引用中的类、字段、方法的访问性private、protected、public、default是否可被当前类访问。 2.1.2.3 准备
为类变量分配内存不包括实例变量并将其初始化为默认值。此时为默认值在初始化的时候才会给变量赋值即在方法区中分配这些变量所使用的内存空间。例如 此时在准备阶段过后的初始值为0而不是123将value赋值为123的 putstatic 指令是程序被编译后存放于类构造器clinit方法之中。 特例 此时value的值在准备阶段过后就是123。 2.1.2.4 解析
把类型中的符号引用转换为直接引用。
符号引用与虚拟机实现的布局无关引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同但是它们能接受的符号引用必须是一致的因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用可以是指向目标的指针相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用那引用的目标必定已经在内存中存在
主要有以下四种
类或接口的解析字段解析类方法解析接口方法解析 2.1.2.5 初始化
初始化阶段是执行类构造器clinit方法的过程。clinit方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证clinit方法执行之前父类的clinit方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块那么编译器可以不为这个类生成clinit方法。
java中对于初始化阶段有且只有以下五种情况才会对要求类立刻“初始化”加载验证准备自然需要在此之前开始
使用 new 关键字实例化对象、访问或者设置一个类的静态字段被final修饰、编译器优化时已经放入常量池的例外、调用类方法都会初始化该静态字段或者静态方法所在的类。初始化类的时候如果其父类没有被初始化过则要先触发其父类初始化。使用 java.lang.reflect 包的方法进行反射调用的时候如果类没有被初始化则要先初始化。虚拟机启动时用户会先初始化要执行的主类含有main。jdk 1.7后如果 java.lang.invoke.MethodHandle 的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄并且这个方法所在类没有初始化则先初始化。 2.2 运行时数据区HotSpot虚拟机
JVM在程序执行时管理着多个内存区域称为运行时数据区。每个区域有不同的用途主要包括以下几个区域
方法区存储类的相关信息类信息、常量、静态变量、方法数据等。堆Heap存储Java对象是垃圾回收器管理的区域。虚拟机栈JVM Stack每个线程都有一个栈用来存储方法的局部变量、操作数栈、返回地址等信息。程序计数器PC Register存储当前线程正在执行的字节码的地址。本地方法栈Native Method Stack为JVM调用本地方法如C/C编写的库提供支持。 2.2.1 程序计数器
线程私有是一块较小的内存空间可以看做是当前线程所执行的字节码的行号指示器唯一不会造成 OutOfMemoryError 情况的区域。不过当线程执行的是 Native 方法的时候这个计数器中的值为 undefined 。
2.2.2 java虚拟机栈
线程私有描述的是 java 方法执行的内存模型每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧指执行一个方法所使用的那部分栈线程的所有方法的栈帧串起来就组成了一个完整的 java 虚拟机栈。java 虚拟机栈针对线程而言线程里的方法对应栈中的元素----栈帧每执行一个方法就为这个方法创建对应的栈帧并 push 到 线程这个大容器-----java虚拟机栈 中执行完成就 pop 出栈。
局部变量表存放了编译期可知的各种基本数据类型boolean、byte、char、short、int、long、float、double、对象引用用来指向内存中分配的类实例或者数组 和 returnAddress 类型指向字节码的指针。注long和double会占用2个局部变量空间8字节其余数据类型只占用一个4字节。对于局部变量如果是基本类型会把值直接存储在栈如果是引用类型比如String s new String(william);会把其对象存储在堆而把这个对象的引用指针存储在栈。
Java虚拟机规范中对这个区域规定了两种异常状况
线程请求栈的深度大于虚拟机所允许栈的深度将抛出Stack Overflow Error如果虚拟机栈可以动态扩展且扩展时无法申请到足够的内存会抛出OutOfMemoryError
这里注意的是如果递归的方法递归的太深很容易抛出上面两种异常所以递归虽然写起来方便但是性能会有所下降并且容易抛出异常。
-Xss128k设置每个线程的堆栈大小为128k若不设置则为JVM默认值此时默认可动态扩展。在相同物理内存下减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的不能无限生成经验值在3000~5000左右。 2.2.3 本地方法栈
线程私有作用和 java 虚拟机栈相似区别是 java 虚拟机栈为虚拟机执行 java 方法(也就是字节码)服务而本地方法栈则为虚拟机使用到的 Native 方法服务。Native方法指java调用非java代码(c)的接口(jni)。 sayHello() 方法加了一个关键字 native 就代表是一个native接口。执行这个方法时会根据 jni.h来找到真正的C编写的sayHello() 的实际函数。jni.h 是存在于 %JAVA_HOME%\include 下面的一个文件另外还有个 %JAVA_HOME%\include\win32 下的jni_md.h。 2.2.4 java堆
线程共享是java虚拟机所管理的内存中最大的一块在虚拟机启动时创建用于存放对象实例。java堆是垃圾收集器管理的主要区域。由于现在的垃圾回收算法多是分代收集所以Java堆里面又可分为新生代和老年代再细致一点年轻代还能分为Eden区、From Survivor空间、To Survivor空间。并且根据Java虚拟机规范的规定Java堆可以处于物理上不连续的内存空间中只要逻辑上连续即可。注意有实例没有被分配且堆无法再扩展的时候会抛出OutOfMemoryError异常虚拟机调优其实也主要关注的是这个区域。成员变量作为对象的属性当然是放在堆里了。对象在堆里对象中的内容就是各种字段。
-Xms:初始堆大小(最小堆)。-Xmx:最大堆大小。
-Xms 和 -Xmx 设置成一致的值可以避免堆自动扩展。Oracle官方推荐堆的初始化大小与堆可设置的最大值一般是相等的即 Xms Xmx因为起始堆内存太小Xms会导致启动初期频繁 GC起始堆内存较大Xmx有助于减少 GC 次数
-Xmn:年轻代大小Sun官方推荐配置为整个堆的1/3。对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置。
JVM内存大小 年轻代大小 老年代大小 永久代大小perm。永久代一般固定大小为64m所以增大年轻代后将会减小年老代大小。此值对系统性能影响较大Sun官方推荐配置为整个堆的3/8。
-XX:NewSize 指定新生代初始大小。-XX:MaxNewSize 指定新生代最大大小。-XX:NewRatio 是年老代 新生代相对的比例比如NewRatio2表明年老代是新生代的2倍。老年代占了heap的2/3新生代占了1/3。-XX:SurvivorRatio 配置的是在新生代里面Eden和一个Survivor比例。-XX:SurvivorRatio8表示新生代的Eden占8/10S1和S2各占1/10.
2.2.5 方法区
线程共享方法区是JVM的一种规范方法区在jdk1.7的实现是永久代jdk1.8的实现是元空间Metaspace。可以选择不实现垃圾收集。方法区主要用于存储类字节码、静态变量、常量池、即时编译器编译后的代码等数据。
对于方法区的理解我们要注意以下几个方面
方法区(Method Area)与堆一样是各个线程共享的内存区域。方法区在JVM启动的时候被创建并且它实际的物理内存空间和虚拟机堆区一样都可以是不连续的。方法区在JVM启动的时候被创建并且它实际的物理内存空间和虚拟机堆区一样都可以是不连续的。方法区的大小跟堆空间一样可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类。如果系统定义了太多的类导致方法区溢出虚拟机同样会抛出内存溢出错误如java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace。
以下情况都可能导致方法区发生OOM异常加载大量的第三方jar包、Tomcat部署的工程过多3050个或者大量动态地生成反射类。 JVM常量池详细见下一章 class文件常量池诞生于编译时存在于class文件中存放符号引用和字面量。运行时常量池诞生于JVM运行时jdk1.7永久代被移除后存在于元空间存放class文件元信息描述、引用类型数据、编译后的代码数据、类文件常量池综合了每个class文件常量池字符串常量池jdk1.6处于永久代jdk1.7后处于堆区存放字符串对象的引用基本类型包装类常量池位于堆区 2.2.5.1 永久代
存储已被 java 虚拟机加载的类字节码、静态变量、常量池、即时编译器编译后的代码等数据。jdk1.7 的 HotSpot 虚拟机中已经把原本放在永久代的 字符串常量池 和 静态变量 转移到 java 堆符号引用(Symbols) 转移到了本地内存(Native Memory)。jdk1.8中原有的永久代改为了元空间Metaspace直接使用机器物理内存因为原来的永久代内存大小不易评估同时调优效率较低。永久代的垃圾收集是和老年代捆绑在一起的。 配置参数 -XX:PermSize300M 设置永久代大小 -XX:MaxPermSize300M 设置永久代最大大小 2.2.5.2 元空间Metaspace
存储类的元信息存储位置不在虚拟机中而是使用本地内存 -XX:MetaspaceSize 初始空间大小达到该值就会触发垃圾收集进行类型卸载同时GC会对该值进行调整如果释放了大量的空间就适当降低该值如果释放了很少的空间那么在不超过MaxMetaspaceSize时适当提高该值。 -XX:MaxMetaspaceSize 最大空间默认是没有限制的。 除了上面两个指定大小的选项以外还有两个与 GC 相关的属性 -XX:MinMetaspaceFreeRatio 在GC之后最小的Metaspace剩余空间容量的百分比减少为分配空间所导致的垃圾收集。 -XX:MaxMetaspaceFreeRatio 在GC之后最大的Metaspace剩余空间容量的百分比减少为释放空间所导致的垃圾收集。 永久代和元空间的区别 1) 存储位置不同永久代物理上是堆的一部分和老年代、新生代地址在逻辑上是连续的元空间属于本地内存。 2) 存储内容不同元空间存储类的元信息静态变量和字符串常量池等并入堆中相当于永久代的数据被分裂到了堆和元空间。 JDK 8 中永久代向元空间的转换原因 1、字符串存在永久代中容易出现性能问题和内存溢出。2、类及方法的信息等比较难确定其大小因此对于永久代的大小指定比较困难太小容易出现永久代溢出太大则容易导致老年代溢出。而对于元空间来说类的元数据可以在本地内存(native memory)分配,所以其最大可利用空间是整个系统内存的可用空间。永久代会为 GC 带来不必要的复杂度并且回收效率偏低。Oracle 可能会将HotSpot 与 JRockit 合二为一。