1 JVM简介
VM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
2 JVM的组成部分
我们先把JVM这个虚拟机画出来,如下图所示:
从这个图中可以看到,JVM是运行在操作系统之上的,它与硬件没有直接的交互。JVM有哪些组成部分,如下图所示:
整个JVM分为四部分:
- Class Loader 类加载器
类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java程序,然后通过javac编译成class文件,那怎么才能加载到内存中被执行呢?Class Loader承担的就是这个责任,那不可能随便建立一个.class文件就能被加载的,Class Loader加载的class文件是有格式要求,在《JVM Specification》中式这样定义Class文件的结构:
在这张图中,每一行表示两个字节长度,按照从上到下、从左到右的顺序描述了class文件的结构。其中,浅颜色的部分是无符号数,深颜色的部分是表。下面以表格的形式详细描述一下具体的信息:
类型 |
名称 |
数量 |
U4 |
magic |
1 |
U2 |
minor_version |
1 |
U2 |
major_version |
1 |
U2 |
constant_pool_count |
1 |
cp_info |
constant_pool |
constant_pool_count-1 |
U2 |
access_flags |
1 |
U2 |
this_class |
1 |
U2 |
super_class |
1 |
U2 |
interfaces_count |
1 |
U2 |
interfaces |
interfaces_count |
U2 |
fields_count |
1 |
field_info |
fields |
fields_count |
U2 |
methods_count |
1 |
method_info |
methods |
methods_count |
U2 |
attributes_count |
1 |
attribute_info |
attributes |
attributes_count |
Class Loader只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine负责的。
- Execution Engine 执行引擎
执行引擎也叫做解释器(Interpreter),负责解释命令,提交操作系统执行。
- Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见。
- Runtime data area运行数据区
运行数据区是整个JVM的重点。所有写的程序都被加载到这里,之后才开始运行。
整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,一个完整的系统诞生了!
2 JVM的内存管理
所有的数据和程序都是在运行数据区存放,它包括以下几部分:
- Stack 栈
栈也叫栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就Over。问题出来了:栈中存的是那些数据呢?又什么是格式呢?
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。
那栈帧中到底存在着什么数据呢?栈帧中主要保存3类数据:本地变量(Local Variables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(Frame Data),包括类文件、方法等等。光说比较枯燥,我们画个图来理解一下Java栈,如下图所示:
图示在一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2又调用了方法1,栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。
- Heap 堆内存
一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
Permanent Space 永久存储区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
Young Generation Space 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。
Tenure generation space养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。 三个区的示意图如下:
- Method Area 方法区
方法区是被所有线程共享,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
- PC Register 程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。
- Native Method Stack 本地方法栈
3 JVM相关问题
问:堆和栈有什么区别
答:堆是存放对象的,但是对象内的临时变量是存在栈内存中。
栈是跟随线程的,有线程就有栈,堆是跟随JVM的,有JVM就有堆内存。
问:堆内存中到底存在着什么东西?
答:对象,包括对象变量以及对象方法。
问:类变量和实例变量有什么区别?
答:静态变量是类变量,非静态变量是实例变量,有static修饰的变量是静态变量,没有static修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。
问:Java的方法(函数)到底是传值还是传址?
答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去。
问:为什么会产生OutOfMemory产生?
答:Heap内存中没有足够的可用内存了。这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。 但如果此时GC没有回收就会产生挂起情况,系统不响应了。
问:产生的对象不多呀,为什么还会产生OutOfMemory?
答:继承层次忒多了,Heap中 产生的对象是先产生 父类,然后才产生子类。
问:OutOfMemory错误分几种?
答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。
“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,一般重新启动一下即可。
问:为什么会产生StackOverflowError?
答:因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的。
问:一个机器上可以看多个JVM吗?JVM之间可以互访吗?
答:可以多个JVM,只要机器承受得了。JVM之间是不可以互访,不能在A-JVM中访问B-JVM的Heap内存,这是不可能的。在以前老版本的JVM中,会出现A-JVM Crack后影响到B-JVM,现在版本非常少见。
问:JVM中到底哪些区域是共享的?哪些是私有的?
答:Heap和Method Area是共享的,其他都是私有的,
问:什么是JIT?
答:JIT是指Just In Time,有的文档把JIT作为JVM的一个部件来介绍,有的是作为执行引擎的一部分来介绍,这都能理解。Java刚诞生的时候是一个解释性语言,即使编译成了字节码(byte code)也是针对JVM的,它需要再次翻译成原生代码(native code)才能被机器执行,于是效率的担忧就提出来了。Sun为了解决该问题提出了一套新的机制,好,你想编译成原生代码,没问题,我在JVM上提供一个工具,把字节码编译成原生码,下次你来访问的时候直接访问原生码就成了,于是JIT就诞生了,就这么回事。
问:JVM还有哪些部分是没有提到的?
答:JVM是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的:
常量池(constant pool):按照顺序存放程序中的常量,并且进行索引编号的区域。比如int i =100,这个100就放在常量池中。
安全管理器(Security Manager):提供Java运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader在Security Manager认证通过后才能加载class文件的。
方法索引表(Methods table),记录的是每个method的地址信息,Stack和Heap中的地址指针其实是指向Methods table地址。
问:为什么不建议在程序中显式的生命System.gc()?
答:因为显式声明是做堆内存全扫描,也就是Full GC,是需要停止所有的活动的(Stop The World Collection)。