java内存分析

释放双眼,带上耳机,听听看~!

java内存分析

  • java内存分析

  • 程序计数器

    • 虚拟机栈
  • 局部变量表
    * 操作数栈
    * 动态连接
    * 方法返回地址
    * 附加信息

    • 本地方法栈
  • 内存划分

  • 堆内存

  • 新生代

  • eden
    * survivor

    
    
    1
    2
    1          * 老年代
    2
    • 方法区
    • 元空间
  • 快速入门
    * 内存分配模型
    * 调优

java内存分析

images/JAVA虚拟机运行时数据区(2).png

JAVA虚拟机运行时数据区(2).png

程序计数器

  • 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个确定的时刻,一个处理器(对于多核来说,就是一个核)都会执行一条线程中的指令。因为,为了线程切换后能恢复到正确的位置,每条现成都需要一个独立的程序计数器,各个线程之间计数器互相不影响,独立存储,这类内存区域为“线程私有”内存,如果正在执行的是Native方法,则技术器数值为空,此区域是java虚拟机中没有规定任何OutofMemoryError情况的区域

虚拟机栈

  • 线程私有,生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态连接,发放出口等信息。每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

老师们总说的「堆内存」(Heap)和「栈内存」(Heap),其中的栈指的是上图中的「虚拟机栈」或「局部变量表」部分。
虚拟机栈是一个大的主体,包括局部变量表、操作数栈,动态连接,返回地址等

局部变量表

当前线程调用函数内的局部变量。
「变量槽」(slot)是局部变量表的最小单位,一般为32位大小,一个slot可以存放boolean,byte,char,short,int,float,reference和returnAddress8种类型,reference表示一个对象的实例引用,通过他可以获得对象在java堆中存放的起始位置和数据类型等信息,slot通过2个连续的空间存放long跟double类型,returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,他指向了一条字节码指令的地址
变量槽的复用:
虚拟机通过索引定位的方式使用局部变量表。局部变量表存放的是方法参数和局部变量。当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。


1
2
3
4
5
6
7
8
9
10
11
12
1public class Test {
2    public static void main(String[] args) {
3        {
4            byte[] temp = new byte[64 * 1024 * 2014];
5        }
6        System.gc();
7    }
8}
9[GC (System.gc())  131353K->129368K(245248K), 0.0028307 secs]
10[Full GC (System.gc())  129368K->129283K(245248K), 0.0053750 secs]
11
12


1
2
3
4
5
6
7
8
9
10
11
12
13
1public class Test {
2    public static void main(String[] args) {
3        {
4            byte[] temp = new byte[64 * 1024 * 2014];
5        }
6        int a=1;
7        System.gc();
8    }
9}
10[GC (System.gc())  131353K->129336K(245248K), 0.0049695 secs]
11[Full GC (System.gc())  129336K->387K(245248K), 0.0109887 secs]
12
13

可以看到上面两断代码的gc效果「jvm启动参数:-verbose:gc」,当声明局部变量temp数组的时候,内存占用了一部分空间,但是第一段代码的gc并没有回收没有引用的temp数组。第2段进行的正常的垃圾回收。这里就是变量槽的复用。主要是根据index来进行。只要被局部变量表中直接或者间接引用的对象都不会被回收.下面再看一个变量槽复用的代码。


1
2
3
4
5
6
7
8
9
10
1  public static void main(String[] args) {
2        {
3            int a = 3;
4            System.out.println(a);
5        }
6        int c = 3;
7        System.gc();
8    }
9
10

通过jclasslib分析。可以看到下面这图:

java内存分析

images/mer.png
变量槽的index1复用过2次。第一次是a定义的时候,当局部变量失去引用的时候,把index1又给了c进行复用。

操作数栈

  • 和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

  • 虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。

  • 虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的


1
2
3
4
5
6
7
8
1    begin  
2    iload_0    // push the int in local variable 0 onto the stack  
3    iload_1    // push the int in local variable 1 onto the stack  
4    iadd       // pop two ints, add them, push result  
5    istore_2   // pop int, store into local variable 2  
6    end
7
8

动态连接

此处为空,没有太理解

方法返回地址

当方法返回时,可能进行3个操作:

  • 恢复上层方法的局部变量表和操作数栈

  • 把返回值压入调用者调用者栈帧的操作数栈

  • 调整 PC 计数器的值以指向方法调用指令后面的一条指令

附加信息

虚拟机规范并没有规定具体虚拟机实现包含什么附加信息,这部分的内容完全取决于具体实现。在实际开发中,一般会把动态连接,方法返回地址和附加信息全部归为一类,称为栈帧信息。

本地方法栈

本地方法栈与虚拟机栈作用相似,区别为虚拟机栈为虚拟机执行java方法服务,本地方法栈则为虚拟机使用到的native方法服务。

java堆是java虚拟机所管理内存中最大的一块。java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,堆(Heap)的唯一目的就是存放对象实例,所有的对象实例以及数组都要在堆上分配,同时堆也是垃圾收集器管理的主要区域。从内存回收来看,现在的收集器都采用分代收集算法,所以java堆中还可以分为「新生代」和「老年代」,再细致可以分为eden空间,From Survivor空间,To Survivor空间等.根据java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。当前主流的虚拟机都是按照可扩展来实现的(通过Xmx和Xms)。如果堆中没有内存完成实例分配并且堆也无法扩展的时候,就会出先OOM异常。

内存划分

java内存分析

memery.png

堆内存

堆内存大小通过-Xms -Xmx来指定大小,堆内存分为新生代和老年代。通过-Xmx/-Xms来分配最大和最小堆内存。

新生代

  • 新生代和老年代默认的空间比例为1:2。可以通过-XX:NewRatio来配置,设置-XX:NewRation=3表示年轻代与老年代的比是1:3即年轻代占年轻代+老年代内存的4分之1.

  • 新生代中细分为eden和From Survivor和To Survivor三个区。默认eden:FromSurvivor:ToSurvivor=8:1:1。

  • 发生在新生代的是Minor GC,采用的是复制算法。

eden

  • 一般新创建的对象都会分配到eden区(对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。)。当这些对象经过第一次Minor gc后,如果仍然存活的会被移动到survivor区。因为java中创建的对象基本是朝生夕死(80%),第一次Minor gc会回收很多的对象。

MinorGC:从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。

当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。

执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉

所有的 Minor GC 都会触发“stop-the-world”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

survivor

survivor分为From Survivor和To Survivor两个区。
这两个区总有一个是空的。在GC开始的时候,在eden中回收一些瞬时对象,剩下的存活对象会复制到To区域中(对象年龄+1)。而在From区中,存活的对象根据年龄来决定去向,年龄到达一定值的时候,对象会被移动到老年代中,没有达到一定值的时候,对象会移动到To区,并且年龄+1.经过此次GC。eden区和From区已经被清空。下次GC的时候,会把对象从To移动到From区中,就会变成eden和To区清空。每次GC进行重复上面的复制回收动作。使From或To区总有一个是清空的。当To 或 From区一个被填满之后,会将所有对象移动到老年代中。

-XX:SurvivorRatio:设置eden和survivor的比值。-XX:SurvivorRatio=3表示eden:Survivor=3:2;eden占5分之三,From/To Survivor各占五分之一

-XX:+PrintTenuringDistribution:显示每次Minor GC时Survivor区中各个年龄段的对象的大小

-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold:晋升到老年代的对象年龄的最小值和最大值。每个对象在坚持过一次Minor GC之后,年龄就加0

老年代

  • 老年代发生的GC称为MajorGC,采用的是标记-清除算法。

  • 标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作

-XX:+PrintGCDetails:控制台显示 GC 相关的日志信息

Full GC == Major GC指的是对老年代/永久代的stop the world的GC

FullGC与MajorGC的区别{:target="_blank"}

方法区

方法区与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它还有一个别名叫Non-Heap(非堆)。对于HotSpot虚拟机。他还有一个别名,叫做永久代。在java8之前,可以通过-XX:MaxPermSize来设置方法区的上限。

在java8之前,如果无法分配内存会抛出OOM异常。可以通过上面的命令来增加上限值

在java8中,HotSpot已经移除了这个方法区。有了一个新的「元空间」来替代方法区。

元空间

快速入门

  • 它是本地堆内存中的一部分

  • 它可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来进行调整

  • 当到达XX:MetaspaceSize所指定的阈值后会开始进行清理该区域

如果本地空间的内存用尽了会收到java.lang.OutOfMemoryError: 「Metadata space」或「Java heap space」的错误信息。
和持久代相关的JVM参数-XX:PermSize及-XX:MaxPermSize将会被忽略掉,并且在启动的时候给出警告信息。

  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致

内存分配模型

  • 绝大多数的类元数据的空间都从本地内存中分配

  • 用来描述类元数据的类也被删除了,分元数据分配了多个虚拟内存空间

  • 给每个类加载器分配一个内存块的列表,只进行线性分配。块的大小取决于类加载器的类型, sun/反射/代理对应的类加载器的块会小一些。

  • 不会单独回收某个类,如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉。这样减少了碎片,并节省GC扫描和压缩的时间。

调优

  • 使用-XX:MaxMetaspaceSize参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。

  • 使用-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小。

  • 一旦类元数据的使用量达到了“MaxMetaspaceSize”指定的值,对于无用的类和类加载器,垃圾收集此时会触发。为了控制这种垃圾收集的频率和延迟,合适的监控和调整Metaspace非常有必要。过于频繁的Metaspace垃圾收集是类和类加载器发生内存泄露的征兆,同时也说明你的应用程序内存大小不合适,需要调整。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块(2) Hmac

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索