JVM篇
- JDK、JRE 和JVM的区别
- 说一下JVM由哪些部分组成
- 说一下 JVM运行时数据区域
- 详细介绍程序计数器
- 详细介绍Java 虚拟机栈
- 什么是栈帧
- 什么是局部变量表? 操作数栈?
- 垃圾回收算法
- Java中会存在内存泄漏吗?
- Minor GC、 Major GC、 Full GC 是什么?
- Minor GC 、Major GC 的触发条件
- 为什么新生代要分Eden 和两个Survivor区域?
- 为什么要分代(新生代和老生代)?
JDK、JRE 和JVM的区别
JDK(Java Development Kit,java开发工具)包括Java语言、工具类 (包括编译器(javac.exe)、开发工具(javadoc.exe、jar.exe、keytool.exe、jconsole.exe))) 还有JRE(Java Runtime Environment)。开发Java程序(包括编译和运行),需要安装JDK。
JRE(Java运行环境)包含了部署技术、JavaSE API(基本的类库如java.lang和util等jar)和JVM。如果只是部署和运行Java程序,那么 实际上只安装JRE 就能达到效果。在服务器上只安装JRE的情况: 1.发布到服务器上时所有文件都是编译好的文件,包括 JSP 文件。 2. 后期不在服务器上直接修改(因为导致修改后的文件未重新编译)。而JRE中没有Java编译和翻译所需工具类(java和Javac)。
JVM(Java Virtual Machine, Java 虚拟机),用于解释字节码文件并翻译成不同操作系统的机器码。是Java实现跨平台运行的基础。
JAVA中就虚拟机是用的是C语言+汇编语言开发,除此之外其余都是java语言开发的。
如下图:
说一下JVM由哪些部分组成
JVM包含两个子系统和两个子组件:两个子系统即类加载子系统(Class Loader System)和执行引擎(Execution Engine)。两个组件为运行数据区域(Runtime Data Area)和本地接口(Native Interface )
- 类加载器(Class Loader): 根据给定的全限定类名(如java.util.Scanner)来装载class 文件到运行时数据区域(Runtime Data Area) 的方法区(Method Area)。
- 执行引擎(Execution Engine): 执行字节码文件中的指令
- 本地接口(Native Interface): 与Native Libraries 交互,是与其他编程语言交互的接口。
- 运行时数据区域(Runtime Data Area): JVM内存
执行流程:
源文件(.java)经编译器(Javac .exe)编译成字节码文件(.class )。字节码文件经类装载子系统的类加载器(Class Loader)加载(从硬盘读取到内存的过程)到运行时数据区的方法区(Method Area )。而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作 系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine)将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。
说一下 JVM运行时数据区域
Java运行时数据区域包括:
- 虚拟机栈(VM Stack): JVM为每个执行的Java方法分配一个栈帧。栈帧保存该方法的出口、局部变量表、参数、操作数栈、动态链接等。栈帧是虚拟机栈的基本单位。
- 本地方法栈(Native Method Stack): 与虚拟机栈作用一样。只不过本地方法栈维护的是本地方法,这类方法是由c++/c语言编写。
- 程序计数器(Program Counter Register): 当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的数值,来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理都是基于这个计数器来实现的
- Java 堆(Java Heap): Java虚拟机中内存最大的一块,是被所有线程共享,几乎所有的对象和数组实例都在这里分配内存。
- 方法区(Method Area): 用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
详细介绍程序计数器
程序计数器是一块较小的内存空间,可以看成当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转异常处理、线程恢复都依赖于这个计数器。
为了使得线程切换后还能恢复到正确的执行位置,每个线程都有自己的一个计数器,各个线程之间的计数器互不影响,这类内存区域被称为“线程私有”的内存
这个程序计数器记录的是正在执行的虚拟机字节码指令的地址,但是如果是native方法,则程序计数器的值为空(undefined)。
此内存区域是唯一一个在Java虚拟规范中没有规定任何OutOfMemoryError情况的区域。
详细介绍Java 虚拟机栈
与程序计数器一样,该空间也是线程私有的,其生命周期与线程相同。
虚拟机栈描述的是Java方法执行时的内存模型:每个方法执行时都会创建一个栈帧,该栈帧中存储方法的**局部变量表、操作数栈、动态链接、方法的出口等信息。**每个方法从调用到执行结束就对应着一个栈帧在虚拟机栈的入栈和出栈。
如果线程请求的栈深度大于虚拟允许的深度(递归调用方法太多时),将抛出 StakOverflowError异常。
如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
什么是栈帧
每个方法执行时都会创建一个栈帧,该栈帧中存储方法的**局部变量表、操作数栈、方法的出口、动态链接等等信息。
什么是局部变量表? 操作数栈?
局部变量表:局部变量表存放的是一个方法内部存储的局部变量和方法参数,该变量可以是基本类型(boolean、byte、char、short、int、float)也可以是引用类型(reference)和returnAddress类型 (returnAddress 中保存的是return后要执行的字节码的指令地址)。
操作数栈:和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
通过一个实例来了解局部变量和操作数栈:
来源
实例方法中的局部变量表
public class T {
private int a = 0;
public void add(int b,int c){
a = b + c;
}
}
原来,JVM在编译代码的时候,偷偷在局部变量表中添加了一个this引用(很明显this保存的实例的引用),这也是我们为什么可以在方法中访问实例中的成员变量的原因,如下
操作栈:
图中字节码的简要解释如下:
0)aload_0 将this的引用入栈 (aload_0即将局部变量表中索引为0的引用压到操作数栈中)
1)iload_1 将参数b入栈 (将局部变量中的索引为1的整数压到操作数栈中)
2)iload_2 将参数c入栈
此时栈的内容有(0为栈顶)
0.c
1.b
2.this
3)iadd 将栈顶的两个数相加,并将结果保存至栈顶,此时栈的内容为
0.b+c
1.this
4). putfield 将栈顶的两个值出栈,第一个值(b+c)赋值给第二个值(this)的对应的成员变量(是的,没错即使是赋值也要执行两次出栈操作)
动态链接: 在Java源文件被编译成字节码文件时所有方法引用都作为符号引用(Symbolic Reference)保存在方法区的常量池中。而在 实际调用过程中,会将符号引用转换为直接引用(该引用的实际内存地址),这一过程就是动态链接。
例如:Java源代码中Person per = new Person();符号引用为全类名:com.johnny.Person ,而在调用时会动态查找该符号引用对应的内存地址。
方法出口: 出口就是return,不正常的话就抛出异常。
垃圾回收算法
Java中会存在内存泄漏吗?
Java中可能存在内存泄漏情况。当长生命周期的对象持有端生命周期对象的引用时,就很可能发生内存泄漏。
Person p1 = new Person(“zs”,15);
public class Test01 {
static Vector v = new Vector(10);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Object obj = new Object();
v.add(obj);
obj = null;
}
}
p1是引用变量, 右边new Person(“zs”, 15)是对象。
一个对象可以拥有多个引用,一个引用只能同时绑定0个或1个对象。
Minor GC、 Major GC、 Full GC 是什么?
- MinorGC 是新生代GC,指的是发生在新生代的垃圾收集动作。由于 Java对象大都是朝生夕死的,所以MinorGC 非常频繁,一般回收速度也比较快。 (一般采用复制算法)
- Major GC 是老年代GC,指的是发生在老年代的垃圾收集动作。通常执行Major GC会连着Minor GC 一起执行。Major GC 的速度要比Minor GC 慢很多。(可采用标记-清除算法 和 标记-整理算法)
- Full GC 是清理整个堆空间,包括新生代和老年代。
Minor GC 、Major GC 的触发条件
Minor GC的触发条件一般为:
- Eden区满时,触发Minor GC 。
- 当新建的对象大小 大于 Eden所剩空间时触发Minor GC
Major GC 的触发条件 一般为:
- 晋升到老年代的对象大小超过了老年代剩余空间
- 堆内存分配了很大的对象
Full GC 的触发条件一般为:
- 执行Syste.gc()
- CMS GC 异常
为什么新生代要分Eden 和两个Survivor区域?
- **降低Major Gc 触发的次数。**如果没有Survivor ,Eden区每进行一次Minor GC ,存活对象就会被送到老年代。老年代很快就被填满,从而触发Major GC。而老年代的内存空间远大于新生代的内存空间,进行一次Major GC 消耗的时间远大于MinorGC。所以需要分Eden和Survivor区。
- 除此之外,Survivor 存在的意义,就是减少送往老年代 的对象,进而减少Major GC 的发生。Survivor的预筛选,保证了只有经历了15次Minnor GC 还能在新生代中存活的对象才会被送往老年代。
- 避免了空间碎片化。新建的对象在Eden区,经过一次Minnor GC,Eden区存活的对象就会被复制到Survivor Space(即S0)区域,Eden区被清空。等Eden区满了,就再触发一次MinorGC。这时Eden区存活的对象就会和s0区的对象复制到第二块Survivor Space(即S1)中,然后清空Eden和S0区。这种复制算法保证了S1来自S0和Eden两部分的存活对象占用连续的内存空间。
为什么要分代(新生代和老生代)?
主要原因就是可以根据各个年代的特点,对对象分区存储,更便于采用最合适的拉圾回收算法回收。
在新生代中: 每次垃圾回收都有大批对象死去,只有少数对象存活。因此只需要付出时候少量的存活对象复制成本就可以完成收集。所以采用复制算法。
而老年代因为对象存活率高,又没有额外的空间给予担保,就必须采用“标记- 清除”或者“标记-整理”算法。
对象存放过程:
- 数据首先会分配到Eden区(大对象则直接放入老年代)。当Eden没有足够的空间的时候就会触发JVM发起一次Minor GC。
- 如果对象经过一次Minor GC还存活且又能被Survivor 空间接受,那么将被移动到Survivor空间中,并将年龄设置为1。对象在Survivor每熬过一次Minor GC ,年龄就加1 。 当年龄达到一定程度(默认为15)时,就会被晋升到 老年代中。