java内存管理
java的内存托管。想一座围墙,墙内的想出去,墙外的想进来
1. 内存管理划分
不用再关心内存具体的分配和释放。jvm的内存管理,就像一个高级的内存池。帮你管理了内存的分配和释放。
程序计数器:此内存区域是唯一一个在java虚拟机规范中没有OutOtMemoryError的区域
直接内存:直接内存不是虚拟机运行时数据区的一部分,只能通过full gc进行回收。netty等NIO的框架就用到了直接内存,所有不能显示禁用gc的调用。
虚拟机栈和本地方法栈:都是方法的调用栈,线程私有,不用回收。当线程过多时会发生OutOtMemoryError。每个java方法执行时会创建一个栈桢(stack frame):用于存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表:是提前分配好的。比对进程空间中的栈空间。
方法区:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。包括运行时常量池。
java堆:java中所有对象都在堆中分配(逃逸分析,可以让对象在栈上分配)。是内存回收的主要地。
对象的创建
- 虚 拟 机 遇 到 一 条 new 指 令 时 , 首 先 将 去 检 查 这 个 指 令 的 参 数 是 否 能 在 常 量 池 中 定 位 到 一 个 类 的 符 号 引 用 , 并 且 检 查 这 个 符 号 引 用 代 表 的 类 是 否 已 被 加 载 、 解 析 和 初 始 化 过 。 如 果 没 有 , 那 必 须 先 执 行 相 应 的 类 加 载 过 程 , 本 书 第 7 章 将 探 讨 这 部 分 内 容 的 细 节 。
- 分配内存
○ 内存分配的方式
§ 指针碰撞(bump the pointer):java堆中的内存规整(用过的的在一边,没用过的再另外一边,分配空间直接移动指针) (serial,parnew等带compact过程的收集器)
§ 空闲列表(free list):在列表中找到一个足够大的。(CMS带mark-sweep的算法收集器)
○ 内存分配的同步性(c的malloc函数就会加锁)
§ cas方式同步
§ 通过每个线程的本地内存分配(-XX:+UseTLAB 来开启)
- 对象必要的设置(对象头) init指令,对象的初始化
对象的内存布局
hotspot中,对象的内存布局分为3块区域:对象头(Header)、实例数据(Instance Data)、对象填充(Padding)
对象头
- 第一部分:对象自身的运行数据(hashcode,gc分代,锁),这部分数据也叫“mark word”。
- 第二部分:类型指针(虚拟机通过这个指针来确定这个对象是哪个类的实例)
对象的访问
- 句柄方式(对象移动时(gc发生后),本地变量表不需要更改)
-
直接指针(速度快)。sun hotspot的实现方式
- gc后,suverice空间不足的时候,直接放在old generation。
- -XX:PretenureSizeThreshold=3145728 大于3m的对象会直接发配在老年代
- 长期存活的对象进入老年代 -XX:MaxTenuringThreshold = 15 默认15岁
- 空间分配担保(如果老年代不够的时候,会不会冒险,在新生代很大的时候,需要冒险 jdk6 以后不能冒险了)
2. 内存垃圾回收
gc分代
基于大部分对象都是短时对象,内存回收一般都会对内存进行分代。
永久代,现在的元空间,都是对JVM规范中方法区的实现。
T1~T3是本地线程分配缓存(Thread Local Allocation Buffer),对象的创建优先在自己的线程缓存中创建,减少内存分布的同步。
主要算法
- 标记-清除算法 直接清除不用的内存,导致过多内存碎片。
- 复制算法 一般对新生代,内存小的区域使用。保留一部分内存,在回收时,将存活的内存复制到保留内存。再一次直接清除使用过的内存。
- 标记-整理算法 比标记-清除算法,多了整理的过程,解决了内存碎片的问题。
内存回收器
垃圾回收器的权衡,主要看吞吐量还有停顿时间。
java -XX:+PrintCommandLineFlags -version
可以看到java 8服务端默认是用的Parallel GC
CMS
CMS回收器立足于gc的低停顿时间,一直是java服务器的首选回收器,但是却只能和ParNew配合。
CMS是基于标记-清除算法。不用在标记清除所有过程中都停止用户线程,只在在初始标记和重新标记的过程中需要暂停用户线程。
G1的目的就是替换掉CMS,目前java 9已经将G1换成了默认的垃圾回收器。
细节和可调参数
-XX:CMSInitiatingOccupancyFraction=75
老年代达到75%的时候,进行full gc。
-XX:UseCMSCompactAtFullCollection=true
默认是开启的,标记删除算法会留下大量空间碎片,所以在要full gc之前,会清理下内存空间
full gc
full gc 是需要避免的,他会stop world,然后对新生代和老年代都做回收。
算法实现
可达性分析
引用计数的算法无法局别循环引用的问题,所以java用的可达性分析。通过GC Roots的节点搜索引用链,来判断对象是否存活
枚举根节点
即使号称不会发生停顿的是CMS,G1,ZGC等收集器,在枚举根节点的时候,也是必须停顿的。
查找引用链
可作为GC Roots的对象:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
可以看到作为GC roots的对象基本都在非堆内存
OopMap
当用户线程停顿下来之后,其实不用一个不漏的检查完所有执行上下文和全局引用位置,虚拟机可以通过OopMap数据结构来知道哪些地方存放有对象的引用,而不是整型。
一旦类加载动作完成的时候,hotSpot就会把对象内什么偏移量是什么类型的数据计算出来。
安全点
openjdk的实现位于openjdk/hotspot/src/share/vm/runtime/safepoint.cpp
在安全点的时候,会在OopMap记录对象引用信息。还会轮休GC标志位,如果将要发生内存回收,就stop在安全点。
安全点的选定既不能太少以至于让收集器等待太久,也不能太过频繁以至于过分增大运行时的内存负荷。
通过JIT编译的代码里,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个safepoint
安全局域
解决了程序“不执行”的时候,无法进入安全点时,JVM如何执行垃圾回收。线程在block的时候,会将自己现在标志为进入安全局。当JVM发起内存回收的时候,就不用管安全局域的线程。
GC触发的时机
- 显示调用System.gc
- 当young gen中的eden区分配满的时候触发 young gc
内存调优
gc日志
加上-XX:+PrintGCDetails
参数,可以在发生内存回收的时候打印日志,是内存调优的重要手段。不同回收器,都自己实现的gc日志,但是gc日志还是有一定的共性。也有多种工具可以进行日志分析。
命令行工具
监控内存两个重要的命令jstat
和jmap
。
- jstat
主要看java程序内存的分配情况。看是否发生内存溢出。
jstat -gcutil pid
- jmap A
jmap -dump:format=b,file=dump.bin pid
dump程序的内存然后用MAT等工具进行分析。jmap -histo:live pid| more
实时统计当前进程,每个类占用的内存。jmap -heap pid
java堆的详细信息,用的哪种回收器,参数配置,分代状态等。
建议
- 将初始内存和最大内存设置成一样。 减少java堆内存调整的性能消耗。
-Xms4g -Xmx4g
- 指定合适的元空间。 这部分的空间比较确定,程序启动后不会有大的变化。可以根据项目指定合适的大小。
-XX:MetaspaceSize=256m
- 指定合适的新生代和老年代。这个比较难有确定的标准。
- 内存溢出后自动dump内存。当然更好的是有更完善的监控,提前发现内存溢出。`-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=logs/
- 合理设置新生代,减少老年代的内存回收