【JVM最全系列】一、JVM内存模型

发布于:2023-01-26 ⋅ 阅读:(604) ⋅ 点赞:(0)

我是强哥,一个在互联网苟且的男人,微信关注搜索【光头强编程】,跟强哥一起学习更多内容


前言

围城有一句话,叫做“婚姻是一座围城,城外的人想进去,城里的人想出来”,其实语言的内存分配和回收也是一样的,java和C/C++中间就隔着这一堵墙。

一、概述

C/C++的程序开发者掌握着对象的分配和生死大权,既维护着每一个对象的生存,又掌控着对象的回收,肩负着每一个对象生命从开始到终结的维护责任。

二、内存模型

这里的内存模型是最新的内存模型,1.8版本的内存模型同1.7相比较,最大的差别就是原空间取代了永久代。圆孔的本质和永久代类似,都是堆JVM规范中方法区的实现。元空间与永久代之间最大的区别在于:元空间并不存在虚拟机中,而是使用本地内存

在这里插入图片描述
(图片来源于网络)

  • jvm虚拟机内存模型主要分析的角度是从”运行时数据区结构“ 进行分析
  • 1.7的jvm内存模型:程序计数器、java虚拟机栈、本地方法栈、java堆、方法区、运行时常量池、直接内存
  • 1.8后的jvm内存模型:程序计数器、java虚拟机栈、本地方法栈、java堆、元空间、运行时常量池、直接内存

1.程序计数器

程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机多线程是通过线程的轮流切换并且分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了切换线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的 程序计数器,各条线程程序计数器互不影响,独立存储,因此是线程独享。线程私有内容

2.Java虚拟机栈

与程序计数器一样,都是线程私有的,生命周期和线程的生命周期相同。虚拟机栈描述的是java方法执行的内存模型;每个方法执行的时候都会创建一个栈帧,存储“局部变量表”“操作数栈”“动态链接”“方法出口等信息”,一个方法对应着一个栈帧从入栈道出栈的过程。
在日常中,经常性的会将Java的内存分为堆和栈,栈通常所说的就是虚拟机栈,或者是说虚拟机栈中局部变量表部分。
局部变量表中存储了,编译可知的基本数据类型、对象引用类型、returnAddress类型,在表中,64位长度的lang和double数据会占据2个字节,其余数据类型均占用1个。局部变量表的数据均是在编译期间就将数据空间大小分配完成,在运行期间,空间大小事情固定的。如果出现异常,线程请求栈帧的深度大于虚拟机允许深度,即会跑出StackOverflowError异常;如果可动态扩展,那无法申请更多内存的时候,就会抛出OOM异常;下图左侧位栈帧多,右侧为栈帧过大。
在这里插入图片描述

2.1.栈帧过多测试

当无限递归调用(栈帧过多)时,会出现StackOverflowError的异常,示例代码如下

public class test1 {
    
    public static void main(String[] args) {
        runTest();
    }
    
    public static void runTest(){
        runTest();
    }
}

出现下面的报错内容,说明没有继续对其继续处理。代码的逻辑以及不正确了。
在这里插入图片描述

3.本地方法栈

本地方法栈就是Native Stack,与Java虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务
是线程私有的。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行,下图是从编译过程中得到的,因此,可以看出java通过调用本地的C或者是C++方法于操作系统层进行交互。

在这里插入图片描述

4.Java堆

  • 用来存放对象的存储空间,几乎所有的对象都会存储在此处。
  • 线程共享。垃圾回收的主要场所。在虚拟机启动时创建。堆可以分为新生代(Eden区、From Survior,To Survivor)、老年代。虚拟机规范规定:堆可以处于不连续的内粗空间中,但在逻辑上应该是视为连续的内存空间;关于s0、s1区:复制后交换,谁空闲谁是s0.
  • 同时,Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用;通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间;堆的内存空间既可以固定大小,也可运行时动态地调整,通过参数-Xms设定初始值、-Xmx设定最大值。

4.1.新生代与老生代

  • 新生代周期叫老生代短
  • 新生代:老生代内存空间比例1:2。通过JVM调参数 XX:NewRatio=2,新生代占1,老年代占2,新生代占堆的1/3空间
  • Hospot中Eden:s0:s1 = 8:1:1
  • 对象在new的时候,是在eden区的,如果对象太大,即大对象,直接存储进入老年代。

4.2.对象分配过程

  • den区存放新new的对象
  • 若创建对象时,Eden区满了,直接出发Minor GC,回收Eden区不再被其他对象饮用的对象,再加载新的对象到Eden区,注意s满了不触发Minor GC,仅Eden区域满了,才会进行Minor GC,并将S区顺便清理。
  • Eden中剩余存活对象复制到S区
  • 再次触发垃圾回收(1.JAVA垃圾回收),将存活内容复制到s0中,如果未进行回收,则继续存活,s0变s1,s1变s0
  • 按照上面的顺序重复进行,默认限制次数位15次,超过15次,则将s区存活的转去老年区;(jvm参数设置修改默认次数-XX:MaxTenuringThresahold=N N为次数)

有个现象经常会在内存泄漏的时候出现,就是jvm在不断的FullGC(监控指令见性能测试常用linux指令篇),那触发的条件是什么呢?

4.3.FGC的触发条件:

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足、方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 在新生代回收内存时,由Eden区和Survivor From区把存活的对象向Survivor To区复制时,对象大小大于Survivor To空间的可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。即老年代无法存放下新年代过度到老年代的对象的时候,便会触发Full GC。Full GC的STW时间较长,应该要避免这种情况的发生

5.运行时常量池

  • 运行时常量池就是将编译后的类信息放入方法区中,也就是说它是方法区的一部分
  • 运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。
  • 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。每个class都有一个运行时常量池,类在解析之后将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
  • 运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中-常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

6.直接内存

  • 又名堆外内存。和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
  • 作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存

7.元空间

  • 移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap
  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
    -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
    -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  • 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
    -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

8.方法区

线程共享区域,在jvm启动时就创建,且物理内存可以不连续,大小可以固定,也可以好似动态扩展的,他的大小决定了系统可以保存多少个类,如果系统定义了太多类就会出现oom的情况。并且方法区会随着jvm的关闭而释放内存。
在这里插入图片描述

9.永久代和元空间关系

  • 永久代变为元空间是JRockit和HotSpot融合后的结果,因为JRockit没有永久代,所以他们不需要配置永久代。主要原因是:为永久代设置空间大小是很难确定的,而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,对永久代进行调优是困难的很的。

三、JVM优点

1.一次编译,到处运行

提到jvm的优点,那么必须说的一句话”一次编译,到处运行“,说的就是jvm的功效,不论是linux还是windows,亦或者是macos都是如此。

2.自动内存管理,垃圾回收机制

在开始的时候,我们说到的与C/C++的不同,就是基于此。

3.数组下标越界检查

在Java诞生之时,还有个让当时C和C++大佬头疼的问题是,数组下标越界是没有检查机制的,JVM又一次果断提供了数组下标越界的自动检查机制,在检测到数组下标出现越界后,会在运行时自动抛出“java.lang.ArrayIndexOutOfBoundsException”这个异常


4、总结

  • JVM的模型中存在知识点内容很多,在学习的过程中,不应该仅仅局限于一点或者是某一层,应该是从辩证的全局进行查看。
  • JVM的模型共计分为几个部分:程序计数器、虚拟机栈、堆、常量池、直接内存、方法区、元空间。

你懂得越多,就懂得不懂的越多。我是团结屯光头强。


网站公告

今日签到

点亮在社区的每一天
去签到