目录
1、堆空间
1.1、概述
1.2、堆内存再细分
1.3、堆的大小设置
1.4、新生区和老年区
1.5、对象分配过程
1.6、各种GC
1.7、TLAB(快速分配策略)
1.8、代码优化
1.9、StringTable
1.10、常用调优工具
1.11、面试问题
2、元空间(方法区)
2.1、堆、栈、元空间的交互关系
2.2、方法区的理解
2.3、方法区空间大小设置
2.4、方法区内部结构
2.5、方法区的演化细节
PDF版笔记:JVM的学习笔记PDF版-互联网文档类资源-CSDN下载
1、堆空间
1.1、概述
- 一个JVM实例只存在一个堆内存,JVM启动时堆就被创建了,其空间大小也确定了。一般为JVM上最大最重要的空间。
- 堆空间大小可调节,调节指令 -Xms10m -Xmx20m(设置初始10m,最大20m)
- 在物理上可以是不连续,但是逻辑上需要连续的
- 所有线程共享Java堆,在这里还可以划分线程私有缓冲区(TLAB)
- 几乎所有的对象都是存储在堆中的。数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
- 在方法结束后,堆中的对象不会马上清除,而是要在垃圾回收(GC)时才会被清除。
- 堆是垃圾回收(GC)的主要区域
工具:jdk->bin->jvisualvm.exe
1.2、堆内存再细分

Java 7 及之前在逻辑上分为:新生区 + 老年区 + 永久区
Java 8 及之后在逻辑上分为:新生区 + 老年区 + 元空间
1.3、堆的大小设置
- 手动设置:“-Xms”用于设置初始内存大小、“-Xmx”用于设置最大内存大小,建议两个设置为相同大小(防止由于堆空间大小的调整过程中对系统产生不必要的压力)
- -XX:+PrintGCDetails 打印JVM堆信息
- 默认堆空间大小为物理内存的六十四分之一,最大内存为物理内存的四分之一
- jps:查看JVM下的进程及其编号。jstat -gc 编号:查看编号的进程信息
- 如果堆空间内存不足则会发生
1.4、新生区和老年区
- 配置新生区和老年区在堆结构的占比:默认-XX:NewRatio=2,表示新生区占1,老年区占2
- 新生区:默认情况下Eden:S0:S1=8:1:1,可以通过“-XX:SurvivorRatio=8”调整Eden区占比,需要显式地使用指令调整才能够完全看到8:1:1
- 几乎所有的Java对象都是在Eden区new出来的,除了超大型对象(大过Eden区),绝大多Java对象都是在Eden区销毁的
- “-Xmn”设置新生区的空间大小,一般不设置
1.5、对象分配过程

- new的对象放在Eden区,此区域有大小限制
- 当Eden区满的时候,程序又需要创建对象,则JVM的垃圾回收器会对Eden区进行垃圾回收(YGC/Minor GC),将不再引用的对象销毁,放入新对象到Eden区。
- 将Eden存活的对象和幸存区存活的对象放至另一个为空的幸存区,并标记age为1。
- 幸存区的对象每经历一次垃圾回收(YGC/Minor GC),不再引用的对象被销毁,幸存的对象的age加1。若幸存区中相同年龄的所有对象大小的总和 > 幸存区空间的一半,则年龄 >= 该年龄的对象,可直接进入老年区。(动态对象年龄判断)
- 同时会将该幸存区的对象放至到另一为空的幸存区。并置空。
- 当age大于阈值(可设置,“-XX:MaxTenuringaThreshold=N”,默认15),则晋升老年区。
- 当老年代内存不足时触发垃圾回收(FGC/Major GC),若垃圾回收后内存空间还不足则触发OOM(内存溢出)异常
1.6、各种GC
JVM在进行GC时,并非时每次都堆新生区、老年区、方法区一起进行回收
- Minor GC:针对新生区的垃圾回收(Eden、幸存区),当Eden区满的时候触发GC,幸存区满的时候不会触发GC,但是触发的GC会处理幸存区。速度比较快,频率比较高。
- Major GC:针对老年区的垃圾回收。老年代空间不足时,先尝试触发Minor GC,如果之后空间还是不足,则触发Major GC。性能比Minor GC慢10倍以上
- Mixed GC:针对新生区和部分老年区的垃圾回收(目前,只有G1 GC存在这种行为)
- Full GC:针对整个Java堆和方法区的垃圾回收 。当调用System。gc()(不必然执行)、老年代空间不足、方法区空间不足、通过Minor GC后进入老年代的平均大小 > 老年代可以内存、Eden区和From区向To区复制时,对象大小 > To区可以空间,则把该对象放至老年区,且老年区可用内存大小 < 该对象大小
1.7、TLAB(快速分配策略)
原因:由于堆是共享的,存在线程不安全的现象。加锁虽然能保证线程的安全,但是降低了效率。
原理:在Eden为每个线程开辟一个私有缓冲区(仅占Eden 1%),线程的对象优先在缓冲区进行加载,加载满了之后,再在公共区域加载。
优势:在多线程分配内存时,使用TLAB可以避免一些了的非线程安全问题,同时还能提升内存分配的吞吐量
使用:默认是开启的,“-XX:TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。

1.8、代码优化
- 栈上分配:如果没有发生逃逸的方法内对象,则可以再栈上分配
- 同步省略:如果一个对象被发现只能从一个线程访问到,那么对应这个对象的操作可以不考虑同步
- 分离对象或标量替换:有的对象可能不需要作为连续内存结构存在也可以访问到,那么对象的部分可以不存储在CPU寄存器中。比如把一个不逃逸的对象里面的两个标量,作为两个局部变量存储在栈中。
1.9、StringTable
基本特效
- String声明未final,不可被继承
- 实现了Serializadble,表示字符串支持序列化;实现了Comparable,表示String可以比较大小
- JDK8中,字符串用char[]数组存储的,JDK9中,字符串用byte[]数组存储。在使用过程中发现大部分存储的是拉丁字符,拉丁字符又只需要一个字节,导致其中一大部分的内存被浪费。所以修改将字符串使用byte数组存储。并补充了一个字符集编码的标识,如拉丁字符用一个字节去存;如果不是,则用两个字节去存。
- String代表不可变的字符串序列,检查不可变性。
- 字符串常量池中是不会存储相同内容的字符串的。其长度固定(JDK8 最小可设置为1009,JDK7 默认60013)“-XX:StringTableSize”设置StringTable长度,若存入的字符串过多,则会
字符串拼接
- 常量与常量拼接,结果放在常量池。原理是编译器优化。
- 常量池中不会存在相同内容的常量。
- 只要其中有一个是变量(不能被final修饰),结果就在堆中,变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,主动将常量池中还没有的字符串对象放入池中。并返回此对象地址。
intern()
将这个字符串对象尝试放入串池。如果串池中有,则返回已有的串池中的对象的地址;如果没有,JDK6中,会把此对象复制一份,放入串池,并返回串池中的对象地址,JDK7中,会把此对象的地址复制一份,放入串池,并返回串池中的引用地址。
1.10、常用调优工具
JDK命令行、Eclipse:Memory Analyzer Tool、Jconsole、VisualVM、Jprofiler、Java Flight Recorder、GCViews、GC Easy
1.11、面试问题
Q:为什么把Java堆分代(区)?不分就不能正常工作了吗?
A:其实不分区也是可以工作的,分区的唯一目的就是为了优化GC的性能。如果没有分区,则所有的对象放在一起,每次GC都需要处理整块堆空间;而进行分区后,只需要子啊Eden区域进行频繁的GC,老年区则不需要频繁的GC
Q:堆是分配对象的唯一选择吗?
A:如果一个对象进行逃逸分析,发现其并没有逃逸出方法,则将该对象优化到栈上分配
2、元空间(方法区)
2.1、堆、栈、元空间的交互关系
.class文件本身需要放到方法区,在new一个对象后,该对象会将到该对象类型数据的指针作为数据保存在对象的对空间中,该指针指向方法区的对象类型数据。
2.2、方法区的理解
- 在JVM规范中,逻辑上方法区是堆的一部分,但是在实现时,并不要求其必须实现GC和压缩。所以方法区可以看作一块地理于Java堆的内存空间。
- 方法区各个线程关系,生命周期与JVM同步。实际内存可不连续。可选择方法区为固定大小或动态大小。
- 如果方法区加载的类过多,JVM可能抛出OOM(内存溢出)。
发展历程:在JDK7之前被称为永久代,JDK8之后被称为元空间。JDK7之前,永久代被内置在JVM内存,但是JDK8后,其放至在物理内存中
2.3、方法区空间大小设置
JDK7及之前设置固定大小:
- 设置永久代初始分配空间:“-XX:PermSize” 默认20.75M
- 设置永久代最大可分配空间:“-XX:MaxPermSize” 默认 64M(32位) 82M(64位)
- 超出其范围则包OOM
JDK8及之后设置固定大小:
- 设置元空间初始分配空间:“-XX:MetaspaceSize”,其默认值依赖平台,Windows是21M
- 设置元空间最大可分配空间:“-XX:MaxMetaspaceSize” 默认-1 即没有限制
- 默认情况下,会一直用完物理内存的所有可用内存,JVM才会宝OOM
- 初始的元空间大小被设置位一个高水位线,当元空间内存触及这个高水位线,将会触发Full GC,同时重置该高水位线。若Full GC是否的空间不足,那么在不超过元空间最大值的情况下,适当提高该高水位线值;如果释放的空间过多,则使得降低该值。为了避免频繁的触发Full GC,应该将该高水位线适当调高。
2.4、方法区内部结构
存储内容:类型信息(类信息、属性信息、方法信息)、常量、静态变量、即时编译器编译后的代码缓存(JIT)
运行时常量池:
- 将字节码文件里面的常量池通过加载到方法区之后,即变为了运行时常量池
- 常量池表包含了各种字面量和对类型、域和方法的符号引用
- 通过使用常量池,节省空间
- JVM为每个已加载的类维护一个常量池
2.5、方法区的演化细节
- 只有Hotspot才有永久代,BEA JRockit、IBM J9来说,只有元空间
- JDK1.6 有永久代,静态变量存放在永久代上 JDK1.7 有永久代,但是已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 JDK1.8之后,无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但是字符串常量池、静态变量仍在堆
取消永久代的原因:
- 永久代设置空间的大小是很困难的
- 对永久代进行调优是很困难的
字符串常量池(StringTable)为什么要调整:
因为永久代回收效率很低,而字符串又会被大量创建,这就导致StringTable的回收下路不高,导致OOM,放到堆里能够及时回收内存
方法区的垃圾收集的主要内容:常量池中废弃的常量和不再使用的类型
常量回收:只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃处理与回收堆中的对象类型
类的回收:
- 该类所有的实例已经被回收,也就是堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收,通常很难达成
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过在任何地方通过反射访问该类的方法