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 的方式查看堆内存信息
[root@master1 opt]# jmap -heap 5792
Attaching to process ID 5792, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:--堆内存初始化配置
#-XX:MinHeapFreeRatio设置JVM堆最小空闲比率
MinHeapFreeRatio = 40
#-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率
MaxHeapFreeRatio = 70
#-XX:MaxHeapSize=设置JVM堆的最大大小
MaxHeapSize = 482344960 (460.0MB)
#-XX:NewSize=设置JVM堆的‘年轻代’的默认大小
NewSize = 10485760 (10.0MB)
#-XX:MaxNewSize=设置JVM堆的‘年轻代’的最大大小
MaxNewSize = 160759808 (153.3125MB)
#-XX:OldSize=设置JVM堆的‘老生代’的大小
OldSize = 20971520 (20.0MB)
#-XX:NewRatio=:‘年轻代’和‘老生代’的大小比率
NewRatio = 2
#-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
SurvivorRatio = 8
#-XX:MetaspaceSize=<value>:设置元空间的初始大小
MetaspaceSize = 21807104 (20.796875MB)
#-XX:MetaspaceSize=<value>:设置元空间的最大大小
MaxMetaspaceSize = 17592186044415 MB
#-XX:CompressedClassSpaceSize=<value>:设置元空间中Klass Metaspace的大小
CompressedClassSpaceSize = 1073741824 (1024.0MB)
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
#年轻代区内存分布,包含伊甸园区+1个Survivor区
New Generation (Eden + 1 Survivor Space):
capacity = 9502720 (9.0625MB)
used = 3302968 (3.1499557495117188MB)
free = 6199752 (5.912544250488281MB)
34.758132408405174% used
Eden Space: #伊甸区
capacity = 8454144 (8.0625MB)
used = 2348608 (2.23980712890625MB)
free = 6105536 (5.82269287109375MB)
27.780553536821706% used
From Space:
capacity = 1048576 (1.0MB)
used = 954360 (0.9101486206054688MB)
free = 94216 (0.08985137939453125MB)
91.01486206054688% used
To Space:
capacity = 1048576 (1.0MB)
used = 0 (0.0MB)
free = 1048576 (1.0MB)
0.0% used
tenured generation: # 老年代
capacity = 20971520 (20.0MB)
used = 13093296 (12.486740112304688MB)
free = 7878224 (7.5132598876953125MB)
62.43370056152344% used
14624 interned Strings occupying 1375560 bytes.
根据提供的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区上
/**
* @Author haonan.bian
* @Description 测试初始化类分配在Eden区
* @Date 2019-12-25 20:36
* //vm optition 参数 -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
**/
public class HeapEdenTest {
private static final int byteSize = 1024*1024;
public static void main(String[] args) {
byte[] bytes = new byte[40*byteSize];
/**
* Heap
* PSYoungGen total 76288K, used 44892K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
* eden space 65536K, 68% used [0x000000076ab00000,0x000000076d6d7250,0x000000076eb00000)
* from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
* to space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
* ParOldGen total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
* object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000)
* Metaspace used 3073K, capacity 4496K, committed 4864K, reserved 1056768K
* class space used 338K, capacity 388K, committed 512K, reserved 1048576K
*/
/**
* Heap
* def new generation total 78656K, used 45157K [0x00000006c0000000, 0x00000006c5550000, 0x0000000715550000)
* eden space 69952K, 64% used [0x00000006c0000000, 0x00000006c2c19628, 0x00000006c4450000)
* from space 8704K, 0% used [0x00000006c4450000, 0x00000006c4450000, 0x00000006c4cd0000)
* to space 8704K, 0% used [0x00000006c4cd0000, 0x00000006c4cd0000, 0x00000006c5550000)
* tenured generation total 174784K, used 0K [0x0000000715550000, 0x0000000720000000, 0x00000007c0000000)
* the space 174784K, 0% used [0x0000000715550000, 0x0000000715550000, 0x0000000715550200, 0x0000000720000000)
* Metaspace used 3073K, capacity 4496K, committed 4864K, reserved 1056768K
* class space used 338K, capacity 388K, committed 512K, reserved 1048576K
*/
}
}
- 少数情况下也可能会直接分配在老年代中
有些大对象会直接分配在老年代中,所谓的大对象是指需要连续内存空间的Java对象,典型的大对象就是那种很长的字符串以及数组。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于阈值的对象直接在老年代中分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制
/**
* @Author haonan.bian
* @Description 测试初始化大对象分配到老年代
* @Date 2019-12-25 20:55
* //vm optition 参数 -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3M
**/
public class HeapBigObjectTest {
private static final int byteSize = 1024*1024;
public static void main(String[] args) {
byte[] bytes = new byte[40*byteSize];
/**
* Heap
* def new generation total 78656K, used 4197K [0x00000006c0000000, 0x00000006c5550000, 0x0000000715550000)
* eden space 69952K, 6% used [0x00000006c0000000, 0x00000006c0419618, 0x00000006c4450000)
* from space 8704K, 0% used [0x00000006c4450000, 0x00000006c4450000, 0x00000006c4cd0000)
* to space 8704K, 0% used [0x00000006c4cd0000, 0x00000006c4cd0000, 0x00000006c5550000)
* tenured generation total 174784K, used 40960K [0x0000000715550000, 0x0000000720000000, 0x00000007c0000000)
* the space 174784K, 23% used [0x0000000715550000, 0x0000000717d50010, 0x0000000717d50200, 0x0000000720000000)
* Metaspace used 3074K, capacity 4496K, committed 4864K, reserved 1056768K
* class space used 338K, capacity 388K, committed 512K, reserved 1048576K
*/
}
}
如果启动了本地线程分配缓冲,将按线程有限在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 关闭逃逸分析
/**
* @Author haonan.bian
* @Description 测试逃逸分析
* @Date 2019-12-25 21:20
**/
public class EscapeTest {
public static Object obj;
public void variableEscape(){
obj = new Object(); // 发生逃逸
}
public Object methodEscape(){
return new Object(); //方法逃逸
}
public static void alloc(){
byte [] b = new byte[2];
b[0] = 1;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(int i = 0;i< 10000000;i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println(end-start); //开启逃逸 3 , 关闭逃逸 63
}
/**
* 关闭逃逸 可以看到经过多次的young GC
* [GC (Allocation Failure) [PSYoungGen: 65536K->528K(76288K)] 65536K->536K(251392K), 0.0013581 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 66064K->544K(76288K)] 66072K->560K(251392K), 0.0006352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 66080K->576K(76288K)] 66096K->600K(251392K), 0.0007234 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* 67
* Heap
* PSYoungGen total 76288K, used 44000K [0x000000076ab00000, 0x0000000774000000, 0x00000007c0000000)
* eden space 65536K, 66% used [0x000000076ab00000,0x000000076d568008,0x000000076eb00000)
* from space 10752K, 5% used [0x000000076eb00000,0x000000076eb90000,0x000000076f580000)
* to space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000)
* ParOldGen total 175104K, used 24K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
* object space 175104K, 0% used [0x00000006c0000000,0x00000006c0006000,0x00000006cab00000)
* Metaspace used 3077K, capacity 4500K, committed 4864K, reserved 1056768K
* class space used 338K, capacity 388K, committed 512K, reserved 1048576K
*/
/**
* 开启逃逸之后没有了 young GC
* 4
* Heap
* PSYoungGen total 76288K, used 7864K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
* eden space 65536K, 12% used [0x000000076ab00000,0x000000076b2ae2e0,0x000000076eb00000)
* from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
* to space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
* ParOldGen total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
* object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000)
* Metaspace used 2998K, capacity 4500K, committed 4864K, reserved 1056768K
* class space used 328K, capacity 388K, committed 512K, reserved 1048576K
*/
}