java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称。
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。下面通过本文给大家介绍 jvm 原理与调优相关知识,感兴趣的朋友一起学习吧
JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM 屏蔽了与具体操作系统平台相关的信息,使 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码), 就可以在多种平台上不加修改地运行。是运行 Java 应用最底层部分。
JDK(Java Development kit)
整个 Java 的核心,包括了 Java 运行环境(Java Runtime Envirnment),一堆 Java 工具(编译,debug 等)和 Java 基础的类库 (rt.jar)。是开发 java 应用的基础。
JRE(Java Runtime Environment,Java 运行环境)
运行 JAVA 程序所必须的环境的集合,包含 JVM 标准实现及 Java 核心类库。运行 java 应用的基础。
J2SE(Java 2 Platform,Standard Edition)。
包含那些构成 Java 语言核心的类。比如:数据库连接、接口定义、输入 / 输出、网络编程
J2EE(Java 2 Platform,Enterprise Edition)。
Enterprise Edition(企业版) J2EE 包含 J2SE 中的类,并且还包含用于开发企业级应用的类。比如:EJB、servlet、JSP、XML、事务控制。
主要 JVM
首先,JVM 是一套规范。很多公司均实现了各自的虚拟机。常见的有
HotSpot JVM(sun)
Jrockit JVM(BEA 公司的 JVM,应用于 weblogic)
IBM JVM
Apache Harmony
其中,我们常用的是 HotSpot JVM.
JVM 结构
第一步 (编译):
创建完源文件之后,程序会先被编译为. class 文件。Java 编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后 引用,这个有点象 make。如果 java 编译器在指定目录下找不到该类所其依赖的类的. class 文件或者. java 源文件的话,编译器话 报 "cant find symbol" 的错误。
第二步(运行):
Java 类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。
需要说明的是:JVM 主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
1、 在 编译好 java 程序得到 MainApp.class 文件后,在命令行上敲 java AppMain。系统就会启动一个 jvm 进程,jvm 进程从 classpath 路径中找到一个名为 AppMain.class 的二进制文件,将 MainApp 的类信息加载到运行时数据区的方法区内,这个过程叫做 MainApp 类的加载。
2、 (java 命令)然后 JVM 找到 AppMain 的主函数入口,开始执行 main 函数
3、 (类加载器)执行过程中,会创建对象。JVM 会首先从方法区加载类信息和相关常量,class 加载完毕之后, 在堆上为对象分配内存,然后调用初始化实例,当然这时候实例保持指向 class 类型信息,这个信息保存在方法区中。
4、 (执行引擎)调用实例方法时,会根据引用找到对象信息,进而可定位对应的 class 类型信息,和方法表。
5、 (执行引擎)执行方法时,在虚拟机栈中进行,分配栈帧,随着入栈出栈,完成方法调用操作。
执行引擎
运行 Java 的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过 JIT 执行编译过的本地代码。我们上文讲到的 main 函数,也就是执行引擎的操作入口。
Class 文件
实际上,Class 文件中方法的字节码流就是有 JVM 的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随 0 个或多个操作数。
iload_0 // 把存储在局部变量区中索引为 0 的整数压入操作数栈。
iload_1 // 把存储在局部变量区中索引为 1 的整数压入操作数栈。
iadd // 从操作数栈中弹出两个整数相加,在将结果压入操作数栈。
istore_2 // 从操作数栈中弹出结果
JVM 运行时数据区
1) 程序计数器 (线程私有)
当前线程所执行的字节码的行号指示器,通过改变这个计数器的值,确定下一条要执行的命令。分支,循环,跳转都需要它的支持。
它是线程私有的,每个线程都有专属于自己的程序记数器,线程之间互不影响,独立存储,保证了线程切换后,可以恢复到原先执行位置。
2)Java 虚拟机栈(线程私有)
每个方法的执行,同时都会在虚拟机栈上创建一个栈帧。用于存储局部变量表,操作数栈,方法出口,动态链接等。一个方法的执行周期,同时也就对应着栈帧的出栈入栈操作。有时候方法的递归,会造成大量的栈帧,达到一定的深度,会报 StackOverflowError 异常。有一点需要说明: 在编译器编译 Java 代码时,就已经在字节码中为每个方法都设置好了局部变量区和操作数栈的数据和大小。并在 JVM 首次加载方法所属的 Class 文件时, 就将这些数据放进了方法区。因此在线程调用方法时,只需要根据方法区中的局部变量区和操作数栈的大小来分配一个新的栈帧的内存大小,并堆入 Java 栈。
局部变量区: 用来存放方法中的所有局部变量值,包括传递的参数。这些数据会被组织成以一个字长 (32bit 或 64bit) 为单位的数组结构 (以索引 0 开始) 中。其中类 型为 int, float, reference(引用类型,记录对象在堆中地址)和 returnAddress(一种 JVM 内部使用的基本类型)的值占用 1 个字长,而 byte, char 和 shot 会扩大成 1 个字长存储,long,double 则使用 2 个字长。
操作数栈: 用来在执行指令的时候存储和使用中间结果数据。
帧数据区: 常量池的解析,正常方法返回以及异常派发机制的信息数据都存储在其中。
3)本地方法栈(线程私有)
与 Java 虚拟机栈类似,只不过该区域是为 native 方法提供服务。
4)方法区 (Perm)(线程共享)
存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。包含运行时常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容是在类加载后进入方法区运行时常量池中。
5)堆
堆是整个内存数据区最负责的部分,负责对象的创建。同时,垃圾回收的主要工作也在于此。堆又进一步进行细分,主要是为了满足垃圾回收。
堆的组成
Eden(伊甸园):对象创建的入口。
Survivor Space: 用于保存在 eden space 内存池中经过垃圾回收后没有被回收的对象,也就是 "幸存还活着" 的对象。
幸存者 0 区(Survivor 0 space)和幸存者 1 区(Survivor1 space):当伊甸园的空间用完时,程序又需要创建对象;此时 JVM 的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象 进行销毁工作。同时将伊甸园中的还有其他对象引用的对象移动到幸存者 0 区。幸存者 0 区就是用于存放伊甸园垃圾回收时所幸存下来的 JAVA 对象。
当将伊甸园中的还有其他对象引用的对象移动到幸存者 0 区时,如果幸存者 0 区也没有空间来存放这些对象时,JVM 的垃圾回收器将对幸存者 0 区进行垃圾 回收处理,将幸存者 0 区中不在有其他对象引用的 JAVA 对象进行销毁,将幸存者 0 区中还有其他对象引用的对象移动到幸存者 1 区。幸存者 1 区的作用就是用于 存放幸存者 0 区垃圾回收处理所幸存下来的 JAVA 对象。
Tenured : 对象经过 survivor 1 space 内存池,每经历过一次垃圾回收,年龄就增加 1,超过设定阀值后,被移入终身代,当然也包括由于担保机制移入的对象。对于新生代和老年代,垃圾回收器对其态度不同。发生在新生代的回收频率频繁,大部分对象是 "朝生夕死",收集算法一般采用高效简单的复制算法,也就是上文描述的对象转移操作(Eden->survivor 0,survivor 0->survivor 1)。发生在该区域的垃圾回收为 Young GC. 对于老年代,由于大部分对象主要为存活率高的对象,垃圾回收器采用 "标记 - 整理" 算法。发生在该区域的垃圾回收为 FULL GC.
堆相关参数
(影响堆空间划分,进而会影响 GC 发生频率)JVM 调优工作,主要是基于这些参数,进行适当调整管理,达到调整堆内存大小及比例大小,以满足实际业务需求。另外还包括方法区。
-Xms:设置 Java 应用程序启动时的初始堆大小;
-Xmx:设置 Java 应用程序能获得的最大堆大小;
-Xss:设置线程栈的大小;
-XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个数值时,JVM 便会扩展堆空间;
-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆;
-XX:NewSize:设置新生代的大小;
-XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小;
-XX:SurvivorRatio:新生代中 eden 区与 survivor 区的比例;
-XX:MaxPermSize:设置最大的持久区大小;
-XX:TargetSurvivorRatio: 设置 survivor 区的可使用率。当 survivor 区的空间使用率达到这个数值时,会将对象送入老年代。
对象的生命周期
创建阶段
1、检查指令的参数,是否能在常量池中定位到一个类的符号引用,如果是引用,判断代表的类是否加载,解析和初始化过
2、如果没有加载,则必须进行加载,解析和初始化
3、类加载检查,这时候已经知道所需内存的大小。
4、分配内存。从 java 堆中划分一块大小确定的内存。支持 2 种方式,至于选择哪种方式分配内存,与 java 堆是否规整有关(也就是是否空间空间和使用空间相互交错情况)。1. 指针碰撞(分界点的指示器移动);2. 空闲列表方式。然而,java 堆是否规整,则取决于垃圾收集器的工作方式。此外,在分配内存时还要考虑多线程情况,保证原子性。分配内存的原子性有 2 种方式进行保证(CAS 和 本地线程分配缓冲 - XX +/- UseTLAB)。
5)、分配内存完成后,初始化内存空间(初始化为 0)
6、维护对象的对象头信息。如元数据信息,哈希码,GC 分代年龄,锁信息,类元指针。
7、调用 init 方法,按照程序员意愿进行初始化。
<7.1> 从超类到子类对 static 成员进行初始化;
<7.2> 超类成员变量按顺序初始化,递归调用超类的构造方法;
<7.3> 子类成员变量按顺序初始化,子类构造方法调用。
应用阶段
分为强引用、软引用、虚引用、若引用
不可视阶段;
当一个对象处于不可视阶段,说明我们在其他区域的代码中已经不可以在引用它,其强引用已经消失,例如,本地变量超出了其可视的范围。
不可到达阶段;
处于 JVM 对象生命周期不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量, 所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其 实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
可收集阶段、终结阶段、释放阶段 ;
当一个对象处于可收集阶段、终结阶段与释放阶段时
<1> 回收器发现该对象已经不可达。
<2> finalize 方法已经被执行。
<3> 对象空间已被重用。
来源: http://www.phperz.com/article/17/1222/357587.html