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日志还是有一定的共性。也有多种工具可以进行日志分析。

命令行工具

监控内存两个重要的命令jstatjmap

  1. jstat

主要看java程序内存的分配情况。看是否发生内存溢出。 jstat -gcutil pid

  1. jmap A
  2. jmap -dump:format=b,file=dump.bin pid dump程序的内存然后用MAT等工具进行分析。
  3. jmap -histo:live pid| more 实时统计当前进程,每个类占用的内存。
  4. jmap -heap pid java堆的详细信息,用的哪种回收器,参数配置,分代状态等。

建议

  • 将初始内存和最大内存设置成一样。 减少java堆内存调整的性能消耗。-Xms4g -Xmx4g
  • 指定合适的元空间。 这部分的空间比较确定,程序启动后不会有大的变化。可以根据项目指定合适的大小。-XX:MetaspaceSize=256m
  • 指定合适的新生代和老年代。这个比较难有确定的标准。
  • 内存溢出后自动dump内存。当然更好的是有更完善的监控,提前发现内存溢出。`-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=logs/
  • 合理设置新生代,减少老年代的内存回收

Updated: