JVM内存结构和堆调优

开局先放一张JVM内存结构图

JVM内存结构

类加载器

负责加载class文件(class文件在文件开头有特定的文件标识)将class文件字节码内容加载到内存,并将这些内容转换成方法区中的运行时数据结构。

注意:CLassLoader只负责加载,至于它是否可以运行,则由Execution Engine决定。

ClassLoader分类

java虚拟机自带的:

  • 启动类加载器(BootStrap) C++
  • 扩展类加载器(Extension) Java
  • 应用程序类加载器(AppClassLoader)Java

(也叫系统类加载器,加载当前应用的classpath的所有类
以上负责加载各自录下下的类。)
用户自定义的:
Java.lang.ClassLoader的子类,用户可以定制类的加载方式

本地方法栈(线程私有)

Native方法放在本地方法栈中。native方法就是调用操作系统或c语言等第三方库,因为java出来的时候c语言毕竟火爆,当时为了立足,必须去调用c。

程序计数器(线程私有)

记录下一条指令的地址。

方法区(线程共享)

存储了每个类的结构信息,例如运行时常量池、字段和方法数据、构造方法和普通方法的字节码内容。以上是规范,在不同虚拟机里具体实现是不一样的,最典型的是永久代(jdk7)和元空间(jdk8)。

stack栈

栈管运行,堆管存储。栈主管java程序的运行,是在线程创建时创建,跟随线程的生命周期,线程结束栈内存就释放。栈不存在垃圾回收。
栈保存的内容:8种基本类型的变量+对象的引用变量+实例方法。
方法(java)==》栈帧(JVM)
栈帧主要保存三类数据:

  • 本地变量:输入参数和输出参数以及方法内的变量
  • 栈操作:记录出栈入栈的操作
  • 栈帧数据:包括类文件、方法等

堆(heap)

堆中保存着对象和数组。是垃圾收集器进行垃圾回收最重要的区域。
堆由三部分组成:

  • 新生代
    • 伊甸区(Eden Space)
    • 幸存0区(Survivor 0 Space)
    • 幸存1区(Survivor 1 Space)
  • 老年代(Tenure Generation Space)
  • 永久代(jdk7之前)

堆分代空间描述

新生代

伊甸区

Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老
年代)。当 Eden区内存不够的时候就会触发 MinorGC,对新生代区进行
一次垃圾回收。

ServivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

ServivorTo

保留了一次 MinorGC 过程中的幸存者。

其中MinorGC过程(复制-清空-交换)如下:

  1. eden、 servicorFrom 复制到 ServicorTo,年龄+1

    首先,第一次在eden区满的时候出触发第一次Minor GC, 把活着的对象拷贝到ServivorFrom区, 当eden区再次触发GC时会把 Eden 和 ServivorFrom 区域中存活的对象复制到 Ser vicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);

  2. 清空 eden、 servicorFrom

    然后,清空 Eden 和 ServicorFrom 中的对象;

  3. ServicorTo 和 ServicorFrom 互换

    最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom
    区。

总体大致流程

新生区是类诞生,成长,消亡的区域,又分为Eden Space和幸存者0区和幸存者1区。在类对象在eden Space中被new出来,导致该区域空间不足时,会触发Minor GC,将该区域中不再被其他对象引用的对象销毁,然后将剩余的对象移动到幸存者0区。若幸存者0区也满了,则对幸存者0区再进行Minor GC,然后将剩余对象移动到幸存者1区。

如果幸存者1区也满了,则移动到老年区。如果老年区也满了,那么会触发Major GC,对老年区进行内存清理。

若执行了Major GC后仍然无法对对象进行保存,就会产生OOM异常。

永久代(jdk7之前)

永久区用于存放JDK自身所携带的Class,Interface的元数据。它存储着运行环境必须的类信息,被装载进此区域的数据不会被垃圾回收,关闭JVM才会释放此区域的内存。

堆调优

堆逻辑上分为新生代,老年代和方法区(永久代java7或元空间java8),物理上则只有新生代和老年代。堆空间描述信息

参数解释

JVM Heap

  • -Xms 堆初始值大小
  • -Xmx 堆最大值大小

Young Gen

  • -Xmn 新生代大小

-XX:PermSize 永久代初始值
-XX:MaxPermSize 永久代最大值

JDK8堆空间描述
除了永久代(在虚拟机中)变成了元空间,其他与java7一致,而且元空间不在虚拟机中,而是使用本机物理内存。

因此,默认情况下,元空间大小仅受本地内存限制,类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而是由系统的实际可用空间来控制。

堆内存调优演示

堆内存调优演示

图为java7,java8只需要将蓝色处的PsPermGen换为元空间MetaSpace。

GC收集日志信息

YoungGC(MinorGC)
在这里插入图片描述
FullGC(MajorGC)
在这里插入图片描述
[Times: user=11.53 sys=1.38, real=1.03 secs]解释
real —— 程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。
user —— 进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
sys —— 进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间。
user + sys 时间告诉我们程序执行实际使用的 CPU 时间。注意这里指所有的 CPU,因此如果在进程里有多个线程的话,这个时间可能会超过 real 所表示的时钟时间。

Java GC 时间
上面我们解释了三个时间的概念,接下来我们用一些例子来更好地说明这些概念:
例1:
[Times: user=11.53 sys=1.38, real=1.03 secs]

在这个例子中,user + sys 时间的和比 real 时间要大,这主要是因为日志时间是从 JVM 中获得的,而这个 JVM 在多核的处理器上被配置了多个 GC 线程,由于多个线程并行地执行 GC,因此整个 GC 工作被这些线程共享,最终导致实际的时钟时间(real)小于总的 CPU 时间(user + sys)。
例2:
[Times: user=0.09 sys=0.00, real=0.09 secs]

上面的例子中的 GC 时间是从 Serial 垃圾收集器 (串行垃圾收集器)中获得的。由于 Serial 垃圾收集器是使用单线程进行垃圾收集的,因此 real 时间等于 user 和 sys 时间之和。

总结
在做性能优化时,我们一般采用 real 时间来优化程序。因为最终用户只关心点击页面发出请求到页面上展示出内容所花的时间,也就是响应时间,而不关心你到底使用了多少个 GC 线程或者处理器。但并不是说 sys 和 user 两个时间不重要,当我们想通过增加 GC 线程或者 CPU 数量来减少 GC 停顿时间时,可以参考这两个时间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值