avatar

Catalog
JVM

转载 https://zhuanlan.zhihu.com/p/34426768

内存结构图

未命名文件

程序计数器

  • 程序计数器是一块较小的空间,它可以看作是当前线程所执行的字节码的行号指示器
  • 如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(可以理解为上图所示的行号),如果正在执行的是native方法,这个计数器的值为undefined。
  • JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。
  • 此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。

虚拟机栈

  • 每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。

  • 虚拟机栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、动态链接、方法出口等信息。

    • 局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。

    • 64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。

    • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。

  • 栈的大小可以固定也可以动态扩展。

    • 在固定大小的情况下,当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError异常。

    • 在动态扩展的情况下,若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

  • 和虚拟机栈类似,两者的区别就是虚拟机栈是为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务 。

  • HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

  • 与虚拟机栈一样,本地方法栈也会抛StackOverflowError和OutOfMemoryError异常。

  • JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区。
  • 堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”
  • JAVA堆的分类:

    • 从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)

    • 从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)

  • JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
  • 可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常

方法区 (存的是比较全局的一些东西)

  • 方法区也是线程共享区,用于存储【虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态变量,即时编译器编译后的代码等数据】

  • 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

  • 方法区可选择不实现垃圾收集,一般来说,这个区域对内存回收的条件较为苛刻,但是这部分区域的回收确实是必要的。

  • 当方法区无法满足内存分配需求时,将会抛OutOfMemoryError异常

  • 运行时常量池

    • 运行时常量池是方法区的一部分。

    • class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后加入方法区的运行时常量池中存放。

    • 运行时常量池相于class文件中的常量池,所不同的是其具备了动态性。class文件中常量池中的常量在编译期间就已经定义好了,而运行时常量池在程序运行期间也可以将常量放入该常量池中,最常见的做法就是调用String类的intern()方法。

      尽管在输出中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。在调用”ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。

运行时常量池

简介

运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。

运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)

Class文件中的信息常量池

在Class文件结构中,最头的4个字节用于存储Megic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。

常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  • 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

双等号==的含义

  • 基本数据类型之间应用双等号,比较的是他们的数值。
  • 复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。

基本类型的包装类和常量池

java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。

类加载器

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

类加载器分类

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载机制——双亲委派机制

  • 1、当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
  • 3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;
  • 4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException

GC

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 在新生代的GC叫做轻GC、在老年代的GC叫做重GC。

对象存活判断

判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

    在Java语言中,GC Roots包括:

    • 虚拟机栈中引用的对象。
    • 方法区中类静态属性实体引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中JNI引用的对象。

垃圾收集算法

  • “标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

    它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-压缩算法

  • “复制”(Copying)的收集算法,它将可用内存按容量划分成两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

    这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但是这种方法需要一块未使用的内存作为复制区,造成内存浪费,而且只适用于新生代这种存活对象少的情况。

  • 标记-压缩算法。复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

    根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

    “分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

Question:为什么要设置两个幸存区?

答:因为新生代采用 “复制”算法来收集垃圾,所以需要一个空的内存区域,用来保存存活下来的对象,这是幸存区存在的原因。而之所以需要两个,是因为当伊甸区满了之后,不光要对伊甸区进行垃圾回收,同时要对幸存区进行垃圾回收,此时又需要一块空的内存区域,用来保存存活下来的对象。所以需要两个幸存区,交替着用。 通常 伊甸区:幸存区1:幸存区2=8:1:1 .

而老年代 使用的不是“复制”算法,而是标记压缩算法,所以就不需要幸存区。

内存调优

参数含义

先说VM选项, 三种:

  • - : 标准VM选项,VM规范的选项
  • -X: 非标准VM选项,不保证所有VM支持
  • -XX: 高级选项,高级特性,但属于不稳定的选项

参见 Java HotSpot VM Options

题主提到的参数前缀为X,显然属于第二类

再说这几个参数,其语义分别是:

  • -Xmx: 堆的最大内存数,等同于-XX:MaxHeapSize
  • -Xms: 堆的初始化初始化大小
  • -Xmn: 堆中新生代初始及最大大小,如果需要进一步细化,初始化大小用-XX:NewSize,最大大小用-XX:MaxNewSize
  • -Xss: 线程栈大小,等同于-XX:ThreadStackSize

命名应该非简称,助记的话: memory maximum, memory startup, memory nursery/new, stack size.

Jprofiler使用

1、下载软件并安装

2、在idea 市场中安装对应插件

3、在setting/tools/Jprofiler中配置软件安装目录

4、在运行时配置虚拟机参数 -XX:+HeapDumpOnOutOfMemoryError

5 、在项目目录下找到dump出来hprof的文件。

Author: realLiuSir
Link: http://yoursite.com/2020/04/18/JVM/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶