一 堆(Heap)
1、概念
一个JVM实例只存在一个堆,Java堆区在启动JVM时即被创建,其空间大小也就确定了,但可以通过参数进行调整空间大小。
是所有线程共享的一块内存区域,是“几乎”所有的对象实例分配内存的区域。
2、堆结构
Java堆是垃圾收集器管理的内存区域,也被称作“GC堆”。现代垃圾收集器大部分都是基于分代收集理论设计,细分为:
Java7及之前堆内存逻辑分为:新生代、老年代及永久代
Java8及之后分为:新生区、来老年区及元空间
3、设置堆内存大小与OOM
Java堆(年轻区和老年区)是用于存储Java对象实例,大小可以通过参数进行设置
-Xms #用于表示堆区起始内存
-Xmx #表示堆区最大内存
一旦堆区中内存大小超过“-Xmx“所指定的最大内存时将会抛出OOM异常,
查看设置的参数:方式一
jps
jstat -gc 进程ID
方式二:-XX:+ PrintGCDetails
可视化工具:自带的jconsole
4、年轻代和老年代
Java堆区进一步细分分分为:年轻代(YoungGan)和老年代(OldGen),其中年轻代可以划分为:Eden空间、survivor0 空间和 survivor1空间。
5、对象分配过程
(1)new的对象先放在eden区,当Eden满时JVM的垃圾回收起将对Eden进行垃圾回收,将不再被其他对象所用的对象进行销毁。
(2)然后Eden区中的剩余对象移动到幸存者S0区
(3)如果再次触发垃圾回收,此时上次幸存者下来的放到幸存者S0区,如果没有回收就会放到幸存者S1区
(4)如果再次经历垃圾回收,此时会重新放回幸存者S0区,接着再去幸存者1区
(5)啥时候区养老区?可以设置次数,默认15次
-XX:MaxTenuringThreshold=<N>
总结:
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
6、常用优化工具
JDK命令行、Jconsole、VisualVM、Jprofiler、Java Flight Recorder、GCViewer、GC Easy
7、Minor GC、Major GC 及Full GC的对比
(1)针对hotspot VM的实现,它里面的GC按照回收区域又分为两大类型:部分收集(Partial GC)和整堆收集(Full GC)
部分收集:不是完整收集整个Java堆垃圾收集,其中又分为:
-
新生代收集(MinorGC/YoungGC):只是新生代(Eden和S0,S1)的垃圾收集
-
老年代收集(MajorGC/OldGC):只是老年区代垃圾收集。
- 目前只有CMS GC会单独收集老年代的行为。
- 注意:很多时候MajorGC会和FullGC混淆使用,需要具体分辨是老年代回收还是正堆回收
-
混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有G1GC会有这种行为
整堆收集(FullGC):收集整个Java堆和方法区垃圾收集
(2)年轻代GC(MinorGC)触发机制
-
当年轻代空间不足时就会触发MinorGC,这里的年轻代满指的是Eden代满,survive满不会触发GC
-
因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
-
Minor GC会引发 STW,暂停其他用户线程等垃圾回收结束,用户线程才会恢复运行。
(3)老年代GC(MajorGC/FullGC)触发机制
-
当老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
-
Major GC 的速度比MinorGC慢10陪以上,导致STW的时间更长
-
如果Major GC后内存还不足,就报OOM
(4)FullGC(后面细讲)
- 触发Full GC执行的情况有如下五种:
- 调用system.gc() 时,系统建议执行Full GC,但不是必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space 0(From space)区向survivor space1(To space)区复制时,对象大小大于Tospace可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
说明:Full GC 是开发或调优中尽量避免的
GC举例及日志分析
源码:
import java.util.ArrayList;
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
ArrayList<String> list = new ArrayList<>();
String str = "cathax";
while (true) {
list.add(str);
str = str + str;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数:" + i);
}
}
}
参数设置:
-Xms9m -Xmx9m -XX:+PrintGCDetails
8、对象分配过程:TLAB
堆区是线程共享区域,任何线程 都可以访问到堆中的共享数据。由于对象实例的创建在JVM中非常频繁,因此在开发环境下堆区 中划分内存空间是线程不安全的,为了避免多个线程操作同一个地址,需要使用加锁等机制,进二影响分配速度。
9、总结:堆空间的参数设置
-Xms:初始化堆空间
-Xmx:最大堆空间内存
-Xmn:设置新生代的大小
-XX:NewRatio:设置新生代与老年代在堆结构的占比
-XX:SurvivorRatio:设置新生代中Eden区和S0/S1空间比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX:+PrintGGCDetails:输出详细的GC处理日志
-XX:+PrintGC:打印GC简要信息
-verbose:gc:打印GC简要信息
-XX:HandlePromotionFailure:是否设置空间分配担保
10、堆空间分代思想
堆分代目的优化GC性能,
二、方法区
1、栈、堆、方法区的交互关系
Person person = new Person();
Person存放方法区
person存放栈
new Person()存放Java堆中
2、方法区的理解
方法区与Java堆一样都是各个线程共享的内存区域,方法区在JVM启动时候被创建,并且它的实际的物理内存空间中和 Java堆区一样都可以是不连续的,空间大小可以选择固定或者可扩展的。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机会抛出溢出异常。
3、HostPot中方法区的演进
在JDK7及以前习惯把方法区称为永久代,到JDK8开始使用元空间取代了永久代了
4、设置方法区大小与OOM
JDK 7及以前:
通过-XX:PermSize来设置永久代初始分配空间,默认值是20.75M
-XX:MaxPermSize来设置永久代最大可分配空间,32位机器默认是64M,64位机器默认是82M
如果JVM加载的类信息容量超过了这个值会抛出OOM:PermGen space
JDK8开始
元数据大小可以使用参数-XX:Metaspacesize和-XX:MaxMetaspaceSize指定代替JDK7及之前的参数
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
5、举例方法区OOM异常
6、如何解决OOM
(1)通过内存映像分析工具对DUMP出来的堆转储快照进行分析,分析到底是内存泄露(Memory Leak)还是内存溢出(Memory OverFlow)
(2)如果是内存泄露,我们可以通过工具查看泄漏对象到GC Roots的引用链,这样可以找到泄漏的对象是怎么样的路径与GC Roots相关联导致垃圾收集器无法自动回收。
(3)如果不存在内存泄漏,内存中的对象确实还必须存活着,那就检查堆参数(-Xmx 与 -Xms)
7、方法区内部结构
方法区主要存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓冲等。
常量池中有:数量值、字符串值、类引用、字段引用、方法引用
运行时常量池:是方法区的一部分
常量池是class文件的一部分,用于存放编译期生成的各种字面亮与符号引用,这部分内容将在类加载放到方法区的运行时常量池中。
8、JDK7 、JDK8的区别
Hotspot虚拟机中方法区的变化:
JDK6及之前,有永久代(Permanent generation),静态变量存放在永久代上
JDK7有永久代,但是已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
JDK8及之后,没用永久代、类型信息、字段、方法区、常量保存在本地内存的元空间,但是负资产常量池、静态变量仍在堆中
9、永久代为什么换成元空间?
在某种场景下,如果动态加载类过多,就很容易产生Perm区的OOM,就比如Web工程因为功能点比较多,在运行时要不断动态加载很多类,这样就容易出现OOM。元空间和永久代之间最大区别是:元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制。还有永久代调优比较困难,空间大小很难确定。
10、方法区的垃圾收集
方法区的垃圾收集主要回收两部分:常量池中废弃的常量和不再使用的类型
三、对象的创建过程
对象创建步骤:
- 判断对象对应的类是否加载、链接、初始化:当我们通过new对象,它首先查看这个指令的参数能否在metaSpace的常量池中定位到一个类的引用符号,且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有加载那么就在双亲委派模式下使用当前类加载器以classLoader+包名+类名为Key进行查找对应的class文件,如果没有找到文件会抛出classNotFountException异常,如果找到则进行加载并生成对应class类对象。
- 为对象分配内存
- 处理并发安全问题(因为堆是共享的,所有可能会出现并发问题):采取CAS失败重试,区域加锁保证更新的原子性 和 每个线程预分配一块TLAB-通过
-XX:+/-UseTLAB
参数来设定。 - 初始化分配到的空间:所有属性默认值,保证对象实例字段在不赋值是可以直接使用
- 设置对象的对象头:就是将对象的hashCode和对象 GC信息、锁信息等数据存储在对象头
- 执行init方法进行初始化
简介步骤:
1、加载类元信息;2、为对象分配内存;3、处理并发问题;4、属性默认初始化;5、设置对象头信息;6、属性显示初始化、代码块中初始化、构造器初始化
对象在堆中内存布局
-
对象头(header):
- 运行时元数据(Mark Word):包括哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
- 类型指针:指向类元数据instanceKlass,确定改对象所属的类型
-
实例数据(Instance Data)
-
对齐填充
四、对象访问定位
JVM是如何通过栈桢中的对象引用访问到其内部的对象实例?
更多收录于GitHub:https://github.com/metashops/GoFamily
Go学习路线