一篇文章带你了解JVM内存模型

目录
  • 1. JVM介绍
    • 1.1 什么是JVM?
    • 1.2 JVM的优点
      • 1.2.1 一次编写,到处运行。
      • 1.2.2 自动内存管理,垃圾回收机制。
      • 1.2.3 数组下标越界检查
      • 1.2.4 多态
    • 1.3 JVM、JRE、JDK之间的关系
      • 1.3.1 JVM的简介
      • 1.3.2 JRE的简介
      • 1.3.3 JDK的简介
    • 1.4 JVM的常见实现
    • 1.5 JVM的内存结构图
      • 1.5.1方法区、堆
      • 1.5.2虚拟机栈、程序计数器、本地方法栈
      • 1.5.3执行引擎
      • 1.5.4 GC(垃圾回收机制)
      • 1.5.5本地方法接口
  • 2. JVM内存结构-程序计数器
    • 2.1 程序计数器的定义
    • 2.2 操作步骤
    • 2.3 特点
      • 2.3.1 线程私有的
      • 2.3.2 不会存在内存溢出
  • 3. JVM内存结构-虚拟机栈
    • 3.1 定义
    • 3.2 栈内存溢出
      • 3.2.1 发生原因
      • 3.2.2 栈内存溢出小实验
  • 4. JVM内存结构-本地方法栈
    • 4.1 定义
  • 5. JVM内存结构-堆
    • 5.1 定义
    • 5.2 特点
      • 5.2.1线程共享
      • 5.2.2有垃圾回收机制
    • 5.3 堆内存溢出小实验
      • 5.3.1 修改堆内存大小参数的小实验
      • 5.3.2 堆内存诊断的小实验
  • 6. JVM内存结构-方法区
    • 6.1 定义
    • 6.2 特点
    • 6.3 JVM内存结构示意图
  • 总结

1. JVM介绍

1.1 什么是JVM?

JVM是Java Virtual Machine(Java虚拟机)的简称,是一种用于计算设备的规范,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。

1.2 JVM的优点

1.2.1 一次编写,到处运行。

JVM可以让java程序,一次编写,导出运行。让底层代码和运行环境分离开,编写好一份代码后,不用再次修改内容,只用通过安装不同的JVM环境自动进行转换即可运行,在各种系统中无缝连接。

1.2.2 自动内存管理,垃圾回收机制。

在Java诞生之时,C和C++称霸天下,但是这两种语言中没有内存管理机制,是通过手动操作来进行的管理,非常麻烦和繁琐。

此时Java应运而生,为了处理内存管理这个方面,专门设计了垃圾回收机制,来自动进行内存的管理。极大的优化了操作,让程序员们不用正在噼里啪啦在码代的海洋中遨游时,还要操心内存会不会溢出这些“影响我方输出”的问题,顿时获得了成吨的好评。

1.2.3 数组下标越界检查

在Java诞生之时,还有个让当时C和C++大佬头疼的问题是,数组下标越界是没有检查机制的,这还了得,又是一个影响“我方暴力输出”的罪魁祸首,因此JVM继续抱着暖男的思想,又来了个爱的抱抱。

JVM又一次看见了大佬们的烦恼,果断提供了数组下标越界的自动检查机制,在检测到数组下标出现越界后,会在运行时自动抛出“java.lang.ArrayIndexOutOfBoundsException”这个异常,在当时可是感动了很多业界大佬(我猜的)。

1.2.4 多态

JVM还有一个多态功能,是通过相同接口,不同的实例进行实现,完成不同的业务操作,比如:定义了一个动物接口(里面有一个吃的方法),我们就可以通过这个动物创造小猫(吃鱼),再创造一个狗狗(吃肉),再创造一个小助手(吃零食,O(∩_∩)O哈哈~)。

仔细想想,对我们有啥影响呢,那好处老多了,比如:

(1)消除类型之间的耦合关系;

(2)可替换性;

(3)可扩充性;

(4)接口性;

(5)灵活性;

(6)简化性;

1.3 JVM、JRE、JDK之间的关系

1.3.1 JVM的简介

JVM是Java Virtual Machine的简称,是Java虚拟机,是一种模拟出来的虚拟计算机,它通过在不同的计算机环境当中模拟实现计算功能来实现的。

引入Java虚拟机后,Java语言在不同平台上运行时就不需要重新编译。在其中,Java虚拟机屏蔽了与具体平台的相关信息,使得Java源程序在编译完成之后即可在不同的平台运行,达到“一次编译,到处运行”的目的,Java语言重要的特点之一跨平台,也即与平台的无关性,其关键点就是JVM。

1.3.2 JRE的简介

JRE是Java Runtime Environment的简称,是Java运行环境,是让操作系统运行Java应用程序的环境,其内部包含JVM,也就是说JRE只负责对已经存在的Java源程序进行运行的操作,它不包含开发工具JDK,对JDK内部的编译器、调试器和其它工具均不包含。

1.3.3 JDK的简介

JDK是Java Development Kit的简称,是Java开发工具包,是整个Java程序开发的核心。其主要包含了JRE、Java的系统类库以及对Java程序进行编译以及运行的工具,例如:javac.exe和java.exe命令工具等。

1.4 JVM的常见实现

Oracle(Hotspot、Jrockit)、BEA(LiquidVM)、IBM(J9)、taobaoVM(淘宝专用,对Hotspot进行了深度定制)、zing(垃圾回收机制非常快,到达1毫秒左右)。

1.5 JVM的内存结构图

当Java程序编译完成为.class文件==》类加载器(ClassLoader)==》将字节码文件加载进JVM中;

1.5.1方法区、堆

方法区中保存的主要是类的信息(类的属性、成员变量、构造函数等)、堆(创建的对象)。

1.5.2虚拟机栈、程序计数器、本地方法栈

堆中的对象调用方法时,方法会运行在虚拟机栈、程序计数器、本地方法栈中。

1.5.3执行引擎

执行方法中代码时,代码通过执行引擎执行中的“解释器”执行;方法中经常调用的代码,即热点代码,通过“即时编译器”执行,其执行速度非常快。

1.5.4 GC(垃圾回收机制)

GC是针对堆内存中没有引用的对象进行回收,可以手动也可以自动。

1.5.5本地方法接口

因为JVM不能直接调用操作系统的功能,只能通过本地方法接口来调用操作系统的功能。

2. JVM内存结构-程序计数器

2.1 程序计数器的定义

Program Counter Register即程序计数器(寄存器),用于记录下一条Jvm指令的执行地址。

2.2 操作步骤

javap主要用于操作JVM,javap -c 是对java代码进行反汇编操作。下图为通过先编译demo.java后,再执行javap -c demo的输出结果:

其中第一列为二进制字节码,即JVM指令,第二列为java源代码。第一列中的序号为JVM指令的执行地址。

JVM会通过程序计数器记录下一条需要执行的JVM指令的地址(比如第一行的0),然后交给解释器解析为机器码,最后交给cpu(只能识别机器码),完成一行的执行。

想要执行下一行,继续让JVM的程序计数器记录下一条地址(比如第二行的3),再交给解释器解析后给cpu,以此类推执行结束。

2.3 特点

2.3.1 线程私有的

2.3.2 不会存在内存溢出

3. JVM内存结构-虚拟机栈

3.1 定义

虚拟机栈是每个线程运行所需要的内存空间,每个栈中由多个栈帧组成,每个线程中只能有一个活动栈帧(对应当前正在执行的方法),所有栈帧都遵循后进先出,先进后出的原则。

栈帧是每次调用方法时所占用的内存,在栈帧中保存的内容参数、局部变量、返回地址。

注1:垃圾回收不涉及栈内存,因为栈内存是由方法调用产生的,当方法调用结束后会弹出栈。

注2:栈内存不是分配的越大越好,因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数会越来越少。

注3:方法的局部变量,当其没有逃离方法的作用范围时,是线程安全的;如果其引用了对象(比如静态变量,即共享变量,用对象作为参数的方法,返回值为对象的方法),并且逃离出了方法的作用范围,就需要考虑线程安全的问题了。

3.2 栈内存溢出

3.2.1 发生原因

(1)虚拟机栈中,栈帧过多(无限递归),如图1栈帧过多;

(2)每个栈帧所占用过大,如图2 栈帧过大。

3.2.2 栈内存溢出小实验

3.2.2.1 栈帧过多的小实验

无限递归调用(栈帧过多)的小实验,method1()方法在主方法中无限调用自己,那么会发生什么情况呢?

答案很明显,程序崩溃了,产生了栈内存溢出错误,如下图所示:

-Xss:该参数规定了每个线程虚拟机栈的大小;

接着我们通过设置一个虚拟机栈的大小是256k试试会发生什么?

我们发现当我们调整了虚拟机栈的大小后执行了4315次方法后内存就溢出了,而调整虚拟机栈之前,我们是23268次,很明显我们可以通过-Xss参数调整虚拟机栈的大小来控制内存的溢出情况。

3.2.2.2 线程运行诊断小实验

想象中的场景,大佬在疯狂输出,突然CPU爆表了,显示CPU占用过多,如何去定位哪行代码的问题,是的是哪行(大佬都很忙的好吗,当然要精确了,一分钟几千万上下的,O(∩_∩)O哈哈~)?

Linux环境下:

在后台运行Stack_6这个java字节码(.class)文件:

注:无论是否将nohup命令的输出重定向到终端,输出都将附加到当前目录的 nohup.out 文件中。

(1)通过top命令,查看进程(相当于任务管理器),发现了一个占用CPU达到100%的可疑家伙,这还了得,赶紧瞅瞅具体发生了什么,还有没有王法,这让其他小伙伴还怎么愉快的玩耍,秒速纠错ING。。。

注:top命令,查看哪个进程占用CPU过高,返回进程号。

(2) 通过ps H -eo pid,tid ,%cpu | grep 命令过滤任务管理器中的内容。

注:ps H -eo pid,tid,%cpu |grep,是通过ps命令查看哪个线程占用CPU过高,返回进程id,其中pid为进程id,tid为线程id,%cpu为CPU占用情况;

发现了罪魁祸首,这一串串心惊肉跳的红色。。。

(3) 通过jstack 进程id查看,20389这个有问题的进程中具体的情况。

注:jstack 进程id,是通过jstack 命令定位具体哪段代码出现占用CPU过高,注意jstack命令查找的线程id是16进制的,需要转换;

发现里面有一堆执行的代码,那么我们怎么找到具体是哪个家伙搞事情的呢?上图我们可以发现搞事情的线程是20441,那么我们通过计算器将20441转换为16进制的4FD9再去试试,真相只有一个。

通过对比nid(线程id)4fd9,我们发现这个叫thread1的线程一直在运行(RUNNABLE状态),并且查看到位置是位于Stack_6.java文件的第11行出现的问题。。。

现在我们回到源码中,在Stack_6文件的第11行,我们发现原来这里一直在执行死循环,终于找到你,还好我没放弃,奈斯~

4. JVM内存结构-本地方法栈

4.1 定义

由于Java本身有时候是无法直接和操作系统底层交互的,但有时候需要Java调用本地的C或C++方法,所以此时本地方法栈应运而生,它们是一类带有native关键字的方法。

5. JVM内存结构-堆

5.1 定义

Heap堆:是通过new关键字创建的对象存放在堆中

5.2 特点

5.2.1线程共享

堆中存放的对象都是线程共享的,因此都是需要考虑线程安全问题的。

5.2.2有垃圾回收机制

因为堆中存放的对象存放了大量的对象,因此给他配了个小助手——垃圾回收机制(可以调自动挡和手动挡哦~)。

5.3 堆内存溢出小实验

5.3.1 修改堆内存大小参数的小实验

继续幻想一个场景,当一个大佬开发完一个段代码的时候(当然一般大佬都是很自信的,我写的代码怎么可能有问题,不存在的。。。),但是测试可跑不了,稳妥起见咱们还是默默得搞测试试试嘛,安全第一。但是机器的内存就这么大,大佬肯定跑了很多次了,都没出现问题的,这不是找茬嘛。。。还是默默改下机子参数再试试吧(想去怼大佬,一定要拿出证据嘛~)。

-Xmx:JVM调优参数之一,该参数表示java堆可以扩展到的最大值。下面上案例:

在执行了26次之后,果断的后台报了堆内存溢出错误。

下面通过-Xmx JVM调优参数将堆内存调小至8m,再试试会发生什么呢?

操作基本和栈内存溢出的时候的案例一样,次数明显变小了,只调用了17次就出现了堆内存溢出错误了。

5.3.2 堆内存诊断的小实验

jps工具:查看当前系统中有哪些java进程

jmap工具:查看堆内存的占用情况jmap -heap 进程id

jconsole工具:图形化的工具,拥有多功能的监测功能,可以连续监测。

下面我们通过运行代码后通过jconsole可视化图形工具,来查看堆内存的使用情况。

上图我们可以看到,在我们创建10mb的数组对象时,内存使用有一定上升;然后在我们手动调用垃圾回收机制后,内存又得到了很大的释放。

6. JVM内存结构-方法区

6.1 定义

Java虚拟机中有一个被所有jvm线程共享的方法区。方法区有点类似于传统编程语言中的编译代码块或者操作系统层面的代码段。它存储着每个类的构造信息,譬如运行时的常量池,字段,方法数据,以及方法和构造方法的代码,包括一些在类和实例初始化和接口初始化时候使用的特殊方法。

方法区有个别称non-heap(非堆),可以看作是一块独立于堆的内存空间,是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量,JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。

6.2 特点

(1)方法区与java堆一样,是各个线程共享的内存区域;

(2)方法区在JVM启动的时候被创建;

(3)方法区的大小,跟堆空间一样,可以选择固定大小或扩展;

(4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出溢出错误OutOfMemoryError;

(5)关闭JVM就会释放这个区域的内存。

6.3 JVM内存结构示意图

在JVM内存结构1.6的时候,方法区保存在内存结构中,叫做永久代,里面存储了运行时的常量池(包含串池StringTable)、类的信息、类加载器;

在JVM内存结构1.8的时候,方法区做为一个概念,保存在本地内存中,叫做元空间,里面存储了运行时的常量池、类的信息、类加载器,此时串池(StringTable)储存在堆之中。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • JVM内存模型知识点总结

    内存模型如下图所示 堆 堆是Java虚拟机所管理的内存最大一块.堆是所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域唯一的目的就是存放对象实例.所有的对象实例都在这里分配内存 Java堆是垃圾收集器管理的主要区域.从内存回收的角度来看,由于现在的垃圾收集器采用的是分代收集算法.所以,java堆又分为新生代和老年代.从内存分配的角度来说,线程共享的java对中可能划分出多个线程私有的fenp缓冲区(Thread Local Allocation Buffer). 可以通过 -Xms.-X

  • 图解JVM内存模型

    前言 上篇文章我们一起了解了jvm虚拟机类的加载机制,而且是以一种纯大白话进行的一场闲聊,相信小伙伴们应该印象深刻,感兴趣的小伙伴可以重温一下上一篇文章大白话谈JVM的类加载机制. 当jvm加载了类后,会把需要使用的对象放入到内存当中,那么jvm的内存模型是什么样的呢? 今天我们就来探索一下jvm的内存模型.由于有小伙伴反映想加些图更容易理解,王子接下来的文章打算用更多的图例来讲解. 方法区 很多小伙伴之前也了解过jvm的内存模型,知道有方法区这个东西,但可能了解的不是很详细. 其实方法区是在J

  • Java内存模型与JVM运行时数据区的区别详解

    首先,这两者是完全不同的概念,绝对不能混为一谈. 1.什么是Java内存模型? Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性.原子性的问题,解决共享变量的多线程操作冲突问题. 多线程编程的普遍问题是: 所见非所得 无法肉眼检测程序的准确性 不同的运行平台表现不同 错误很难复现 故JVM规范规定了Java虚拟机对多线程内存操作的一些规则,主要集中体现在volatile和synchronized这两个关键字. vo

  • 详解JVM系列之内存模型

    1. 内存模型和运行时数据区 这一章学习java虚拟机内存模型(Java Virtual machine menory model),可以这样理解,jvm运行时数据库是一种规范,而JVM内存模型是对改规范的实现 java虚拟机重点存储数据的是堆和方法区,所以本章节也重点从这两个方面进行比较详细描述.堆和方法区是内存共享的,而java虚拟机栈.Native方法栈.程序计数器是线程私有的 2.思维导图和图例 一个是非堆区(方法区),方法区也一般被称之为"永久代".另外一个是堆区,分为you

  • 一篇文章带你了解JVM内存模型

    目录 1. JVM介绍 1.1 什么是JVM? 1.2 JVM的优点 1.2.1 一次编写,到处运行. 1.2.2 自动内存管理,垃圾回收机制. 1.2.3 数组下标越界检查 1.2.4 多态 1.3 JVM.JRE.JDK之间的关系 1.3.1 JVM的简介 1.3.2 JRE的简介 1.3.3 JDK的简介 1.4 JVM的常见实现 1.5 JVM的内存结构图 1.5.1方法区.堆 1.5.2虚拟机栈.程序计数器.本地方法栈 1.5.3执行引擎 1.5.4 GC(垃圾回收机制) 1.5.5本

  • 一篇文章带你了解JVM垃圾回收

    目录 1.堆空间的基本结构: 2.空间分配担保机制 3.如何判断一个对象已经无效 4 不可达的对象并非"非死不可" 5 如何判断一个常量是废弃常量? 6 如何判断一个类是无用的类 7.垃圾回收算法 7.1 标记-清除算法 7.2 标记-复制算法 7.3 标记-整理算法 7.4 分代收集算法 总结 如何判断对象是否死亡(两种方法). 简单的介绍一下强引用.软引用.弱引用.虚引用(虚引用与软引用和弱引用的区别.使用软引用能带来的好处). 如何判断一个常量是废弃常量 如何判断一个类是无用的类

  • 一篇文章带你搞懂Python类的相关知识

    一.什么是类 类(class),作为代码的父亲,可以说它包裹了很多有趣的函数和方法以及变量,下面我们试着简单创建一个吧. 这样就算创建了我们的第一个类了.大家可以看到这里面有一个self,其实它指的就是类aa的实例.每个类中的函数只要你不是类函数或者静态函数你都得加上这个self,当然你也可以用其他的代替这个self,只不过这是python中的写法,就好比Java 中的this. 二.类的方法 1.静态方法,类方法,普通方法 类一般常用有三种方法,即为static method(静态方法),cl

  • 一篇文章带你了解Java Spring基础与IOC

    目录 About Spring About IOC Hello Spring Hello.java Beans.xml Test.java IOC创建对象的几种方式 Spring import settings Dependency Injection 1.构造器注入 2.set注入 3.拓展注入 P-namespcae&C-namespace Bean scopes singleton prototype Bean的自动装配 byName autowire byType autowire 小结

  • 一篇文章带你搞定Python多进程

    目录 1.Python多进程模块 2.Python多进程实现方法一 3.Python多进程实现方法二 4.Python多线程的通信 5.进程池 1.Python多进程模块 Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Process对象来创建一个进程对象.这个进程对象的方法和线程对象的方法差不多也有start(), run(), join()等方法,其中有一个方法不同Thread线

  • 一篇文章带你了解清楚Mysql 锁

    一丶为什么数据库需要锁 数据库锁设计的初衷是处理并发问题.作为多用户共享 的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则.而锁就是用来实 现这些访问规则的重要数据结构. 根据加锁的范围,MySQL 里面的锁大致可以分成全局锁.表级锁和行锁三类 二丶全局锁&全库逻辑备份 全局锁就是对整个数据库实例加锁.全局锁的典型使用场景是,做全库逻辑备份,全库逻辑备份有以下几种方式: 1.Flush tables with read lock (FTWRL) Flush tables with

  • 一篇文章带你弄清楚Redis的精髓

    目录 一.Redis的特性 1.1 Redis为什么快? 1.2 Redis其他特性 1.3 Redis高可用 二.Redis数据类型以及使用场景 2.1 String 2.1.1 基本指令 2.1.2 应用场景 2.2 Hash 2.2.1 基本指令 2.2.2 应用场景 2.3 List 2.3.1 基本指令 2.3.2 应用场景 2.4 Set 2.4.1 基本指令 2.4.2 应用场景 2.5 ZSet(SortedSet) 2.5.1 基本指令 2.5.2 应用场景 三.Redis的事

  • 一篇文章带你彻底搞懂Redis 事务

    目录 Redis 事务简介 Redis 事务基本指令 实例分析 Redis 事务与 ACID 总结 Redis 事务简介 Redis 只是提供了简单的事务功能.其本质是一组命令的集合,事务支持一次执行多个命令,在事务执行过程中,会顺序执行队列中的命令,其他客户端提交的命令请求不会插入到本事务执行命令序列中.命令的执行过程是顺序执行的,但不能保证原子性.无法像 MySQL 那样,有隔离级别,出了问题之后还能回滚数据等高级操作.后面会详细分析. Redis 事务基本指令 Redis 提供了如下几个事

  • 一篇文章带你吃透JavaScript中的DOM知识及用法

    目录 一.前言 二.DOM框架 三.认识DOM节点 四.JS访问DOM 1.获取节点 2.改变 HTML 3.改变 CSS 4.检测节点类型 5.操作节点间的父子及兄弟关系 6.操作节点属性 7.创建和操作节点 总结 一.前言 DOM:Document Object Model(文档对象模型),定义了用户操作文档对象的接口,可以说DOM是自HTML将网上相关文档连接起来后最伟大的创新.它使得用户对HTML有了空前的访问能力,并使开发者将HTML作为XML文档来处理. 本文知识导图如下: 二.DO

随机推荐