热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【深入学习JVM01】运行时数据区域划分

前言在使用c进行编程时,我们通过new创建的每一个对象都需要有对应的delete操作去释放对象所占用的内存,对内存的掌控度比较高,但是程序

前言

在使用c++进行编程时,我们通过new创建的每一个对象都需要有对应的delete操作去释放对象所占用的内存,对内存的掌控度比较高,但是程序员需要知道对象什么时候不需要使用了,并需要手动释放内存,如果忘记了delete释放,很容易出现内存泄漏(申请内存后,没有释放,会一直占用着)和内存溢出(因为过多的内存泄漏导致无法申请足够的内存,即out of memory)的问题。

相比之下,java虚拟机提供了自动内存管理机制,java程序员可以解放双手,不再需要去写delete等手动释放内存的代码,虚拟机会自动将内存中无用的对象占用的内存释放。

了解jvm的必要

虽然有自动内存管理机制的存在,但是不代表写的每个java程序都不存在内存泄漏和内存溢出问题,我们需要对虚拟机有足够的了解,才能在发生内存泄露和内存溢出的时候有效地排查问题。

本文将对jvm虚拟机运行时内存进行一个基本的介绍,后续的文章也会讲解jvm其他知识,大部分都是自己的读书总结加上自己的理解。希望将自己的所学进行总结的同时能惠及他人,如果有什么地方讲的不对,希望各位同学能够指出。

内存划分

java虚拟机将其管理的内存划分为以下几块:

  • 程序计数器 (PC Register)
  • 虚拟机栈 (JVM Stack)
  • 本地方法栈 (Native Method Stack)
  • 堆 (Heap)
  • 方法区 (Method Area)

各个区域都有其各自的特点和作用,以及不同的创建和销毁的时间

各个区域的介绍

程序计数器

  • 描述
    • 程序计数器是一个较小的内存区域
  • 作用
    • 记录着当前线程所执行的字节码行号
    • 字节码解释器在工作的时候,通过改变这个计数器的值来选取下一条要执行的字节码指令。
    • 分支,循环,跳转,异常处理,线程恢复等功能都需要使用到这个程序计数器。
  • 特点
    • 线程私有--每个线程都有一个独立的程序计数器。
    • 如果当前线程正在执行一个java方法,这程序计数器的值为虚拟机字节码指令的地址,如果执行的是一个Native 方法,这个计数器的值则为空。
    • 程序计数器是唯一没有规定OutOfMemoryError的内存区域
  • 创建时间
    • 每个线程启动的时候会创建一个较小的内存区域作为线程的程序计数器
  • 销毁时间
    • 线程结束时会释放该内存区域

扩展问题1:为什么需要程序计数器?

java虚拟机的多线程是通过线程轮转,分配CPU时间片来执行java程序,当线程切换时,为了能够回到原来的字节码执行位置继续程序的执行,所以每个线程会有一个程序计数器。

扩展问题2:Native方法是什么?

java程序执行的时候调用的方法,有些是用java语言实现的,有些是用其他语言编写实现的,用其他语言实现的方法称为Native方法本地方法,native方法会使用native关键字进行标注,如Object类的getClass()方法:

public class Object {public final native Class getClass();...
}

由于native方法不是java实现的,也就没有字节码行号之说,此时程序计数器的值应当为空(undefined)。

虚拟机栈

  • 描述
    • 虚拟机栈是描述java方法执行过程的一个内存模型
    • 具体描述:每个方法在执行的时候都会创建一个栈帧,栈帧中存储的是java方法的局部变量表、操作数栈、动态链接、方法出口等信息。java程序在执行的时候每调用一个java方法都会对应的创建栈帧并压入虚拟机栈中,当方法执行完毕,又会将栈帧从虚拟机栈中弹出。虚拟机栈就是栈帧存放的一个栈结构的内存区域。
  • 作用
    • 描述java方法执行的过程,保存栈帧。
  • 特点
    • 线程私有
    • 此区域可能会有两种内存异常情况:
      • 当栈的深度大于虚拟机所限制的最大深度,会抛出StackOverflowError异常。
      • 如果虚拟机栈动态扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 创建时间
    • 线程启动的时候
  • 销毁时间
    • 线程结束的时候

扩展1:局部变量表

局部变量表用于存放编译期可知的各种基本数据类型、对象引用、returnAddress类型(一条字节码指令的地址)

对于基本数据类型,存放的是变量的名和值;

对于引用类型,存放的是指向对象在堆中的起始地址。

ps: 对于64位的long或double类型的局部变量会占用两个局部变量表空间(Slot),其余的数据类型都是只占用一个局部变量表空间。

局部变量表所需要的空间在编译期间已经计算好了,在一个方法执行时,需要为栈帧分配多少局部变量表空间是完全确定的

本地方法栈

本地方法栈的特性和虚拟机栈几乎一样。

  • 本地方法栈与虚拟机栈的区别
    • 本地方法栈为本地方法服务
  • 本地方法栈可能出现的异常
    • 同虚拟机栈一样可能抛出StackOverflowErrorOutOfMemoryError 异常。

  • 描述
    • 堆内存的唯一目的是存放对象实例
    • 堆内存是垃圾收集的主要区域,因此也叫GC堆
  • 作用
    • 存放对象实例
  • 特点
    • 虚拟机所管理的内存中最大的一块
    • 几乎所有的对象都在堆区分配内存,当然也有例外,JIT编译器有可能会进行优化,直接在栈上分配,有关信息可以直接搜索“逃逸分析”了解,这不在本文的讨论范围内。
    • 所有线程共享的一块内存区域
    • 堆内存在物理上不一定是连续的,保证逻辑连续即可
    • 堆内存区域无法满足分配对象实例所需内存,可能抛出OutOfMemoryError异常
    • 堆内存设置固定大小也可以动态扩展,可在启动参数上指定最小大小及扩容的上限。
  • 创建时间
    • 虚拟机启动的时候就创建了堆内存

扩展1: 堆区细分

jvm为了垃圾回收的方便,将堆划分为新生代老年代,新创建的对象基本上都放在新生代中,而存活比较久的对象则会移到老年代中。新生代和老年代采用不同的垃圾收集算法,可以更高效地回收内存。采用复制算法的新生代还可以细分为EdenFrom SurvivorTo Survivor。具体的详情是怎样的,为了不偏离这篇文章的主旨,这里先打个问号,后序的文章将会详细介绍堆区的几个划分的用途。

堆区虽然是线程共享的,但是如果设定了启动参数-XX:+UseTLAB,则开启了本地线程分配缓冲(Thread local Allocation Buffer, TLAB),会为每个线程单独在堆中划分出一个TLAB,哪个线程需要分配内存,就先在该线程对应的TLAB中分配内存,当TLAB用完,才在堆区的Eden中继续申请一块TLAB

方法区

方法区是用于存放虚拟机加载的类信息、常量、静态变量、编译后的代码等数据。

方法区特点:

  • 线程共享
  • 方法区大小可固定也可以动态扩展。
  • 与堆区一样不需要连续的物理内存,但要求逻辑连续。
  • 该区域的垃圾收集目标主要是针对运行时常量池的回收和对类进行卸载
  • 可能出现OutOfMemoryError异常。

扩展1:运行时常量池:

class文件中有个常量池,运行时常量池就是class文件中常量池经过类加载后存放的内存区域。

常量池主要存放两类常量:字面量和符号引用。

字面量指字符串,声明为final的常量值等;而符号引用是java编译后生成的各种常量,其包括:

  • 类和接口的全限定名
  • 成员变量的名称和描述符
  • 方法的名称和描述符

jdk1.8之前,方法区是用永久代实现的, 在jdk1.7以下的版本,运行时常量池是方法区的一部分,而jdk1.7及之后的版本,运行时常量池中的字符串常量池已经不在方法区,而是在java堆中开辟了一块区域作为字符串常量池。

在jdk1.8开始,已经没有永久代的概念,譬如符号引用(Symbols)转移到了native 堆中的元空间;字面量也在 java heap;类的静态变量(class statics)转移到了java heap

扩展2:常量是否只能在编译期产生? 否,运行期也可能将新的常量放入运行时常量池中,比如Stringintern方法。在jdk1.7的表现如下:

// 如果运行时常量池中,存在"10"这个字符串常量
// 则将常量池中的字符串对象返回,
// 如果不存在,则直接在运行时常量池中创建“10"这个字符串,并将其返回。
String s = String.valueOf(10).intern();

直接内存

前面讲的几块都属于虚拟机管理的运行时数据区域,java程序中也有可能会用到不是虚拟机运行时内存区域的一部分。这块内存我们通常称为直接内存

  • 直接内存不受java堆大小的限制,但是受本机物理内存的限制。

  • 直接内存也可能导致出现OutOfMemoryError异常。

直接内存的例子: jdk 1.4 加入的NIO类,引入了一种基于通道Channel和缓冲区Buffer的IO方式。直接通过Native方法在java堆外的直接内存中分配内存, 通过存储在java堆中的DirectByteBuffer对象作为这块直接内存的引用。操作DirectByteBuffer即可操作直接内存,这样做的好处是避免了要使用直接内存的时候需要先复制到java堆中。直接操作直接内存更加高效。

转:https://juejin.im/post/5b8372d96fb9a019c771656a



推荐阅读
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • 本文介绍了在Mac上搭建php环境后无法使用localhost连接mysql的问题,并通过将localhost替换为127.0.0.1或本机IP解决了该问题。文章解释了localhost和127.0.0.1的区别,指出了使用socket方式连接导致连接失败的原因。此外,还提供了相关链接供读者深入了解。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
author-avatar
万象新动HR
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有