- JVM内存结构
- 一、JVM内存结构图
- 二、关于JVM的内存结构
- 1、虚拟机栈
- 2、本地方法栈
- 3、方法区
- 4、程序计数器
- 5、堆
- 三、调整内存的参数
- 四、对象的分配
- 五、GC发生的时机
- 六、代码优化
借鉴文章: (61条消息) JVM原理最全、清晰、通俗讲解,五天40小时吐血整理_小爷欣欣-CSDN博客_jvm详解 一、JVM内存结构图
首先直观的通过下图来认识一下JVM的运行时内存结构:
JVM使用的内存有可能是在操作系统的堆内存中(博客中谈及,个人未探究)。
二、关于JVM的内存结构 1、虚拟机栈 简单的来说,就是一个栈结构,用来存储指令,运行指令,操作局部变量,记录方法引用和返回地址等。虚拟机栈线程私有,在线程调用start()方法时,会开辟一个栈与之对应。
虚拟机栈的栈帧还细分为以下结构:局部变量表,操作数栈,动态链接,方法返回地址和附加信息。
局部变量表:存储方法中声明以及传入的作用域仅在方法内的变量。
操作数栈:用于执行相应指令的栈。
动态链接:具体调用的方法的变量。
方法返回地址:记录栈帧出栈之后接下来要执行的指令位置。
异常:StackOverflowError,OutOfMemaryError
指令执行时,虚拟机栈的变化如下:
2、本地方法栈 与虚拟机栈类似,用于执行本地方法。
3、方法区(1)在虚拟机启动的时候创建。
(2)所有jvm线程共享。
(3)除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。
(4)用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。
(5)被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
(6)运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
方法区在JDK7中基于分代理论进行实现(永久代),JDK8中更改为元空间(直接内存)。
永久代:
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。
原因如下:
a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满。
说明:
Jdk1.6及之前:常量池分配在永久代 。
Jdk1.7:有,但已经逐步“去永久代” 。
Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
异常:OutOfMemaryError
4、程序计数器 也叫PC寄存器,是一块较小的内存空间,线程私有的,因为切换上下文之后需要使用PC判断该线程执行到了哪一条指令,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(1)区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
异常:无
5、堆 JVM中GC回收的主要区域,线程共享的一块区域,java中的各种对象以及数组等,都在堆上开辟空间。堆空间被分为俩部分,年轻代和老年代,其中年轻代又分为Eden、SurvivorFrom和SurvivorTo默认比例为8:1。
异常:OutOfMemaryError
堆内存分代模型图1:
堆内存分代模型图2:
三、调整内存的参数| 参数内容 | 参数作用 |
|---|---|
| -Xms | 设置初始分配大小,默认为物理内存的“1/64” |
| -Xmx | 最大分配内存,默认为物理内存的“1/4” |
| -Xmn | 对-XX:newSize、-XX:MaxnewSize两个参数同时进行配置(注意:JDK1.4之后才有该参数)。 |
| -Xss | 每个线程栈的大小 |
| -XX:NewSize | 设置年轻代的大小 |
| -XX:MaxNewSize | 设置年轻代的最大大小 |
| -XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值,-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 |
| -XX:SurvivorRatio | Eden和Survivor大小,默认设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。 |
| -XX:PermSize | 设置持久代的初始值,物理内存的1/64。 |
| -XX:MaxPermSize | 设置持久代的最大值,物理内存的1/4。 |
| -XX:+/-PrintFlagsInitial | 查看所有参数默认值。 |
| -XX:+/-PrintFlagsFinal | 查看所有参数最终值,修改之后的值。 |
| -XX:PrintGCDetails | GC处理日志详情。 |
优先在Eden区域分配,Eden区域内存充足时直接分配。当Eden区域内存不足,无法为当前对象分配内存时,进行MinorGC/YGC,将存活对象移动至SurvivorFrom区域,清理无用对象,下一次YGC将存活对象移动至SurvivorTo区域,清理无用对象依次循环这个过程。当回收年轻代之后,内存满足要求直接分配。在YGC过程中,如果Survivor区域无法存放存活对象时,直接将大龄对象存放到老年代中,分配担保。回收之后,Eden区域内存依旧无法满足分配条件时,查看老年代是否有足够的内存存放对象,老年代可以存放则分配内存,老年代内存不足则进行MajorGC,若进行GC之后还是无法存放则抛出OOM错误。
TLAB:JVM会为每一个线程分配一个私有缓存区域,仅占整个Eden的1%。分配内存时,JVM会将TLAB作为首选,分配失败通过加锁机制在堆上进行分配。
五、GC发生的时机一、MinorGC一般指清理Young space(Eden and Survivor spaces)的GC。例如G1GC还有 ShenandoahGC中的YoungGC触发一般是:
1、Allocation Failure:分配对象失败,空间不足,内存分配流程涉及到了 bump-the-pointer,TLAB,Allocation Prematch这些机制。
2、Survivor区满了,需要拷贝。不同的GC还会有自己个性化的触发机制,例如G1GC还有Shenandoah GC的TLAB分配失败剩余空间大于最大浪费空间直接在Eden分配也失败,ZGC的预热触发等等。
二、OldGC一般指清理Tenured space的GC。例如G1GC还有ShenandoahGC中的 OldGC一般由MinorGC触发,并且回收的空间依然不足,则可能触发OldGC。还有一些特殊的机制,例如G1GC的Homongous Allocation(大对象分配)在分配超过 RegionSize 一半大小的对象时,会触发OldGC。
三、FullGC一般指清理 所有space的GC(整堆收集),触发时机一般是:
1、System.gc()被调用并且没有指定关闭显示GC,就是没有指定-XX:+DisableExplicitGC这个JVM flag。
2、老年代满了。
3、堆外内存满了,例如metaspace,代码即时编译缓存,直接内存,mmap内存。
4、gc 担保失败,请参考:-XX:-HandlePromotionFailure。
六、代码优化一、逃逸分析
JIT即时编译会进行逃逸分析,逃逸分析就是分析一个对象的引用范围,从而决定是否要将对象分配到堆上。
当一个对象在方法中被定义,只在方法内使用,则认为未发生逃逸。
当一个对象在方法中被定义,且被外部引用,认为发生了逃逸。
认为未发生逃逸,则将对象分配到栈上,发生了逃逸则将对象分配到堆上。
二、以逃逸分析为基础,有一下的代码优化策略:
1、栈上分配:将对象分配在栈上,无需GC,提高性能。
2、同步省略:动态编译同步代码块时,JIT可以通过逃逸分析判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程,如果只有一个线程访问,则取消对该部分代码的同步。
3、标量替换:先来看什么是标量,标量就是指一个无法再分解成更小数据组成的数据。可以再分解的为聚合量。逃逸分析阶段发现一个对象不会被外界访问,经过JIT的优化,会将该对象拆分成若干标量来替换该对象。
-XX:+/-EliminateAllocations 开关标量替换
-XX:+/-DoEscapeAnalysis 开关逃逸分析



