目录
一、基本概念
- JVM是可运行Java代码的假想计算机,包括一套字节码指令集、一个垃圾回收、一组寄存器、一个栈、一个堆和一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。
- 我们都知道Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码。也就是如下:
①Java源文件—->编译器—->字节码文件
②字节码文件—->JVM—->机器码
- 每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
二、运行时数据区域划分
- 根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
- JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA堆、方法区】、直接内存。
- 线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
- 线程共享区域随虚拟机的启动/关闭而创建/销毁。
注:有填充色为线程共享区,无填充色为线程隔离的数据区
三、运行时数据区各部分区域存储的数据:
3.1、程序计数器
3.1.1 作用:一块较小的存储空间,可以看作是当前线程所执行的字节码行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条药执行的指令;
3.1.2 线程私有空间:为了线程切换后恢复到正确的位置,确保各计数器间互不影响,独立存储;
3.1.3 存储:指令,如果正在执行一个java方法在,则存储正在执行虚拟机字节码指令的地址;如果执行的是native方法计数器存储的是Undefined
3.1.4 异常类型:无
3.2、Java虚拟机栈
3.2.1作用:每个方法执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用至执行完毕的过程,就对应一个栈帧在虚拟机栈中 从入栈到出栈的过程
3.2.2线程私有空间,生命周期与线程相同
3.2.3存储:局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用、方法返回地址(Return Address)和一些额外的附加信息:如下图
局部变量表:就是用来存储方法中声明的变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用及returnAddress(指向了一条字节码指令的地址),存储空间以局部变量槽slot来表示(除long与double占用两个,其余类型均占用一个)。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈:栈最典型的一个应用就是用来对表达式求值。
指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
3.2.4异常:StackOverFlowError:线程请求栈的深度大于大于虚拟允许的深度
OutOfMemoryError:当栈扩展时无法申请到足够的内存时抛出。
3.3、本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一,异常类型同Java虚拟机栈。
3.4、Java堆
Java堆是虚拟机管理内存中所占用最大的一块,虚拟机启动时创建,唯一的目就是存放实例对象,Java堆是垃圾收集管理的内存区域,因此也称为GC堆。异常类型 OutOfMemoryError,线程共享。
3.4.1、新生代
是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden区、ServivorFrom、ServivorTo三个区。
3.4.1.1.Eden区
Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
3.4.1.2.ServivorFrom
上一次GC的幸存者,作为这一次GC的被扫描者。
3.4.1.3.ServivorTo
保留了一次MinorGC过程中的幸存者。
3.4.1.4.MinorGC的过程(复制->清空->互换)
MinorGC采用复制算法。
1:eden、servicorFrom 复制到ServivorTo,年龄+1
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);
2:清空eden、servicorFrom
然后,清空Eden和ServicorFrom中的对象;
3:ServicorTo和ServicorFrom互换
最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
3.4.2、老年代
- 主要存放应用程序中生命周期长的内存对象。
- 老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
- MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
3.4.3、永久代
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
注:JAVA8与元数据
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
3.5、方法区
方法区域java堆一样,各个线程共享的区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码换缓存等数据。到JDK8废弃永久代用元空间代替。运行时常量也是方法区的一部分,用于存放在编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
注1:本机内存(Direct Memory)不是虚拟机运行时数据的一部分,但是频繁使用,而且也可能导致出现OutOfMemoryError,因为JDK1.4引入NIO提供了基于Channel与Buffer的IO方式,可以使用Native函数库直接访问堆外内存,堆外内存不受java堆大小限制,但是会受到本机内存大小的限制,所以jvm设置-Xmx时应该预留部分内存,防止出现OutOfMemoryError。
注2:常见的参数如下:
-Xms64m
最小堆内存64m
.-Xmx128m
最大堆内存128m
.-XX:NewSize=30m
新生代初始化大小为30m
.-XX:MaxNewSize=40m
新生代最大大小为40m
.-Xss=256k
线程栈大小。-XX:+PrintHeapAtGC
当发生 GC 时打印内存布局。-XX:+HeapDumpOnOutOfMemoryError
发送内存溢出时 dump 内存。
新生代和老年代的默认比例为 1:2
,也就是说新生代占用 1/3
的堆内存,而老年代占用 2/3
的堆内存。
可以通过参数 -XX:NewRatio=2
来设置老年代/新生代的比例。
四、线程
这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的CPU 上。当原生线程初始化完毕,就会调用Java 线程的run() 方法。当线程结束时,会释放原生线程和Java 线程的所有资源。
- HotspotJVM 后台运行的系统线程主要有下面几个:
虚拟机线程(VM thread) | 这个线程等待JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当 堆修改无法进行时,线程都需要JVM 位于安全点。这些操作的类型有:stop-the- world 垃圾回收、线程栈dump、线程暂停、线程偏向锁(biased locking)解除。 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。 |
GC 线程 | 这些线程支持JVM 中不同的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到JVM 的信号并调用适当的JVM 方法处理。 |