深入Java虚拟机——JVM内存详解

在C++中,程序员拥有每一个对象的所有权,但与此同时还肩负着释放对象内存空间的责任;而Java由于有了虚拟机的帮助,程序员拥有对象的所有权的同时不再需要释放对象的内存空间。由于是JVM自动进行对象内存的释放,所以内存泄漏和内存溢出的问题也很少出现。

Java虚拟机在运行时将内存空间分成5个部分,分别是:方法区、虚拟机栈、本地方法栈、堆、程序计数器。

程序计数器

  • 本质 程序计数器本质上是一块较小的内存空间。
  • 作用 可以把程序计数器简单地看作是当前线程所执行的字节码的行号指示器。 字节码解释器在工作时,通过改变程序计数器的值来选取下一条需要执行的字节码指令。 除此之外,程序的分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖程序计数器来完成。
  • 特性 程序计数器是一个个线程私有内存。每个线程都私有一个程序计数器,用来记录线程当前执行的位置,好让线程恢复执行的时候知道上一次执行的位置。所以各个线程之间的计数器相互独立,互补影响。 若线程正在执行的是一个Java方法,那么当前线程的程序计数器记录的是正在执行的虚拟机字节码指令的地址;若线程正在执行的是一个Native方法,则这个计数器值为空。 PS:Native方法是指不使用Java语言写的方法,或者是使用Java语言写的直接操控计算机硬件的方法。

Java虚拟机栈

  • 是什么? JVM栈是用来描述Java方法执行过程的一个内存模型。 JVM栈中存放着一个个栈帧,每个栈帧都对应着一个Java方法。 当一个Java方法被执行时,JVM会在JVM栈中创建一个栈帧,用于存储:局部变量表、操作数栈、动态链接、方法出口等信息。 每一个Java方法被调用到执行完成的过程,都对应着一个个栈帧在JVM栈中的入栈和出栈。
  • 特性 JVM栈是线程私有的,每一个线程拥有一个独立的JVM栈,这个线程中所要执行的方法就会在它所对应的JVM栈中创建栈帧。
  • 通常所说的“堆“和“栈“ 有人把JVM所使用的内存分为栈内存和堆内存,这种分法比较粗糙。 这种分法中所说的“栈“其实仅仅是JVM栈中一个个栈帧中的局部变量表。
  • 栈帧中的局部变量表 局部变量表中存放着编译时期可知的各种基本数据类型、对象引用、returnAddress类型。 局部变量表所需的内存空间在编译时期完成分配,一个方法所需分配多大的局部变量表是完全确定的,局部变量表的大小在程序运行时不会改变。 PS:对象引用也叫做reference类型,它的本质是一个地址,并不是对象本身,不同的虚拟机这个地址指向的内容也不一样;这个指针可能指向一个对象的起始地址,也可能指向一个代表对象的句柄。 PS:returnAddress类型本质上是一个地址,这个地址指向一条字节码执行。
  • Java虚拟机栈可能可能产生的异常 若线程请求的栈深度大于虚拟机所允许的最大深度,就抛出StackOverFlow异常; 若虚拟机栈可以动态扩展,没有最大深度的限制,当内存已经用完,无法再扩展栈深度时,就抛出OutOfMemory异常。

本地方法栈

本地方法栈的特性和JVM栈的特性几乎一样,只不过JVM栈中存放的是Java方法的相关信息,而本地方法栈中存放的是本地方法的相关信息。 本地方法栈也会抛出OutOfMemoryError和StackOverFlow异常。

  • 是什么? 堆内存的唯一目的就是存放所有的对象实例。
  • 特性 堆内存是Java虚拟机所需要的内存中最大的一块内存。 它是被所有线程共享的一块内存区域。 堆内存物理上不一定要连续,只需要逻辑上连续即可。
  • 堆与垃圾回收的关系? 堆内存是垃圾回收的主要区域,所以也被称为GC堆。
  • 堆可能抛出的异常 若堆中的实例都是有用的,并且内存已经用完时,就会发生OutOfMemoryError异常。

方法区

  • 是什么? 方法区也是JVM需要使用的一块内存区域,它用来存储已经被JVM加载的类的信息、常量、静态变量、编译后的代码等数据。
  • 特性 方法区和堆一样是各个线程共享的内存区域。 它在物理上不需要连续的内存空间。 方法区的大小可以固定,也可以可扩展。 方法区可以不实现垃圾收集。
  • 可以不实现垃圾收集,那么方法区中的数据就永久存在吗? 不是的。方法区中的垃圾收集行为比较少见,但并非数据进入方法区就永久存在了。 方法区的内存回收的主要目标是对常量池的回收和对类型的卸载。
  • 方法区可能抛出的异常 当方法区无法满足内存分配需求的时候就抛出OutOfMemoryError异常。
  • 运行时常量池 a)是什么? 运行时常量池是方法区的一部分。 我们知道,.java文件被编译之后生成的.class文件中除了包含:类的版本、字段、方法、接口等信息外,还有一项就是常量池,常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。 PS:int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值; int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。 b)特性 .class文件中的常量池具有动态性。Java并不要求常量只能在编译时候产生,在运行时就不能增加了;Java允许在运行期间将新的常量放入方法区的运行时常量池中。 Java中String类中的intern方法就是采用了运行时常量池的动态性。当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。 c)可能抛出的异常 运行时常量池是方法区的一部分,所以会受到方法区内存的限制,因此当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。

直接内存

直接内存不是JVM规范中定义的内存区域,但在JVM的实际运行过程中会频繁地使用这块区域。而且也会导致OutOfMemoryError异常。 在JDK 1.4中新加入了NIO=New Input/Output类,引入了一种基于通道和缓冲区的IO方式,它可以使用本地函数直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据。这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据。 直接内存不受Java堆大小的限制,但仍然受本机总内存的限制。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java一日一条

Java中Volatile关键字详解

  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。...

391
来自专栏顶级程序员

死磕 Java 并发 :Java 内存模型之 happens-before

来源:chenssy, cmsblogs.com/?p=2102 那么我们正确使用同步、锁的情况下,线程A修改了变量a何时对线程B可见? 我们无法就所有场景...

3085
来自专栏java工会

java反射机制

1718
来自专栏java一日一条

从根源上解析 Java volatile 关键字的实现

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

242
来自专栏java架构师

【SQL Server】系统学习之二:索引优化

页大小8192个字节,行限制为8060字节(大型对象除外)。 包含varchar nvarchar varbinary sql_variant(8012,obj...

2306
来自专栏java一日一条

Java内存管理原理及内存区域详解

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。Java虚拟机所管理的内存将会包...

121
来自专栏JavaEE

Java的反射机制前言:Java反射的使用:总结:

1004
来自专栏java一日一条

Java内存管理原理及内存区域详解

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。Java虚拟机所管理的内存将会包...

81
来自专栏java系列博客

Java面试通关要点汇总集基础篇之参考答案

1384
来自专栏java一日一条

从根源上解析 Java volatile 关键字的实现

也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

151

扫描关注云+社区