JVM面试向——JVM体系结构
JVM的位置
所以要理解一个问题:JVM是运行在操作系统之上的,它与硬件没有直接的交互
什么是HotSpot?
了解三种JVM:
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的J9VM
JVM体系结构图
如果你不能够闭着眼睛画出JVM体系结构图,说明你还没有入门JVM
java栈
、本地方法栈
、程序计数器
三个区域都是线程私有的,一般没有什么垃圾回收,都是伴随着线程结束或者方法的结束就清除内存了。而java堆
、方法区
是线程共享的、可能会存在垃圾,方法区
中存在垃圾的可能性比较小,大部分垃圾都在java堆
。所以我们说的JVM调优,99%都是在调java堆
这块区域。至于为什么是这样,可以接着看每一块区域的功能。
在整个JVM的学习过程中,大脑里要一直留着这幅图的印象!
程序计数器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的。会随着线程的终止而释放栈内存,随意不需要垃圾回收。
程序计数器是一块较小的内存空间,他的作用可以看作是当前线程所执行的字节码的行号指示器、指令地址。在虚拟机的概念模型里字节码计时器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等其基础功能都需要依赖这个计数器来完成。是一个非常小的内存空间,几乎可以忽略不计。
public class Calc {
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
}
反编译:javap -c xx.class
反编译之后会有助记符。
ldc
表示将int、float或是String类型的常量值从常量池中推至栈顶。bipush
表示将单字节(-128 - 127)的常量值推至栈顶sipush
表示将短整型(-32767 - 32768)的常量值推至栈顶。istore_1
将一个数值从操作数栈存储到局部变量表iadd
加imul
乘
图中使用红框框起来的就是字节码指令的偏移地址,偏移地址对应的bipush等等就是jvm中的操作指令,只是入栈指令。当执行到方法calc()时在当前的线程中会创建相应的程序计数器,在计数器中为存放执行地址0 2 3 …等。
栈(Stack)
栈和队列
栈:后进先出/先进后出
队列:先进先出(FIFO:First Input First Out)
Java虚拟机栈是什么
虚拟机栈是也是线程私有的,栈的周期和线程是一样的,不需要垃圾回收。
Java虚拟机栈中存储的是一个一个的栈帧,一个在执行Java方法会创建一个栈帧压入虚拟机栈。每个栈帧中存储了局部变量表
、操作数栈
、动态连接
、返回地址
。这些部分具体存储内容及实现可自行百度。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
平时我们的Java方法的执行顺序(什么时候开始,什么时候结束)使用的就是这个栈结构在管理,因为栈是先进后出的结构,处于栈顶的方法永远是在运行的方法。
public static void main(String[] args) {
a();
}
private static void a() {
b();
}
private static void b() {
}
public static void main(String[] args) {
a();
b();
}
private static void a() {
}
private static void b() {
}
喝多了吐就是栈,吃多了拉就是队列
栈内存溢出
java.lang.StackOverflowError
当线程请求栈深度超过虚拟机允许栈最大深度时,会报栈内存溢出。可以通过JVM参数调整最大栈深度。
方法自己调自己就会导致栈溢出(递归死循环测试)
static int i = 0;
public static void main(String[] args) {
try {
a();
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
private static void a() {
i++;
b();
}
private static void b() {
i++;
a();
}
结果i=26696,即为栈桢数,栈帧数和栈帧长度会影响栈深度。
本地方法栈
本地方法栈和Java虚拟机栈类似,都是线程私有的,没有垃圾回收。只不过一个是为我们编写的Java字节码文件服务,一个是为了C/C++底层实现的方法服务。
多线程类源码
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
凡是带了native关键字的,说明java的作用范围达不到,去调用底层C语言的库!
JNI:Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈:
Native Method Stack 本地方法栈;
本地接口的作用是融合不同的编程语言为Java所用,他的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体实现是 在Native Method Stack中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多介绍!
方法区
Method Area方法区是Java虚拟机规范中定义的运行时数据区域之一,它与堆(heap)一样在线程之间共享。
注意
首先很容易混淆的概念,方法区
、永久代
、元空间
。
方法区:是一种规范,类似Java中的接口功能,我只需要定义这样一个区域,然后实现什么样的功能。具体怎么实现,名字怎么叫,使用什么数据结构,使用内存还是外存我都不干涉。但是一个虚拟机应该有实现这样一个功能的区域。
永久代:是一种方法区的实现方式。java8之前的HotSpot使用永久代来实现方法区,为什么叫永久代呢,因为方法区是堆的逻辑组成部分。当时GC把堆结构分成了三部分,新生代、老年代和永久代,所以我们一般成方法区为永久代。
元空间:在java8及以后,就用元空间来取代了永久代,元空间使用的是与堆不相连的本地内存区域。这样做有很大的好处。
方法区是什么?
Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
JDK7之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译之后的代码等数据。每当一个类初次被加载的时候,它的元数据都会被都会被放到永久代中。永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即java.lang.OutOfMemoryError:PermGen.
JDK8彻底将永久代移除出HotSpot JVM,将其原有的数据迁移至java Heap(Java堆)或Native Heap(Metaspace 元空间),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在HotSpot JVM中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质与永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大区别在于:元空间并不在虚拟机中,而是使用本地内存。
可以通过-XX:MetaspaceSize
和-XX:MaxMethodspaceSize
配置内存大小。
如果Metaspace的空间占用到达了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。
堆(Heap)
java7之前
Heap堆,一个Java实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类,方法,常量池放到堆内存中,保存所有引用类型的真是信息,以方便执行器执行,堆内存分为三部分:
- 新生区 Young Generation Space Young/New
- 养老区 Tenure Gengration Space Old/Tenure
- 永久区 Permanent Space Perm
堆内存逻辑上分为三部分:新生,养老,永久(元空间:JDK8以后名称)
GC垃圾回收主要是在 新生区和养老区,又分为 轻GC和重GC,如果内存不够,或者存在死循环,就会导致java.lang.OutOfMemoryError:Java heap space
新生区
新生区是类诞生,成长,消亡的区域,一个在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区被new出来的,幸存区有两个:0区和1区,当伊甸区的空间用完时,程序又要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC)。将伊甸区中剩余的对象移动到幸存者0区,若幸存者0区也满了,再进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生Major GC(Full GC),进行养老区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常OutOfMemoryError
。
如果出现java.lang.OutOfMemoryError:java heap space
异常,说明Java虚拟机的堆内存不够,原因如下:
1、Java虚拟机的堆内存设置不够,可以通过参数-Xms(初始值大小),-Xmx(最大大小)来调整
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
Sun HotSpot内存管理
分代管理,不同的区域使用不同的算法:
why? 真相:经过研究,不同对象的生命周期不同,在Java中98%的对象都是临时对象。
永久区(Perm)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被来及回收器收掉的,关闭JVM才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError:PermGen space
,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
注意:
jdk1.6之前: 有永久代,常量池在方法区
jdk1.7 有永久代,但是已经逐步”去永久代“,常量池1.7在堆
jdk1.8及以后 无永久代,常量池1.8在元空间
熟悉三区结构后方可学习JVM垃圾回收机制
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的的:类信息+普通常量+静态常量+编译器编译后的代码,虽然JVM规范是将方法区描述为堆的一个逻辑部分,但它却还有一个别名,叫做(Non-Heap)非堆,目的是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法去称之为”永久代(Permannet Gen)“,但严格本质上说两者不同,或者说使用永久代实现方法而已,永久代是方法区的一个实现(相当于是一个接口interface),jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本,字段,方法,接口描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放!