JVM(Java虚拟机):
Java 内存模型主要包含线程私有的程序计数器、java虚拟机栈、本地方法栈和线程共享的堆空间、元数据区、直接内存。
1、Java运行时数据区域
Java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 Java 进程产生和消失。
根据 JVM 规范,JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。
2、程序计数器(PC 寄存器、计数器)
程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。
在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都有自己的程序计数器。
可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
倘若执行的是 native 方法,则程序计数器中为空。
3、Java 虚拟机栈(JVM Stacks)
虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。
不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。
每个栈帧主要包含的内容如下:
注意:这里的基本数据类型指的是方法内的局部变量
局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
虚拟机栈可能会抛出两种异常:
若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常
若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常
3、本地方法栈(Native Method Stacks)
本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。
本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。
本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。
虚拟机栈和本地方法栈的主要区别:
4、Java 堆(Java Heap)
Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。
Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。
Java 堆的分区:
永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代
Java 堆内存大小:
主要存储的内容是:
static 修饰的静态变量,jdk8 时从方法区迁移至堆中线程分配缓冲区(Thread Local Allocation Buffer)线程私有,但是不影响 java 堆的共性增加线程分配缓冲区是为了提升对象分配时的效率。
堆和栈的区别:
5、方法区(逻辑上)
方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)
方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区
各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。
拓展:
JDK版本 | 方法区的实现 | 运行时常量池所在的位置 |
JDK6 | PermGen space(永久代) | PermGen space(永久代) |
JDK7 | PermGen space(永久代) | Heap(堆) |
JDK8 | Metaspace(元空间) | Heap(堆) |
6、元空间(元数据区、Metaspace)
元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。
元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM
元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:
类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表
常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中
运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法
7、直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。
常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。
分配、回收成本较高,但读写性能高。
直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。
1、JVM 内存(JVM 虚拟机数据区)
Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。
JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM
2、本地内存(元空间 + 直接内存)
对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。
本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。
虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM
3、堆外内存
直接内存
直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。
可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样
内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;
线程堆栈
可通过 -Xss 调整大小
内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)
OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)
Socket 缓存区
每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。
如果无法分配,可能会抛出 IOException:Too many open files异常
JNI 代码
如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存
虚拟机和垃圾收集器
虚拟机、垃圾收集器的工作也是要消耗一定数量的内存。
1、结构图(新生代、老年代、永久代)
JVM 中的堆,一般分为三大部分:新生代、老年代、永久代( Java8 中已经被移除)
2、新生代、MinorGC(Young GC)
2.1、新生代:
主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。
新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:
Eden 和 S0,S1 区的比例为 8 : 1 : 1
幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区
JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%。
当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。
2.2、MinorGC
MinorGC 的过程(采用复制算法):
Minor GC 触发机制:
当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)。
对象年龄(Age)计数器
虚拟机给每个对象定义了一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。
对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。
2.3、老年代、MajorGC(Old GC)
年代
MajorGC 采用标记-清除算法:
2.4、永久代、元数据区(元空间)、常量池
永久代(PermGen)
元数据区(元空间、Metaspace)
默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。
元空间替换永久代的原因分析:
当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。
2.5、类常量池、运行时常量池、字符串常量池
类常量池
在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池
主要存放字面量(字面量一部分便是文本字符)和符号引用
运行时常量池
在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池
(文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)
字符串常量池
存储的是字符串对象的引用,而不是字符串本身
字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)。
2.6、Minor GC、Major GC、Full GC 的区别
Full GC 触发机制:
2.7、堆空间分成不同区的原因
根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。
为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。
如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。
将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。
分两个区的好处就是解决内存碎片化。
假设现在只有一个survivor区,模拟一下流程:
新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
2.8、GC(垃圾回收)
2.8、GC常用算法(垃圾回收)
分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)
它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。
新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。
老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。
标记-清除算法
每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。
标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
优点是可以避免内存碎片。
标记-压缩(标记-整理)算法
标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。
不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。
复制算法
复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。
当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。
此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。
1、调优参数
配置方式
内存参数:
JDK5.0 以后每个线程 Java 栈大小为1M,以前每个线程堆栈大小为 256K。示例:-Xss128k :设置每个线程的堆栈大小为128k。
-XX:NewRatio=n:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值。
示例:设置为 4 :年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5
-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。
注意 Survivor 区有两个。示例:设置为 3 :表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5。
如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。
如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。
2、垃圾回收器参数
VM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。
3、元空间参数:
使用 java -XX:+PrintFlagsInitial 命令查看本机的初始化参数。
当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加 MetaspaceSize 的大小(为了避免过早引发一次垃圾回收)。默认值为40,也就是40%。
设置该参数可以控制 Metaspace 的增长的速度,太小的值会导致 Metaspace 增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长的过快,浪费内存。
指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。
1、年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8 CPU 以上的应用。
2、老年代大小选择
响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。
如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。
最优化的方案,一般需要参考以下数据获得:
吞吐量优先的应用:
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。
原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。较小堆引起的碎片问题,因为老年代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。
当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记-清除方式进行回收。
如果出现“碎片”,可能需要进行如下配置:
页面更新:2024-03-01
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号