内存模型

程序计数器

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  • 新生代内存(Young Generation)
  • 老生代(Old Generation)
  • 永久代(Permanent Generation)

方法区

方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。  
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。

为什么永久代要换成元空间

  • 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
  • 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了
    1
    2
    3
    4
    5
    -XX:PermSize=N //方法区 (永久代) 初始大小
    -XX:MaxPermSize=N //方法区 (永久代) 最大大小

    -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
    -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
    因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

对象创建流程

类加载检查-分配内存-初始化-设置对象头-init

分配内存

  • 指针碰撞

    用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可,代表Serial, ParNew

  • 空闲列表

    虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录,代表CMS

内存并发

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

垃圾回收器

Serial收集器(新生代-复制算法)

JDK1.3.1之前是虚拟机新生代垃圾回收的唯一选择。这个收集器是一个单线程的,它进行垃圾收集时,其他工作线程会暂停

ParNew收集器(新生代-复制算法)

Serial收集器的多线程版本,可配合CMS收集器,可以使用-XX:+UseParNewGC强行指定它,或者使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,使用-XX:ParallelGCThreads来限制垃圾回收线程的数量

Parallel Scavenge收集器(新生代-复制算法)

采用复制算法,又是并行的多线程垃圾收集器。

1
2
3
-XX:MaxGCPauseMills 最大垃圾手机时间
-XX:GCTimeRatio 设置吞吐量大小
-XX:+UseAdaptiveSizePolicy 自适应

Serial Old收集器(老年代-标记整理算法)

Serial收集器的老年代版本,

Parallel Old收集器(老年代-多线程和标记整理算法)

Parallel Scavenge收集器的老年代版本,

CMS收集器(老年代-标记清除算法)

Concurrent Mark Sweep,以获取最短停顿时间为目标

  1. 初始标记 — 仅仅关联GC Roots能直接关联到的对象,速度很快;
  2. 并发标记 — 进行GC Roots Tracing的过程;
  3. 重新标记 — 为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录;
  4. 并发清除

初始标记和重新标记需要暂停用户线程,缺点

  1. CMS收集器对CPU资源非常敏感(默认启动的回收线程数是(CPU数量+3)/4)
  2. 无法处理浮动垃圾(CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后)
  3. 会有大量的垃圾碎片产生 -XX:+UseCMSCompactAtFullCollection

G1收集器(整个堆-整理)

1
2
//开启G1收集器,设置最大堆内存32G,Gc暂停时间200ms
XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

把Java内存拆分成多等份,多个域(Region),最多2048个,

优点

  1. 分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间
  2. 不会产生碎片
  3. 可预测的停顿

###回收流程

  1. 初始标记(Initial Marking),需停止线程,时间很短
  2. 并发标记(Concurrent Marking),利用可达性分析,可与用户程序并发执行
  3. 修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,将线程的Remembered Set Logs里面数据合并到Remembered Set(记忆集,跟踪对象引用),需要停顿,但可并行
    4.筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,需要停顿线程

使用G1的情况

  • 实时数据占用超过一半的堆空间
  • 对象分配或者晋升的速度变化大
  • 希望消除长时间的GC停顿(超过0.5-1秒)

ZGC

总结

GC日志开启命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCDateStamps 输出GC的日期戳
-Xloggc:/var/log/hbase/gc-regionserver-hbase.log GC日志输出的路径
-XX:+UseGCLogFileRotation 打开GC日志滚动记录功能
-XX:NumberOfGCLogFiles 设置滚动日志文件的个数,必须大于等于1 日志文件命名策略是,.0, .1, …, .n-1,其中n是该参数的值
-XX:GCLogFileSize 设置滚动日志文件的大小,必须大于8k
-XX:+PrintGCApplicationStoppedTime 打印GC造成应用暂停的时间
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintTenuringDistribution 在每次新生代 young GC时,输出幸存区中对象的年龄分布

oom的时候生成dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=目录
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=目录/xxx.hprof

总结如下

1
2
3
4
5
6
7
8
9
10
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:D://haha//gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=512k
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintHeapAtGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D://haha

常见设置参数

对象结构

以64位操作系统为例,new Object()占用大小分为两种情况:

  • 未开启指针压缩

占用大小为:8(Mark Word)+8(Class Pointer)=16字节

  • 开启了指针压缩(默认是开启的)

开启指针压缩后,Class Pointer会被压缩为4字节,最终大小为:

8(Mark Word) + 4(Class Pointer) + 4(对齐填充) = 16字节