1.JVM组成部分
程序计数器(寄存器)(PC Register)
- 作用:记住下一条jvm指令的执行地址
- 特点:
- 1.是线程私有的
- 2.不会存在内存溢出
Java虚拟机栈(JVM Stacks)
线程安全问题
举例
栈内存溢出问题
1.栈帧过多导致栈内存溢出
2.栈帧过大导致栈内存溢出
本地方法栈(Native Method Stacks)
堆(Heap)
方法区(Method Area)
StringTable 特性
常量池中的字符串仅是符号,第一次用到时才变为对象 利用串池的机制,来避免重复创建字符串对象 字符串变量拼接的原理是 StringBuilder (1.8) 字符串常量拼接的原理是编译期优化 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象返回 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回
直接内存
2.垃圾回收
引用计数法
可达性分析算法
五种引用
1. 强引用 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 2. 软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身 3. 弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身 4. 虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存 5. 终结器引用(FinalReference) 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
3. 垃圾回收算法
标记清除
优点:速度较快
缺点:造成内存碎片
标记整理
优点:没有内存碎片
缺点:速度较低
复制算法
优点:不产生内存碎片
缺点:产生双倍的内存空间
分代垃圾回收
1.对象首先分配在伊甸园区域 2.新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to 3.minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行 4.当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit) 5.当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长
相关VM参数
4.垃圾回收器
1. 串行 单线程 堆内存较小,适合个人电脑 2. 吞吐量优先
让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高 3. 响应时间优先 多线程 堆内存较大,多核 cpu 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
串行
吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio(1/(1+ratio)) -XX:MaxGCPauseMillis=ms -XX:ParallelGCThreads=n
响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads -XX:CMSInitiatingOccupancyFraction=percent(内存占比) -XX:+CMSScavengeBeforeRemark
5.G1
定义:Garbage First 2004 论文发布 2009 JDK 6u14 体验 2012 JDK 7u4 官方支持 2017 JDK 9 默认 适用场景 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms 超大堆内存,会将堆划分为多个大小相等的 Region 整体上是 标记+整理 算法,两个区域之间是 复制 算法 相关 JVM 参数 -XX:+UseG1GC -XX:G1HeapRegionSize=size -XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
Young Collection
会 STW
Young Collection + CM
在 Young GC 时会进行 GC Root 的初始标记 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent(默认45%)
Mixed Collection
会对 E、S、O 进行全面垃圾回收 最终标记(Remark)会 STW 拷贝存活(Evacuation)会 STW -XX:MaxGCPauseMillis=ms
Full GC
SerialGC 新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc ParallelGC 新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc CMS 新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足 G1 新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足
Young Collection 跨代引用 新生代回收的跨代引用(老年代引用新生代)问题
卡表与 Remembered Set 在引用变更时通过 post-write barrier + dirty card queue concurrent refinement threads 更新 Remembered Set
Remark
pre-write barrier + satb_mark_queue
JDK 8u20 字符串去重
优点:节省大量内存 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
XX:+UseStringDeduplication
将所有新分配的字符串放入一个队列 当新生代回收时,G1并发检查是否有字符串重复 如果它们值一样,让它们引用同一个 char[] 注意,与 String.intern() 不一样 String.intern() 关注的是字符串对象 而字符串去重关注的是 char[] 在 JVM 内部,使用了不同的字符串表
JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸 载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
JDK 8u60 回收巨型对象
一个对象大于 region 的一半时,称之为巨型对象 G1 不会对巨型对象进行拷贝 回收时被优先考虑 G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生 代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
并发标记必须在堆空间占满前完成,否则退化为 FullGC JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent JDK 9 可以动态调整 -XX:InitiatingHeapOccupancyPercent 用来设置初始值 进行数据采样并动态调整 总会添加一个安全的空档空间
6.垃圾回收调优
调优领域
内存 锁竞争 cpu 占用 io
确定目标
【低延迟】还是【高吞吐量】,选择合适的回收器 CMS,G1,ZGC ParallelGC Zing
最快的 GC
答案是不发生 GC 查看 FullGC 前后的内存占用,考虑下面几个问题 数据是不是太多? resultSet = statement.executeQuery("select * from 大表 limit n") 数据表示是否太臃肿? 对象图 对象大小 16 Integer 24 int 4 是否存在内存泄漏? static Map map = 软引用 弱引用 第三方缓存实现
新生代调优
新生代的特点
所有的 new 操作的内存分配非常廉价 TLAB thread-local allocation buffer 死亡对象的回收代价是零 大部分对象用过即死 Minor GC 的时间远远低于 Full GC
越大越好吗? -Xmn Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size. 新生代能容纳所有【并发量 * (请求-响应)】的数据 幸存区大到能保留【当前活跃对象+需要晋升对象】 晋升阈值配置得当,让长时间存活对象尽快晋升 -XX:MaxTenuringThreshold=threshold -XX:+PrintTenuringDistribution
老年代调优
以 CMS 为例
CMS 的老年代内存越大越好 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3 -XX:CMSInitiatingOccupancyFraction=percent
6.类加载阶段
加载
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类 如果这个类还有父类没有加载,先加载父类 加载和链接可能是交替运行的
链接
验证
验证类是否符合 JVM规范,安全性检查
准备
为 static 变量分配空间,设置默认值 static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾 static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
初始化
()v方法
初始化即调用 ()v ,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】 main 方法所在的类,总会被首先初始化 首次访问这个类的静态变量或静态方法时 子类初始化,如果父类还没初始化,会引发 子类访问父类的静态变量,只会触发父类的初始化 Class.forName new 会导致初始化
不会导致类初始化的情况
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化 类对象.class 不会触发初始化 创建该类的数组不会触发初始化
7.类加载器
自定义类加载器
什么时候需要自定义类加载器 1)想加载非 classpath 随意路径中的类文件 2)都是通过接口来使用实现,希望解耦时,常用在框架设计 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器 步骤: 1. 继承 ClassLoader 父类 2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制 3. 读取类文件的字节码 4. 调用父类的 defineClass 方法来加载类 5. 使用者调用该类加载器的 loadClass 方法
8.运行期优化
分层编译
JVM 将执行状态分成了 5 个层次: 0 层,解释执行(Interpreter) 1 层,使用 C1 即时编译器编译执行(不带 profiling) 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) 4 层,使用 C2 即时编译器编译执行 profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
即时编译器(JIT)与解释器的区别
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释 JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需 再编译 解释器是将字节码解释为针对所有平台都通用的机器码 JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:- DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果
方法内联(Inlining)
字段优化
9.内存模型
原子性
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操 作
解决方法
可见性
退不出的循环
解决方法
volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到 主存中获取它的值,线程操作 volatile 变量都是直接操作主存
有序性
诡异的结果
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结 果为1 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过 了) 但我告诉你,结果还有可能是 0
解决方法
volatile 修饰的变量,可以禁用指令重排



