JVM 1
初识JVM
JAVA虚拟机简介
JAVA虚拟机:Java Virtual Machine,简称JVM。JVM可理解为是一台被定制过的现实当中不存在的计算机,模拟硬件执行字节码指令。
虚拟机:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box
JVM和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
了解Java程序的编译过程
我们在深入了解JVM前,需要大致了解一下java程序的一个加载过程。大致如下:
首先,我们的java程序经过JDK编译器执行编译为class字节码文件,然后通过类加载机制把字节码加载到java进程的方法区,在堆中生成一个Class类对象,作为方法区类信息的访问入口,随后,java进程启动,这时候就会创建一个Java虚拟机,解释执行字节码指令(将字节码翻译为机器码),还存在JIT即时编译器(作用是把热点代码编译为机器码,之后就不用每次执行都翻译一遍,提高执行效率),最终还是申请系统调度CPU来执行机器码。
JAVA内存区域与内存溢出异常
运行时数据区域
JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建与销毁。一般来说,JVM所管理的内存将会包含以下几个运行时数据区域:
- 线程私有区域:程序计数器、Java虚拟机栈、本地方法栈。
- 线程共享区域:Java堆、方法区、运行时常量池。
什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存。
程序计数器(线程私有)
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器 (用来明确当线程切换出去后,要恢复时,下一个指令从哪个地方开始执行)。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(内存溢出)情况的区域!
Java虚拟机栈(线程私有)
虚拟机栈描述的是Java方法执行的内存模型 : 每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。java虚拟机栈的生命周期与线程相同,线程启动,虚拟机栈就创建,线程销毁,虚拟机栈就销毁。
我们一直讲的java内存区域划分中的栈区域实际上就是此处的虚拟机栈,再详细一点,是虚拟机栈中的局部变量表部分。
局部变量表 :
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用 (reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress类型(指向了一条字节码指令的地址)。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。
JAVA虚拟机栈会产生以下两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。(比如使用递归时,如果逻辑错误,就会报此错误)
- 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(内存溢出)异常。
本地方法栈(线程私有)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。与虚拟机栈一样,本地方法栈也会在栈深度溢 出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java堆(线程共享)
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块内存区域。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界 里“几乎”所有的对象实例都在这里分配内存。
如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB), 以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不 会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
Java堆是垃圾回收器管理的主要区域,因此很多时候可以称之为"GC堆"。根据JVM规范规定的内容,Java堆可以处于物理上不连续的内存空间中。Java堆在主流的虚拟机中都是可扩展的(-Xmx设置最大值,-Xms设置最小值)。如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM错误。
方法区(线程共享)
方法区与Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量(必须是static final修饰的)、静态变量、即时编译器编译后的代码等数据。在JDK8以前的HotSpot虚拟机中,方法区也被称为"永久代"(JDK8已经被元空间取代)。永久代并不意味着数据进入方法区就永久存在,此区域的内存回收主要是针对常量池的回收以及对类型的卸载。
JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。
注意:JDK1.7 之前,方法区就是永久代;JDK1.8之后,方法区变成了元空间。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(ConstantPoolTable),用于存放编译期生成的各种字面量与符号引用(这个地方指的是class文件常量池),这部分内容将在类加载后存放到方法区的运行时常量池中。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限 制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
JAVA堆内存溢出
当我们在进行创建变量,创建对象,类加载等操作时,需要在对应的内存区域分配一块内存空间。如果该区域内存不足,会先执行垃圾回收GC,如果GC之后内存还是不够,就会出现内存溢出(OOM)的情况。
解决方案:
- 1.优化代码,提高代码的空间复杂度。
- 2.在java进程启动时,加大对应内存的空间的分配额度。(需要考虑系统内存够不够分配的问题)。
- 3.如果方法2中系统内存不足,可以加大系统内存分配。
内存泄漏
内存中,随着进程运行时间越来越长,存放的无用的数据(变量/常量值,对象,类型)越来越多,可用内存空间越来越少,如果某一进程一直运行,随着运行时间越来越长,最终一定会出现某个内存区域空间不足的问题,导致出现OOM报错。
解决方案:
- (1)程序代码上优化:如设置超时时间,定时清理长期不用的数据(可以使用jvm的检测工具)。
- (2)临时方案:有时候有些老旧的大型项目,不太好优化(即使使用内存泄漏的检测工具,也不好定位。万能重启大法:隔一定时间,重启java进程。如果重启间隔的时间觉得太短,加内存(java进程内存,如果系统内存不满足java内存需求,还要加大系统内存)。
内存泄漏,随着使用时间越来越长,最终一定出现OOM,但是OOM不是一定由内存泄漏引起~~