JAVA虚拟机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虚拟机栈会产生以下两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。(比如使用递归时,如果逻辑错误,就会报此错误)
  2. 虚拟机在动态扩展时无法申请到足够的内存,会抛出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不是一定由内存泄漏引起~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值