内存结构
总体内存结构
名称 | 线程私有/共享 | 功能 | 大小 | 异常 |
---|---|---|---|---|
程序计数器 | 私有 | 保存当前线程执行方法 | 通常固定大小 | 不会 |
JVM栈 | 私有 | 方法的栈帧 | -Xss | StackOverflowError,OutOfMemoryError |
本地方法栈 | 私有 | 存储native方法信息 | 通常会固定大小 | StackOverflowError,OutOfMemoryError |
堆 | 共享 | 存储对象和数组 | -Xms初始堆值,-Xmx最大堆值 | OutOfMemoryError |
方法区 | 共享 | 存储类结构 常量 静态变量 | -XX参数 | OutOfMemoryError |
运行时常量池 | 共享 | 常量池运行时表示 | 从属于方法区 | OutOfMemoryError |
概览
PC 程序计数器
用来存放执行指令的偏移量和行号指示器等
Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器
存放指令位置虚拟机的运行,类似于这样的循环:
while( not end ) { 取PC中的位置,找到对应位置的指令; 执行该指令; PC ++;}
JVM Stack
每个线程私有
- Frame - 每个方法对应一个栈帧
- Local Variable Table 局部变量表存放方法参数和局部变量的区域
- Operand Stack 操作栈各种指令往栈中写入或者读取信息对于long的处理(store and load),多数虚拟机的实现都是原子的jls 17.7,没必要加volatile
- Dynamic Linking 动态连接常量池中一个对当前方法的引用https://blog.csdn.net/qq_41813060/article/details/88379473 jvms 2.6.3
- return address 方法返回地址a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方
本地方法栈
为本地方法服务
堆
- 共享,内存大户,存储所有的对象和数组
- 是垃圾收集器管理的内存区域
- 所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率
- -Xms(memory start) 初始堆值,-Xmx(memory max)最大堆值
方法区
各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 1.8之前被HotSpot使用一个永久代实现字符串常量位于永久代FGC不会清理这区域的内存回收目标主要是针对常量池的回收和对类型的卸载 但是类型卸载条件十分苛刻大小启动的时候指定,不能变
- Meta Space (>=1.8)字符串常量位于堆会触发FGC清理不设定的话,最大就是物理内存
运行时常量池
是方法区的一部分
用于存放编译期生成的各种字面量与符号引用
Java语言并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,比如String.intern()
直接内存
JVM可以直接访问的内核空间的内存 (OS 管理的内存)NIO , 提高效率,实现zero copy
一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
本机直接内存的分配不会受到Java堆大小的限制
对象的创建
- 内存分配
- 把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”
- 如果没有一块完整的空闲内存 就无法使用这种方法了
- 线程安全的保证 分配时 多个线程并发执行
- 虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
- 另外一种方式是每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB) 只有这个缓冲区用完了 才会在堆中分配
对象内存布局
- 以下分析都是基于HotSpot虚拟机
普通对象4 或 8 字节 | 4 或 8 字节 |markdord(对象标记)|class pointer(类型指针)|instance data(实例数据)|padding(对齐)数组4 或 8 字节 | 4 或 8 字节 | 4 字节 |markdord(对象标记)|class pointer(类型指针)|length(数组长度)|instance data(实例数据)|padding(对齐)
- 对象头
- 对象本身运行时的数据 哈希吗 GC标记 锁信息等等
- 类元信息:指向Class的指针
- 实例数据:实例成员变量及所有可见的父类成员变量
- 对齐填充
锁升级:无锁状态 -> 偏向锁 -> 自旋锁 -> 重量级锁
64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位,开启指针压缩后,对象头中的类型指针会被压缩成 32 位,使得对象头的大小从 16 字节降至 12 字节,虚拟机通过使用一个 32 位偏移量来表示对象引用,从而降低指针所需要的数据位数,这意味着虚拟机需要对对象的内存进行填充以及重新排列变量的顺序以实现对齐,
Hotspot开启内存压缩的规则
UseCompressedOopsClassPointers
UseCompressedOops
- 4G以下,直接砍掉高32位
- 4G - 32G,默认开启内存压缩 ClassPointers Oops
- 32G,压缩无效,使用64位
对象定位
- 句柄方式
stateDiagram-v2 state 栈 { ref --> 句柄池 } state 堆 { state 句柄池 { 实例数据指针 --> 对象实例数据 类型数据指针 --> 对象类型数据 } 对象实例数据 } state 方法区 { 对象类型数据 }
- 直接定位
stateDiagram-v2 state 栈 { ref --> 对象实例数据 } state 堆 { state 对象实例数据 { 类型数据指针 --> 对象类型数据 } } state 方法区 { 对象类型数据 }
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销
对象实例化
字节码角度
Object ref = new Object()
得到字节码:
stack=2, locals=2, args_size=1 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: return
new:
- 如果类不存在 就先进行类加载
- 为所有属性值分配内存
- 对所有属性值进行0值初始化
- 最后将指向实例对象的引用变量压入虚拟机栈顶
dup:
- 在栈顶复制实例对象的引用变量
- 复制出来的这个变量用来作为句柄调用相关方法
- 早一点的那个变量则是用来赋值
invokespecial:
- 通过上面dup复制的变量调用对象的`
` 方法
从执行步骤
- 确认类信息是否存在于metaspace 否则使用类加载器加载类 并生成相关Class对象
- 计算对象占用的内存空间(实例数据) 接下来在堆内存划分一块空间进行分配 为对象分配内存时 需要进行同步操作
- 设定成员变量的默认值
- 设置对象头 哈希吗 GC信息等等
- 执行init方法 初始化成员变量 执行初始化代码块等等
查看堆内存使用情况
- 使用jstat命令
jstat -class pid # 查看加载的类jstat -gc pid # 查看垃圾回收情况
内存分析
- jmap命令
jmap -histo pid # 查看所有对象jmap -histo:live pid # 查看所有存活对象jmap -dump:format=b,file=filename pid # 导出dump文件
- jhat分析dump文件
- JDK9以后不再提供,被Visual VM代替
jhat filename
- mat分析
内存溢出定位与分析
Java 堆溢出
- 添加运行参数
java -Xmx8m -Xms8m -XX:+HeapDumpOnOutOfMemoryError
List<Object> list = new ArrayList<>();while (true){ list.add(new Object());}
- 分析dump文件
- 如果是内存泄漏 找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们
- 如果内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置
虚拟机栈和本地方法栈移除
-Xss256k
- 减少栈内存容量 异常出现时输出的堆栈深度相应缩小
public class JVMSOFWithMinStack { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JVMSOFWithMinStack oom = new JVMSOFWithMinStack(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); // 3183 throw e; } }}
- 定义了大量的本地变量 异常出现时输出的堆栈深度相应缩小
public class JVMSOFWithMuchParams { private static int stackLength = 0; public static void test() { long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41, unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71, unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81, unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89, unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99, unused100; stackLength++; test(); unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0; } public static void main(String[] args) { try { test(); } catch (Error e) { System.out.println("stack length:" + stackLength); // 127 throw e; } }}
方法区和运行时常量池溢出
在JDK7和7之前如果大量创建String.intern或者动态类 由于类的回收条件苛刻 极有可能造成OOM
但在JDK8之后 这些问题就没有了
本机直接内存溢出
-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } }}
直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常
分析线程执行情况
jstack pid
- 死锁
两个进程互相等待对方,一直阻塞下去
Found one Java-level deadlock:============================="Thread-0": waiting to lock monitor 0x000002292014eb00 (object 0x00000000ffea7640, a java.lang.Object), which is held by "Thread-1""Thread-1": waiting to lock monitor 0x000002292014ea00 (object 0x00000000ffea7630, a java.lang.Object), which is held by "Thread-0"Java stack information for the threads listed above:==================================================="Thread-0": at Main.lambda$main$0(Main.java:24) - waiting to lock <0x00000000ffea7640> (a java.lang.Object) - locked <0x00000000ffea7630> (a java.lang.Object) at Main$$Lambda$14/0x0000000800ba4840.run(Unknown Source) at java.lang.Thread.run(java.base@13/Thread.java:830)"Thread-1": at Main.lambda$main$1(Main.java:37) - waiting to lock <0x00000000ffea7630> (a java.lang.Object) - locked <0x00000000ffea7640> (a java.lang.Object) at Main$$Lambda$15/0x0000000800ba4c40.run(Unknown Source) at java.lang.Thread.run(java.base@13/Thread.java:830)Found 1 deadlock.
JMX
MX(Java Management Extensions)是一个为应用程序植入管理功能的框架。JMX是一套标准的代理和服务,实际上,用户可以在任何Java应用程序中使用这些代理和服务实现管理