JVM 系列 - 第一节:JVM 简介、运行时数据区、内存分代模型
一、什么是JVM?
JVM是Java Virtual Machine(Java虚拟机))的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM是一种规范,有很多种实现,比如Oracle/Sun JDK、OpenJDK等,用的都是相同的JVM:HotSpot VM;IBM开发的一个高度模块化的JVM:J9。除此之外,还有很多其他的JVM实现。通常大家说起“Java性能如何如何”、“Java有多少种GC”、“JVM如何调优”等问题,默认说的就是HotSpot VM,所以HotSpot VM是绝对的主流。下文提到的JVM都是指HotSpot。
Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在字节码文件中的指令。JVM有两个重要作用,1.机器码翻译。JVM保证“一次编译,多次运行”,原因是不同平台有不同的JVM,比如HotSpot有windows版和linux版本,不同的平台使用不同的版本,对于程序员来说,只需要关注些代码,不用考虑代码的移植性,因为不同平台的JVM已经屏蔽了系统的差异了。2.内存管理。程序员需要使用一个对象,只需要new出来,不用关心具体是如何new出来的,不用关心对象的生命周期是怎样的,也不用关心什么时候回收对象。
Java虚拟机主要分为五大模块:类加载器、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。下面的内容主要讲其中两块,运行时数据区、垃圾收集器。
二、运行时数据区
当一个线程运行前,会把需要执行的代码的不同部分,放到运行时数据区的不同区域,当线程运行时会从不同的位置拿数据。
2.1程序计数器
程序计数器存储当前线程正在执行的字节码指令的地址和行号。为什么要记录一个线程正在执行的字节码指令的地址和行号呢?线程是java的最小执行单元,因为CUP同时执行多个线程的时候,会涉及到线程的切换,当CUP切换线程时,要记录这些信息,以便CUP再次切换到当前线程时,该线程知道从什么位置开始继续执行。每个线程都会有自己的程序计数器。
2.1栈
虚拟机栈存储当前线程运行的方法所需要的数据、指令、返回地址。
举一个简单的代码示例:
找到上面TestJVM.java编译后的TestJVM.class文件,通过javap
命令查看字节码的每一条指令,将指令存入TestJVM.txt文件。
指令文件TestJVM.txt:
通过上面的指令描述文件,可以看出,TestJVM这个类的所有指令的描述。
JVM中的每一个线程拥有一个运行时栈。JVM会为每一个线程执行的方法在运行时栈中开辟一块空间,这块空间叫栈帧。每一个栈帧又分为几块,比如方法的局部变量表、操作数栈、动态链接、方法出口(返回地址)等:
在记录test方法的栈帧中,局部变量表中存在的是test方法中的四个本地变量:a/b/c/objc,对应TestJVM.txt指令描述文件的81-86行;
操作数栈中存的是局部变量对应的操作数,比如TestJVM.txt指令描述文件的62-74行指令中,第一个指令iconst_1:int型常量值1进栈,表示将int a = 1
这一句代码中的操作数1放入(压栈)操作数栈里(这时栈里只有一个数1);第二个指令istore0:将栈顶int型数值存入第0个局部变量,第0个局部变量是谁?TestJVM.txt指令描述文件的83行,表明第0个局部变量a,操作数1出站存入局部变量a。其实iconst1和istore_0就是int a = 1
这句代码执行的指令。其他的指令分析就不详细说了,具体每个指令什么意思可以自行网上查阅。
需要注意TestJVM.java的test方法第17行Object objc= new Object();
,这是一个对象,和上面本地变量a/b/c不同,这一句涉及到的指令有new、dup、invokespecial、astore_3四个指令。其中new指的是创建一个对象,具体是在堆上分配内存,并在栈顶压入指向这段内存的地址。对象是存在堆里的,栈里只存对象在堆中的地址。
动态链接指的是如果被调用的方法或对象在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。类似于TestJVM.txt的第70行和第72行中的#2和#1,对应TestJVM.txt的第12和11两行的Constant pool,动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用。简单举一个例子,通常在Controller层调用Service层时,通过@Autowired注入一个Service,通常使用的时一个Service的接口而不是实现类,在Controller的一个方法中通过Service的接口中的方法调用具体的Service实现,如果Service接口有多个实现,程序在编译期并不知道具体使用哪个实现类,这个时候就会在字节码Constant pool部分生成动态链接(#),在程序的运行期最终转换为调用方法的直接引用。通过字节码指令文件可以看出Constant pool翻译成”常量池“并不准确,Constant pool中除了有常量,还有符号引用(包括类、方法、字段等的描述符)。
方法出口(返回地址)指的是,当一个方法执行完成后,要出栈,那么出栈后要去哪,正常执行的方法出栈和异常执行的方法的出栈也不一样。
注意,在线程中递归调用某个方法时,方法的每次调用都会有一个栈帧,所以线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。虽然栈的大小可以自动扩展,但动态扩展时无法申请到更大的空间时,任然会报出OutOfMemory。
栈帧大小确定的时间在编译期,不受运行期数据影响。所以局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
2.3本地方法栈
本地方法栈和虚拟机栈类似,只是它描述的是本地方法在执行是的情况。什么是本地方法?本地方法是指方法被native关键字修饰的方法,在JDK中没有具体实现类的,具体的实现在JVM的代码中,这里就可以找到各种版本的Hotspot源码,源码是C或C++写的。之前文章中在分析CAS时就使用了Hotspot源码。
2.4方法区
方法区中存储类字节码的类信息、常量、静态变量、JIT等信息。
TestJVM.txt指令文件的第10-35行是常量池,这部分的内容就放在方法区的常量池中。
这里可以思考一下,静态变量和常量为什么不放在堆中?我感觉是因为常量和静态变量一般都是不变的,只要存储一份就可以了,放在堆中那么每次new对象都会存在相同的数据,造成空间浪费。
2.5堆
对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。
堆里存储的都是new出来的对象,栈里存储该对象的引用会指向堆里该对象的内存地址。所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟(浅谈HotSpot逃逸分析),这个说法也不是那么绝对,但是大多数情况都是这样的。
三、JVM内存分代模型
在JDK1.8之前,JVM的内存分三大块,新生代、老年代、永久代。其中前两块在堆中,后一块在方法区中。在JDK1.8及以后,移除了永久代使用了Meta Space。
为什么要分代?因为不同的对象的生命周期是不一样的,将不同生命周期的对象放在不同的代,使用不同的垃圾回收算法进行回收。
3.1新生代
一般来说,对象刚new出来会放在新生代,新生代中的对象一般是生命周期比较短,一次回收能回收(Minor GC)98%以上的对象。
新生代内存分为三块Eden区、s0区、s1区,为什么是分三块?
新生代分三块,主要的原因是新生代使用的垃圾回收算法使用的是复制算法。
S0和S1是两块大小相同、功能相同的区域,但是一次GC只能有一块区域起作用。当eden区第一次满了时,会触发第一次minor gc,回收eden区的对象,gc后,还有一个对象是可达的,那么就属于存活的对象,这个对象会被放到s0区域。当eden区第二次满了时,会触发第二次minor gc,这时会回收eden区和s0区域,如果这时对象b依然可达,并且对象j也可达,那么这两个对象就会进入s1区域。可以看出,每次gc时存活的对象会在s0和s1区域来回复制,这就是复制算法。每次gc后存活的对象,年龄都会加1,多次gc后年龄达到固定的阈值(默认15)后,对象会进入老年代。
新生代内存分为三块Eden区、s0区、s1区,比例是:8:1:1。为什么比例是8:1:1?因为复制算法中s0和s1只能一块区域起作用,另一块是空的,所以并不是所有的新生代都是有效的存储空间,s0和s1过大会导致可用内存变小并且eden区过小,minor gc会变得更频繁;s0和s1过小,导致较少的gc次数时,s0和s1就会满了,从而导致年龄较小的对象进入老年代。8:1:1可以看成是二八原则。
3.2老年代
新生代和老年代的内存比例是1:2。
新生代中多次回收后依然存在的对象会进入老年代。
3.3永久代和Meta Space
为了避免永久代的溢出,在JDK1.8及之后,去掉了永久代,使用了Meta Space。Meta Space这块内存属于代外分配的内存,使用的是机器的直接内存。Meta Space可以自动扩容,虽然可以自动扩容,但Meta Space也并不是越大越好,因为机器的总内存是固定的,Meta Space变大会挤压其他的内存空间的使用。
后续将继续介绍java内存模型、四种引用、GC回收算法、GC回收器、JVM优化等内容。
关注公众号,输入“java-summary”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。
版权声明: 本文为 InfoQ 作者【诸葛小猿】的原创文章。
原文链接:【http://xie.infoq.cn/article/759dcf6f8352bba08746bfcb3】。文章转载请联系作者。
评论 (2 条评论)