JVM: JVM 内存划分

简介: JVM: JVM 内存划分

概述


如果在大学里学过或者在工作中使用过 C 或者 C++ 的读者一定会发现这两门语言的内存管理机制与 Java 的不同。在使用 C 或者 C++ 编程时,程序员需要手动的去管理和维护内存,就是说需要手动的清除那些不需要的对象,否则就会出现内存泄漏与内存溢出的问题。如果你使用 Java 语言去开发,你就会发现大多数情况下你不用去关心无用对象的回收与内存的管理,因为这一切 JVM 虚拟机已经帮我们做好了。了解 JVM 内存的各个区域将有助于我们深入了解它的管理机制,避免出现内存相关的问题和高效的解决问题。


引出问题


在 Java 编程时我们会用到许多不同类型的数据,比如临时变量、静态变量、对象、方法、类等等。那么他们的存储方式有什么不同吗?或者说他们存在哪?


运行时数据区域


Java 虚拟机在执行 Java 程序过程中会把它所管理的内存分为若干个不同的数据区域,各自有各自的用途。56b767a4b448de733443b5a0ad4b11a5_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg这其中堆和方法区是线程之间共享的,而栈和程序计数器是线程私有的。

  • 程序计数器
    线程私有的,可以看作是当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
    这时唯一一个没有规定任何 OOM 异常的区域。
  • 虚拟机栈
    虚拟机栈也是线程私有的,生命周期与线程相同。栈里面存储的是方法的局部变量对象的引用等等。
    在这片区域中,规定了两种异常情况,当线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。当虚拟机栈动态扩展无法申请到足够的内存时会抛出 OOM 异常。
  • 本地方法栈
    和虚拟机栈的作用相同,只不过它是为 Native 方法服务。HotSpot 虚拟机直接将虚拟机栈和本地方法栈合二为一了。

  • 堆是 Java 虚拟机所管理内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域唯一的作用就是存放对象实例,也就是 NEW 出来的对象。这个区域也是 Java 垃圾收集器的主要作用区域。
    当堆的大小再也无法扩展时,将会抛出 OOM 异常。
    可以说,此内存区域唯一的作用就是存放对象实例,几乎所有的对象实例和数组都在这里分配内存
    Java 堆是垃圾收集管理的主要区域,因此也被称为 GC 堆。垃圾收集都采用分代垃圾回收算法,所以 Java 堆还可以细分:新声代(再细致一点分为 Eden,From Survivor,To Survivor)和老年代。进一步划分的目的是跟好地回收内存,或者更快地分配内存。

  • ec9be3d3226aef92cb36e5b7fe604667_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png
  • 方法区
    方法区也是线程共享的内存区域,用于存储已经被虚拟机加载的类信息常量静态变量等等。当方法区无法满足内存分配需求时,会抛出 OOM 异常。这个区域也被称为永久代。


补充


虽然上面的图里没有运行时常量池和直接内存,但是这两部分也是我们开发时经常接触的。所以给大家补充出来。

  • 运行时常量池
    运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。也会抛出 OOM 异常。
  • 直接内存
    直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是却是NIO 操作时会直接使用的一块内存,虽然不受虚拟机参数限制,但是还是会受到本机总内存的限制,会抛出 OOM 异常。

这里有一个概念希望大家能够清除,堆中使用分代垃圾回收算法时的永久代表方法区,它并不在堆内存中,上面的图片将其放在一起是为了说明分代垃圾回收算法会作用在这几个区域


JDK 1.8 的改变


对于方法区,它是线程共享的,主要用于存储类的信息,常量池,方法数据,方法代码等。我们称这个区域为永久代。它也是 JVM 垃圾回收作用的区域。大部分程序员应该都见过 java.lang.OutOfMemoryError:PermGen space 异常,这里的 PermGen space 其实指的就是方法区。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出,典型的场景是在 JSP 页面比较多的情况,容易出现永久代内存溢出。在 JDK 1.8 中,HotSpot 虚拟机已经没有 PermGen space 方法区这个地方了,取而代之的是一个叫 Metaspace(元空间)的东西。9d45898d976e658595df133c182e88b7_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

元空间与方法区最大的区别是:元空间不再虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制。常量区原本在方法区中,现在方法区被移除了,所以常量池被放倒了堆中。这样做的好处是:这样更改的好处:

  • 字符串常量存在方法区中,容易出现性能问题和内存溢出。
  • 类和方法的信息等比较难确定大小,因此对于方法区大小的指定比较困难,太小容易出现方法区溢出,太大容易导致堆的空间不足。
  • 方法区的垃圾回收会带来不必要的复杂度,并且回收效率偏低(垃圾回收会在下一章给大家介绍)。


虚拟机对象揭秘


对象的创建过程,最好是能记住,并且能知道每一步在做什么。3127b8d23b327819bda1767cd436942a_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

  1. 类加载检查:虚拟机遇到一条 new 指令的时候,首先去检查这个指令的参数能否在常量池中定位这个类的符号饮用,检查这个类的符号引用所代表的类是否已被加载,解析,初始化过。如果没有,那必须先执行响应的类加载过程。简单来说,就是要看对象的类是否已经被加载过了
  2. 分配内存:在类加载检查通过后,接下来虚拟机将会为新生对象分配内存。对象所需的内存大小在类加载完毕后便可以确定了,为对象分配空间的任务相当于把一块确定大小的内存从 Java 堆中划分出来。
    分配方式有指针碰撞空闲列表两种。选择那种方式由 Java 堆是否规整决定,而 Java 堆是否规整由垃圾收集器是否带有压缩功能决定(复制算法和标记整理算法是规整的,标记清除算法是不规整的)。

    内存分配并发问题
  3. a36ce2278ed99ba9e37bac9bb38ef854_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png
  • CAS 失败重试,CAS 是客观锁的一种实现方式。
  • TLAB:为每一个线程预先在 Eden 分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,如果不够,使用 CAS 进行分配。
  1. 初始化零值:内存分配完毕后,虚拟机将要分配的内存空间都初始化为零值(不包括对象头)。这一步保证了对象实例在 Java 中不赋初值就可以直接使用。
  2. 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置。比如对象的哈希码,对象的 GC 分代年龄信息,偏向锁,这些信息放在对象头中。
  3. 执行 init 方法:上面工作完成后,从虚拟机视角看,一个新的对象已经产生了。然后执行 init 方法,按照程序员的意愿将对象进行初始化。


对象构成


HotSpot 虚拟机中,对象在内存中的布局可以分为三块区域:对象头,实例数据和对齐填充。对象头中包含两部分信息,第一部分用于存储对象自身运行时数据(哈希码,GC 分代年龄,锁状态标志),另一部分是类型指针,即指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。实例数据部分存储的对象的有效信息。对其填充起到的是占位的作用。


对象的访问定位


  1. 句柄。
  2. 直接指针。


补充


String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false

这两种方式创建的对象是有差别的,第一种方式是在常量池中,第二种方式是在堆内存中。

9c1c596b128b648735dc53e78e080f64_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png直接使用双引号声明创建出来的 String 对象会直接存储在常量池中。如果不是使用常量池声明的 String 对象,可以使用 String 提供的 intern 方String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。

String s1 = new String("计算机");
String s2 = s1.intern();
String s3 = "计算机";
System.out.println(s2);//计算机
System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
System.out.println(s3 == s2);//true,因为两个都是常量池中的String对
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象     
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

String s1 = new String("abc"); 这句话创建了几个对象?

先有字符串 “abc” 放入常量池,然后 new 了一个字符串 “abc” 放入 Java 堆。栈中的引用指向堆中的对象。Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean。除了 Boolean 之外的 5 种包装类都默认创建了 【-128 127】的缓存数据,超出此范围仍然去创建新的对象。Float 和 Double 并没有实现常量池技术。

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出false

Integer i1=40;Java 在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。

  • Integer i1 = new Integer(40);这种情况下会创建新的对象。
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出false

相关文章
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
6天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
18 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你
|
2月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
2月前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
2月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
2月前
|
存储 安全 Java
JVM内存结构
这篇文章详细介绍了Java虚拟机(JVM)的内存结构,包括类的加载过程、类加载器的双亲委派机制、沙箱安全机制、程序计数器、Java栈、Java堆、本地方法和本地方法栈等关键组件及其作用。
JVM内存结构
|
3月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
3月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
|
3月前
|
Java
Jinfo 查看 jvm 配置及使用 Jstat 查看堆内存使用与垃圾回收
Jinfo 查看 jvm 配置及使用 Jstat 查看堆内存使用与垃圾回收
56 5

热门文章

最新文章

下一篇
无影云桌面