Java虚拟机内存区域详解

简介: 学习Java的人,都听说过Java虚拟机,也叫JVM,估计也就停留在这里的,(我也差不多,刚开始)。Java语言的诞生在1995年(我出生)Java发布了第一个版本Java1.0,这个时候Java喊出了一句口号 Write Once,Run AnyWhere 一次编写,

更多文章 访问我的博客:http://www.caoyong.xin:8080/blogger 

Java虚拟机内存区域详解

半年前买了一本深入理解Java虚拟机,买了就放在那里去了,期间拿出来想研究一下,还没有看一会,哇 !脑袋疼。也就又放回原处,这段时间事情不多,自己也静下心来,看看这本被誉为佳作的书。


目录结构

    1:Java虚拟机介绍

    2:内存区域介绍

        2.1:程序计数器

        2.2:Java虚拟机栈

        2.3:Java堆

        2.4:方法区

        2.5:本地方法栈

    3:对象的创建(转载)


1:Java虚拟机介绍

    学习Java的人,都听说过Java虚拟机,也叫JVM,估计也就停留在这里的,(我也差不多,刚开始)。Java语言的诞生在1995年(我出生)Java发布了第一个版本Java1.0,这个时候Java喊出了一句口号 Write Once,Run AnyWhere 一次编写,随处运行 而那个时候的Java虚拟机是 Sun Classic VM 。到2000年的时候Java1.3,就把HotSpot作为了一直沿用至今的Java虚拟机。所以我们现在用大部分Java虚拟机都是HotSpot 


2:内存区域介绍

        Java虚拟机在执行Java程序的时候会把内存分为几个数据区域,看下面的图,介绍了Java虚拟机的运行时数据区的划分

    QQ截图20180412135323.png

    下面我们就来一一介绍这些数据区域

    2.1:程序计数器

    程序计数器是一块较小的内存区域,在java的字节码解析器当中,需要辨别当前的字节码解析到了哪个地方,同时需要来控制程序的流程,如果在程序当中没有一个东西来记录当前程序执行到哪个,同时下一步应该执行哪一步操作例如:分支、循环、跳转、异常处理等操作都不是按照原本程序书写的顺序来执行的,所以为了能够引导程序的运行,就需要引进一个用来引导字节码解析顺序的东西,就叫做程序计数器。

        Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。如果一个线程运行一半,就被挂起,等待另一个线程执行完毕后在接着执行。为了线程切换后可以正确的恢复到原来执行的位置,所以每个线程都应该有一个独立的程序计数器,也就是说程序计数器这一块内存区域是私有的。也就叫"线程私有"。

        还有一点,如果线程正在执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址。如果执行的native方法,计数器当中的内容应当是空。 还有此内存区域在java的虚拟机规范当中是唯一一个没有规定OutOfMemoryError(内存溢出错误)的区域。

     2.2:Java虚拟机栈

    也叫Java栈,他也是线程私有的,Java虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会在Java虚拟机栈中创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方发从调入到执行完毕,也就对应这栈帧进栈到出栈的过程。

      局部变量表存放着编译期可知的各种基本数据类型和对象的引用,我们通常说的Java栈存放对象引用,Java堆存放对象实例,现在应该明白了具体存放哪里了。

QQ截图20180412145340.png

Java虚拟机栈有两种异常状况

    第一种 线程请求的栈深度大于最大可用深度,则抛出stackOverflowError;

    第二种栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。

   2.3:Java堆

    Java堆是Java虚拟机所管理的最大的一块内存区域,Java堆是线程共享的一块区域,当然Java堆也是垃圾收集器管理的主要区域,可以分为新生代和老年代(tenured)。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。新生代又可进一步细分为eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。

QQ截图20180412150950.png


2.4:方法区

        方法区也是各个线程共享的内存区域,它 用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码数据。

       方法区中有三个池(jdk1.6之前),

        QQ截图20180412152907.png

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。

  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。

  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。


  1. 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。

  2. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

  3. 字面量:文本字符串、声明为final的常量值等;

  4. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

      JDK1.6之前字符串常量池位于方法区之中。 
      JDK1.7字符串常量池已经被挪到堆之中。

2.5:本地方法栈

和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C实现的。Java底层会调用C编写的的类库中的方法,在Java中调用本地方法使用native关键词。而本地方法栈就是管理这些本地方法的。


下面这一部分是对象在Java虚拟机创建的一系列过程,看到有位博主写了篇关于这部分的内容,所以就转载一下。写的很具体

转载:https://blog.csdn.net/sc313121000/article/details/50819741

一、对象的创建

new Animal();

1.类加载检查:

检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。

2.为对象分配内存

对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

2.1根据Java堆中是否规整有两种内存的分配方式:

(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)

指针碰撞(Bump the pointer):

Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。

空闲列表(Free List):

Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。

2.2分配内存时解决并发问题的两种方案:

对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

对分配内存空间的动作进行同步处理—实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性; 
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

3.内存空间初始化

虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。 
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.对象设置

虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

5.init

在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。 
但是从Java程序的角度看,对象的创建才刚刚开始方法还没有执行,所有的字段都还是零。 
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

二、对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

1.对象头:

HotSpot虚拟机的对象头包括两部分信息。

1.1 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

HotSpot虚拟机对象头Mark Word

1.2 另外一个部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考 三对象的访问定位) 
QQ截图20180412155353.png

2.实例数据:

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类 
中继承下来的,还是在子类中定义的,都需要记录下来。 
HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。

3.对齐填充:

对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。 
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

三、对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的引用数据来操作堆上的具体对象。 
对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。

使用句柄: 
如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

QQ截图20180412155403.png


通过句柄访问对象

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针: 
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而引用中存储的直接就是对象地址。 

QQ截图20180412155411.png

通过直接指针访问对象

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。(例如HotSpot)

目录
相关文章
|
11天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
103 1
|
20天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
32 4
|
20天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
44 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
10天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
33 10
|
6天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
20 1
|
9天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
16天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
18天前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
29 3
|
18天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
31 2
|
19天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
45 2