1. 什么是jvm?
- jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
- jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
- JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
2. JDK、JRE、JVM是什么关系?
我们编写Java程序是依赖JDK的,而运行环境是JRE,现在我们又来讲JVM,那么他们是什么关系呢?先来看下面这张图:
![](https://img.haomeiwen.com/i22972070/ed46ca6c375a2b67.png)
可以看出,
-
JDK - Java Development Kit
(Java开发工具),是开发者用来编译、调试程序用的开发包,提供了编译和运行Java程序所需的各种资源和工具,JDK也是JAVA程序需要在JRE上运行,包括:JRE + java api + java开发工具 -
JRE - Java Runtime Environment
(Java运行环境),也就是java平台。所有的java程序都要在JRE环境下才能运行,包括:Java虚拟机+java的核心类库 -
JVM - Java Virtual Machine
(Java虚拟机),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java语言最重要的特点就是跨平台运行。就是依赖JVM使其与操作系统无关,实现跨平台。
3. JVM的生命周期
-
JVM实例
对应的是一个独立运行的java程序,它是进程级别的,多个JVM可以同时启动,在任务管理器中可以看到多个进程。- 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有
public static voidmain(String[] args)
函数的class都可以作为JVM实例运行的起点 - 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程
- 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
- 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有
-
JVM执行引擎实例
则对应了属于用户运行程序的线程,是线程级别的
4. JVM内存模型
《深入理解Java虚拟机》中的描述是下面这个样子的:
![](https://img.haomeiwen.com/i22972070/5e1c5454bea5428e.png)
可以看出,大致分成了5部分:方法区、堆、虚拟机栈、本地方法栈和程序计数器。
4.1 程序计数器(PC寄存器)
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
如果执行的是java方法,那么记录的是正在执行的虚拟机字节码指令的地址的地址,如果是native方法,计数器的值为空(undefined)。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
4.2 本地方法栈 - Native Method Stacks
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务(与Java环境外交互、与操作系统交互等)
4.3 虚拟机栈
每启动一个线程,JVM 就会在栈空间分配对应的线程栈, 比如 1MB 的空间(-Xss1m)。线程栈也叫做 Java 方法栈。每个线程会有一个私有的栈。
线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B调用 C。。。每执行到一个方法,就会创建对应的 栈帧(Frame)。
Java栈中存放的是一个个的栈帧,栈帧中存放的内容包括:局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
![](https://img.haomeiwen.com/i22972070/9e7f3803defa32a4.png)
注意:在我们通常使用的Sun jdk中,本地方法栈和虚拟机栈是一样的,统一叫栈。因此,都是线程私有的,可以用-Xss控制每个线程的大小,都会抛出StackOverflowError 和OutOfMemoryError异常。
4.4 堆 Heap
堆的作用是存放对象实例和数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。堆是java虚拟机最大的内存区域,也是垃圾收集器管理的主要区域。如果堆中没有可用内存,并且堆也无法再扩展时,可能会抛出OutOfMemoryError异常。
从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden 空间、 Survivor space 0(s0)、To Survivor space 1(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系。而且,Survivor区总有一个是空的,是为GC机制准备的空间。简易图示如下:
![](https://img.haomeiwen.com/i22972070/dff7162fd2aad9b3.png)
在堆中,默认各部分的占比大致如下:
- 老年代:2/3的堆空间
- 年轻代:1/3的堆空间
- eden区:8/10 的年轻代
- survivor0: 1/10 的年轻代
- survivor1:1/10的年轻代
另外,可以使用命令设置内存大小:
- -Xms设置堆的最小空间大小
- -Xmx设置堆的最大空间大小
- -XX:NewSize设置新生代最小空间大小
- -XX:MaxNewSize设置新生代最大空间大小。
4.5 方法区
方法区与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
方法区在Java8以前叫持久代(永久代, Permanentgeneration),-XX:PermSize 设置最小空间 -XX:MaxPermSize 设置最大空间。
在Java8中方法区换了个名字叫 Metaspace。可以使用参数-XX:MaxMetaspaceSzie设定大小,如果不指定大小,默认情况下,虚拟机会耗尽可用系统内存
总结:
-
在我们调优JVM时,最主要的包括3方面:栈(包括上面的虚拟机栈和本地方法栈)、堆(分为新生代和老年代)和 方法区(也叫Metaspace),图示如下
JVM
-
- 各个部分在启动是都可以用命令设置。栈:-Xss;堆: -Xms设置堆的最小空间大小, -Xmx设置堆的最大空间大小, -XX:NewSize设置新生代最小空间大小, -XX:MaxNewSize设置新生代最大空间大小;Metaspace:-XX:MaxMetaspaceSzie
网友评论