1. Java内存结构
JVM内存结构主要有三大块:堆内存、方法区和栈。
堆 :存放 new 出来的对象和数组,堆是JVM中最大的一块,由年轻代和老年代组成,而年轻代内存又被分为三部分:Eden空间、Form Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1 的比例来分配。
方法区:存储已经被虚拟机加载的类的信息、常量、静态变量、编译后的代码等数据,是线程共享的区域,与Java堆区分,方法区还有一个别名Non-Heap(非堆)。
栈: 又分为Java虚拟机栈和本地方法栈主要用于方法的执行。
Java8之后方法区发生了一些变化:
- 移除了永久代(PermGen),替换为元空间(Metaspace);
- 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
- 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
- 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
再通过一张图来了解如何通过参数来控制各个区域的内存大小:
控制参数:
| 参数 | 说明 |
|---|---|
| -Xms | 设置堆的最小空间大小。 |
| -Xmx | 设置堆的最大空间大小。 |
| -XX:NewSize | 设置年轻代最小空间大小。 |
| -XX:MaxNewSize | 设置年轻代最大空间大小。 |
| -XX:PermSize | 设置永久代最小空间大小。 |
| -XX:MaxPermSize | 设置永久代最大空间大小。 |
| -Xss | 设置每个线程的堆栈大小。 |
没有直接设置老年代的参数,但是可以设置堆空间大小和年轻代空间大小两个参数来间接控制。
老年代空间大小 = 堆空间大小 – 年轻代空间大小
从更高的一个纬度再来看JVM和系统调用之间的关系
方法区和堆是所有线程共享的内存区域,而 Java栈、本地方法栈 和 程序计数器是运行在线程私有的内存区域。
再通过另外一张图来观察不同的内存区域抛出不同类型的异常
2.Java堆(Heap)
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中**最大**的一块。Java堆是被所有线程共享的一块内存,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此有很多时候被称为“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:年轻代和老年代,其中年轻代包括Eden空间、From Survivor空间、To Survivor空间三个部分。
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是可连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
我们可以通过 jmap -heap pid 的方式查看堆内存信息
1 |
|
根据提供的Java堆内存信息,可以得出以下分析:
- **堆内存初始化配置:**根据堆内存初始化配置信息,可以看到堆的最小空闲比率(MinHeapFreeRatio)设置为40%,最大空闲比率(MaxHeapFreeRatio)设置为70%,最大堆大小(MaxHeapSize)为482344960字节(460.0MB)。此外,还可以看到年轻代(New Generation)的默认大小(NewSize)为10485760字节(10.0MB),最大大小(MaxNewSize)为160759808字节(153.3MB),老年代(tenured generation)的大小(OldSize)为20971520字节(20.0MB)。
- **年轻代内存分布:**根据年轻代区内存分布信息,可以看到伊甸园区(Eden Space)的容量为8454144字节(8.0625MB),已使用3302968字节(3.1MB),剩余6105536字节(5.8MB)。Survivor区的容量为1048576字节(1.0MB),已使用954360字节(0.9MB),剩余94216字节(0.1MB)。
- **老年代内存分布:**老年代的容量为20971520字节(20.0MB),已使用13093296字节(12.4MB),剩余7878224字节(7.5MB)。
- **字符串池占用:**有14624个字符串池对象,占用1375560字节。
通过这些信息,可以了解到堆内存的配置和使用情况,包括年轻代和老年代的容量、使用情况,以及字符串池的占用情况。这些信息有助于分析内存使用情况和性能调优。
2.1 Java对象分配规则
- 对象主要分配在新生代的Eden区上
1 | /** |
- 少数情况下也可能会直接分配在老年代中
有些大对象会直接分配在老年代中,所谓的大对象是指需要连续内存空间的Java对象,典型的大对象就是那种很长的字符串以及数组。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于阈值的对象直接在老年代中分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制
1 | /** |
如果启动了本地线程分配缓冲,将按线程有限在TLAB上分配
2.2 GC参数指定垃圾回收
-Xms20M、-Xmx20M、-Xmn10M 这三个参数限制了Java堆大小为20M,不可扩展,其中 10MB分给新生代,剩下的10MB 分配给老年代,
-Xx:SurvivorRatio=8 配置说明新生代中Eden 区与两个Survivor的空间比例是8:1:1
2.3 年轻代
2.3.1 Eden
Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的survivor区域。
2.3.2 Survivor
Survivor Space幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象。Survivor有两个,分别为 To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。
Eden Space 和 Survivor Space 都属于年轻代,年轻代中执行的垃圾回收被称之为Minor GC(因为是对年轻代进行垃圾回收,所以又被称为Young GC),每一次 Young GC 后留下来的对象age加1。
2.4 老年代
老年代 (Old Generation Space),用于存放年轻代中经过多次垃圾回收仍然存活的对象,也有可能是年轻代分配不了内存的大对象会直接进入老年代。经过多次垃圾回收都没有被回收的对象,这些对象的年代已经足够老(old)了,就会放入到老年代。
当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC。
heap区即堆内存,整个堆大小 = 年轻代大小 + 老年代大小。堆内存默认为物理内存的1/64(<1GB);默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以通过MinHeapFreeRatio参数进行调整;默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以通过MaxHeapFreeRatio参数进行调整。
2.5 GC 模式
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC:并不收集整个GC堆的模式
- Young GC:只收集新生代的GC
- Old GC :只收集老年代的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个新生代以及部分老年代的GC。只有G1有这个模式
- Full GC:收集整个堆,包括新生代、老年代、永久代(如果存在的话)等所有部分的模式。
2.5.1 Fulll GC
又叫”Major GC”,Full GC 收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
当准备要触发一次Minor GC时,如果发现统计数据说之前Minor GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Minor GC而是转为触发Full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代n,所以不需要事先触发一次单独的Minor GC);或者,如果有永久代的话,要在永久代分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。
Major GC和Full GC的区别是什么?
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说 “Major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC。FullGC 触发条件
- 调用 System.gc():此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下,它会触发Full GC。因此强烈建议不能使用此方法就不要使用,让虚拟机自己去管理它的内存。可以通过 -XX:DisableExplicitGC 来禁止RMI调用System.gc()
- 老年代空间不足:老年代空间不足是常见场景为前面讲的大对象直接进去老年代,长期存活的对象进去老年代等,当执行Full GC 空间仍然不足的时候,就会抛出Java.lang.OutOfMemory。为避免以上原因引起的Full GC,调优时应该做到让对象在Minor GC 阶段被回收,让对象在新生代多存活一段时间以及不要创建过大的对象以及数据。
- 空间分配担保失败:使用复制算法的Minor GC 需要老年代的空间做担保,如果出现了HandlePromotionFailure担保失败,则会触发Full GC
2.5.2 Minor GC
又叫 “Young GC”,只收集新生代的GC。
- Minor GC 触发条件
当Eden区满时,就会触发MinorGC。注意新生代中有部分存活对象会晋升到老年代,所以Minor GC后老年代n的占用量通常会有所升高。
2.6 逃逸分析和栈上分配
- 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:
- 方法逃逸:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸
- 线程逃逸:当一个对象在方法中被定义后,它可能被外部线程访问到,譬如复制给变量或可以在其他线程中访问的实例变量,称为线程逃逸。
- 栈上分配
栈上分配就是把方法中的变量和对象分配到栈上,方法执行完成后自动销毁,不需要垃圾回收的介入,从而提高系统性能。
-XX:+DoEscapeAnalysis 开启逃逸分析(jdk 1.8 默认开启)
-XX:-DoEscapeAnalysis 关闭逃逸分析
1 | /** |