虚拟机的历史版本和JVM数据区域详解
一、虚拟机的历史
- Sun Classic:
Sun Classic VM的技术可能很原始,这款虚拟机的使命也早已终结,但仅凭它“世界上第一款商用Java虚拟机”的头衔,就足够有让历史记住它的理由,Sun公司发布JDK 1.0中所带的虚拟机就是Classic VM。这款虚拟机只能使用纯解释器方式来执行Java代码,如果要使用JIT编译器,就必须进行外挂。正是因为编译器和解释器不配合工作,就导致“Java语言很慢”这样的形象在用户心中建立,为了解决这样的问题,在发布JDK1.2时就发布过一款名为Exact VM的虚拟机。
2.Exact VM:
Exact VM它的执行系统已经具备现代高性能虚拟机的雏形:如两级即时编译器、编译器与解释器混合工作模式等。Exact VM因它使用准确式内存管理(Exact Memory Management,也可以叫Non-Conservative/Accurate Memory Management)而得名即虚拟机可以知道内存中某个位置的数据具体是什么类型。譬如内存中有一个32位的整数123456,它到底是一个reference类型指向123456的内存地址还是一个数值为123456的整数,虚拟机将有能力分辨出来,这样才能在GC(垃圾收集)的时候准确判断堆上的数据是否还可能被使用。
3.HotSpot VM:
Sun JDK和OpenJDK中所带的虚 拟机,也是目前使用范围最广的Java虚拟机。但不一定所有人都知道的是,这个目前看起来“血统纯正”的虚拟机在最初并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的。HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势,如它名称中的HotSpot指的就是它的热点代码探测技术(其实两个VM基本上是同时期的独立产品,HotSpot还稍早一些,HotSpot一开始就是准确式GC,而Exact VM之中也有与HotSpot几乎一样的热点探测。HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。HotSpot虚拟机主要用于服务器、桌面领域的商用虚拟机。
4.Sun Mobile-Embedded VM / Meta-Circular VM:
Sun公司面对移动和嵌入式市场,发布过的虚拟机产品。
5.JRockit VM:
JRockit VM曾经号称“世界上速度最快的Java虚拟机”,BEA公司将其发展为一款专门为服务器硬件和服务器端应用场景高度优化的虚拟机,由于专注于服务器端应用,它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行。除此之外,JRockit的垃圾收集器和MissionControl服务套件等部分的实现,在众多Java虚拟机中也一直处于领先水平。
6.IBM J9:
与BEA JRockit专注于服务器端应用不同,IBM J9的市场定位与Sun HotSpot比较接近,它是一款设计上从服务器端到桌面应用再到嵌入式都全面考虑的多用途虚拟机,J9的开发目的是作为IBM公司各种Java产品的执行平台,它的主要市场是和IBM产品(如IBM WebSphere等)搭配以及在IBM AIX和z/OS这些平台上部署Java应用。因为在电信领域IBM的服务器占据了很大份额,所以才会在华为中IBM J9中有较大份额。
7.BEA Liquid VM:
我们平时所提及的“高性能Java虚拟机”一般是指HotSpot、JRockit、J9这类在通用平台上运行的商用虚拟机,但其实Azul VM和BEA Liquid VM这类特定硬件平台专有的虚拟机才是“高性能”的武器。Liquid VM即是现在的JRockit VE(Virtual Edition),它是BEA公司开发的,可以直接运行在自家Hypervisor系统上的JRockit VM的虚拟化版本,Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如文件系统、网络支持等。由虚拟机越过通用操作系统直接控制硬件可以获得很多好处,如在线程调度时,不需要再进行内核态/用户态的切换等,这样可以最大限度地发挥硬件的能力,提升Java程序的执行性能。
8.Dalvik VM:
Dalvik VM并不是一个Java虚拟机,它没有遵循Java虚拟机规范,不能直接执行Java的Class文件,使用的是寄存器架构而不是JVM中常见的栈架构。但是它与Java又有着千丝万缕的联系,它执行的dex(Dalvik Executable)文件可以通过Class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。目前Dalvik VM随着Android一起处于迅猛发展阶段,在Android 2.2中已提供即时编译器实现,在执行性能上有了很大的提高。
9.Apache Harmony:
Apache Harmony是一个Apache软件基金会旗下以Apache License协议开源的实际兼容于JDK 1.5和JDK 1.6的Java程序运行平台,这个介绍相当拗口。它包含自己的虚拟机和Java库,用户可以在上面运行Eclipse、Tomcat、Maven等常见的Java程序,但是它没有通过TCK认证,所以我们不得不用那么一长串拗口的语言来介绍它,而不能用一句“Apache的JDK”来说明。如果一个公司要宣布自己的运行平台“兼容于Java语言”,那就必须要通过TCK(Technology Compatibility Kit)的兼容性测试。因为Apache和SUN公司的矛盾,Apache就自己发展了,后来随着SUN公司将JDK开源形成openjdk后,Apache Harmony就没落了。
10.Microsoft JVM:
由于微软和SUN公司的官司,这个JVM就流产了。
- 未来的虚拟机技术
- 模块化:
随着应用系统的庞大和复杂,站在软件工业化的角度来看,模块化是一个必然趋势,
JDK9的里面的特性就是模块化,在我们JAVA应用层面来讲,没有模块化的时候,应用上我们就提出了微服务。
2. 混合语言:
虚拟机是独立于语言,不只是只有JAVA语言才能运行在虚拟机上。
3. 多核并行:
CPU一开始成高频转变成多核,多核时代,我们需要关注并发编程,JDK5中引入了比较粗的并发编程,JDK7中引入并行计算框架Fork/join,JDK8中引入lambada,函数编程天生就是适合并发编程的技术。
4. 丰富语法:
JDK5中提出自动装箱、枚举、泛型、动态注解、遍历循环。JDK7里面Switch中可用String、支持二进制数字、try resource语句帮我们进行关闭,这都是JAVA在语法上的丰富。
5、64位:
64位机器正在逐步取代32位机器,JAVA的支持也都在向64位靠拢。
6、更强的垃圾回收:
目前放出来的JDK11里面的ZGC,号称是支持TB级内存容量,暂停时间低(<10ms)。
- 运行时数据区域
- 程序计数器:
较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响;
2. 虚拟机栈:
线程私有,生命周期和线程同生共死,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用(-Xss) ;-Xss参数是用来调整JAVA虚拟机栈的。一个线程调用多个方法,只会有一个栈。栈的缺省大小为1M
3. 本地方法栈:
本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法;我们可以查看源码,找到Object类,找到wait方法就会看到有native修饰。一个线程也只有一个本地方法栈。
4. JAVA堆:
与我们操作最为紧密的区域,在代码中用new对象的时候,操作的就是该区域。几乎是所有的对象都在堆上分配,与堆相关的命令是:(-Xms;-Xmx;-Xmn;-XX:NewSize;-XX:MaxNewSize)
-Xms;堆的最小值
-Xmx;堆的最大值
-Xmn;新生代的大小
-XX:NewSize;新生代的最小值
-XX:MaxNewSize;新生代的最大值
5. 方法区(运行时常量池):
也叫永久区(永久代),用于存储已经被虚拟机加载的类信息,常量("zdy","123"等),静态变量(static变量)等数据(-XX:PermSize;- XX:MaxPermSize;-XX:MetaspaceSize; - XX:MaxMetaspaceSize ),在JDK7及之前用的是-XX:PermSize;-XX:MaxPermSize;在JDK8及之后用的是(元数据空间)-XX:MetaspaceSize;-XX:MaxMetaspaceSize
6. 运行时常量池:
严格来说是方法区的一部分,JDK8及之后运行时常量池放到了堆里面。
JDK8之后提出了元数据空间的概念,这个方法区就消失了,方法区从运行时数据区挪到了虚拟机本身管理之外,只受制于物理内存的大小,不再受制于虚拟机的管理内存大小。同时运行时常量池也挪到了方法区。元空间不是直接内存,直接内存主要用于IO通信。
永久代为什么说在JDK8之后要把它挪出来?在之前写代码中,用的默认的大小是64M,很容易碰到永久代溢出的问题。对永久代的调优也是比较困难的
直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;MaxDirectMemorySize命令:限制直接内存大小。
四、站在线程的角度
1、 线程共享的区域:JAVA堆和方法区。
2、 线程私有内存区:虚拟机栈、本地方法栈、程序计数器。
3、 一个线程同一时间只能运行一个方法,所以肯定只有一个栈。
4、 程序计数器也是同理,同一时间只能运行一行代码。
5、 栈和程序计数器和线程同生共死,堆和方法区和java程序Java进程同生共死,也就决定了可以多个线程共享。
五、方法的出入栈
方法会打包成栈帧,一个栈帧至少包含局部变量表、操作数栈和帧数据区。方法的执行过程就是出栈入栈的过程。
六、栈上分配
什么叫栈上分配?对于线程所私有的对象,可以将其打散,在栈上进行分配,而不在堆上分配。
那么栈上分配有什么好处?
1、跟着函数调用自行销毁。
2、不需要垃圾回收器的回收,可以提高性能。
栈上分配的基础需要逃逸分析,逃逸分析的作用是判断我们分配的对象有没有可能逃出这个方法体如果我们最后return这个对象,那即使你把对象定义在了函数体内,但是这个对象还是会逃出这个方法体这个对象就逃出了你的方法作用域,有可能被其他方法线程调用,就不能进行栈上分配,因为对象逃跑了
代码示例:
public class StackAllocation {
class User {
private Integer id;
private String name;
}
private void test() {
User user = new User();
user.id = 1;
user.name = "name:" + 1;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
StackAllocation stackAllocation = new StackAllocation();
for (int i = 0; i < 100000000; i++) {
stackAllocation.test();
}
long end = System.currentTimeMillis();
System.out.println("时间总计;" + (end - start) + "ms");
}
}
测试逃逸分析的命令:
-server -Xmx:10m -Xms:10m -XX:+DoEscapeAnalysis -XX:+PringGC -XX:+EliminateAllocations -XX:-UseTLAB
-server 表示JVM的运行模式,有桌面模式和服务器模式,windows下会由桌面模式,Linux下一般是由服务器模式用的最多的一般是mix/client模式,mix混合模式会根据你的操作系统来判断用什么模式但这里强制使用server模式是因为只有在server模式下才能进行逃逸分析。
-Xmx:10m 堆最大内存。
-Xms:10m 堆最小内存。
-XX:+DoEscapeAnalysis 是否开启逃逸分析。
-XX:+PringGC 打印GC日志。
-XX:+EliminateAllocations 标量替换,是不是容许将对象打散分配在栈上
-XX:-UseTLAB
TLAB:ThreadLocalAllocBuffer 线程本地分配缓存为什么有这个东西?创建对象需要在堆上分配,堆上有一块一块区域,线程A、B申请各自的区域,如果有大量的线程来堆上申请内存,为了保证线程不会申请同一块内存,我们要进行加锁的操作,为了保证数据的独立TLAB会事先在堆中为每个线程分配一块私有内存,A线程用A的内存,B线程用B的内存,排排坐之后可以大大提高性能,TLAB是指每个线程在new对象的时候进行私有分配。但这个对象对所有的线程是可见和共享的。例如:内存是一个村子,每个人自己有自己的地皮,不能到别人地皮上建房子,但是房子别人都可以看见,都可以来玩,来住。-XX:-UseTLAB 我们是指关闭私有分配。
+和-的意义就是:+代表开启这个参数,-代表关闭这个参数。