JVM 精华

一、基础

1、泛谈:

  • 相比 C++,不用自己处理 内存分配 和 垃圾回收
  • 执行流程是先把 .java 文件 编译成  .class二进制字节码,然后在 JVM里 解释执行
  • 目前默认都是用 HotSpot 虚拟机

2、基本模块

  • JDK:包含了 JRE + 开发工具包,可以把 .java 编译成 .class
  • JRE:包含 JVM + 运行时类库(lib下如 rt.jar),没有编译功能
  • JVM:加载 .class文件进行解释运行

二、整体结构

1、类加载子系统

jvm是按需加载 class文件,用到时才加载到内存生成 class对象,利用到 双亲委派模式(即类加载器收到类加载请求,会一直委派到最上层父类加载,如果父类能完成则自己就不会去加载了),双亲委派防止了类重复加载、保证核心 API安全(沙箱安全);判断两个类相同(全限类名 & 加载器都一致)。

(1)加载:用全限类名得到 二进制字节流,再把里面的静态存储结构转为 方法区的运行时数据结构(类元数据),接着 堆中生成一个 Class对象用于 方法区中此类的访问入口。

(可通过 getClassLoader() 获取是哪个 加载器加载,而用 getClass() 可获取哪个 类创建了对象)

  • Bootstrap/引导类加载器:java核心类库(java/javax/sun开头的)都是直接用它
  • Extension/扩展类加载器:扩展目录(jre/lib/ext)下的加载类库
  • System/系统类/应用类加载器:用户程序默认用它(环境变量 classpath下的那些)

(2)连接:

a、验证:保证 Class字节流符合虚拟机要求

b、准备:为类变量(static修饰那些) 在 方法区中分配内存并给个默认初始值(注:final的则在编译时就分配了,而实例变量是跟对象一样分在 堆里而不是在这里)

c、解析:把常量池 符号引用 改为 直接引用

(3)初始化:负责父类及本身 static 变量/代码块 的初始化

2、运行时数据区

(1)程序计数器/PC寄存器:线程独有,存储指令地址 供 执行引擎读取(软件层面的,不同于物理寄存器);之所以要有是因为多线程切换要记住执行到哪里,也因此得线程私有 自然也没有 GC也没异常

(2)虚拟机栈:可设 -Xss

a、概念

  • 线程独有,管程序如何运行,负责 存局部变量(基本类型/引用类型地址)、部分结果数据、方法调用和返回;
  • 方法执行就是 栈帧(每个方法,对应着每个内存块) 的入栈和出栈的过程,所以不存在 GC;
  • 如果设置固定大小的栈但线程请求 超出栈最大容量会抛出 StackOverflowError异常,但如果设置为动态的但运行时不够内存去创建则抛出 OutOfMemoryError异常;

b、内部结构

  • 局部变量表:一个数字数组,存储单元是 Slot变量槽,存 方法参数 和 局部变量(基本/引用类型,构造/实例方法第一个 Slot存 this);它是默认不会初始化;
  • 操作数栈:数组,计算过程中 变量值 的临时存储空间;
  • 动态链接:指向 运行时常量池的方法引用(比如方法调方法,被调方法是一个符号,需要动态链接转换成 直接引用)(扩展到 静态/动态链接、早期/晚期绑定、非虚/虚方法、编译期/运行期);
  • 方法返回地址:方法结束有 正常结束 和 异常退出 两种,完成后它都返回自己被调用的位置,正常情况 调用此方法的指令下一条指令就是此方法的返回地址,但异常时返回地址由 异常表 负责

(3)本地方法栈:线程独有,管理 本地方法(native 修饰)的调用

(4)堆:线程共享,管数据如何存储,它在 JVM启动时创建并且此时大小就确定了(扩展:TLAB)

a、设置内存:

  • -Xms200m:初始堆内存,默认是主机 1/64
  • -Xmx2048m:最大堆内存,默认是主机 1/4
  • -Xmn200m:年轻代最大内存
  • -XX:NewRatio=4:表示年轻代与年老代所占比值为1:4
  • -XX:SurvivorRatio=8:则两个Survivor区与一个Eden区的比值为2:8
  • -XX:+UseAdaptiveSizePolicy:自适应内存分配
  • -XX:MaxTenuringThreshold=:新生代晋升老年代次数设置,默认15
  • -XX:MetaspaceSize/-XX:MaxMetaspaceSize:设置 元空间初始值和最大值

b、分代

  • 年轻代:占 1/3堆,Eden、Survivor0/From、Survivor1/To(Eden:From:To=8:1:1);
  • 老年代:占 2/3堆
  • 永久代/元空间

c、TLAB(Thread Local Allocation Buffer):堆在 Eden区给 每个线程分配的 私有缓存区,可以用来避免线程安全问题

d、OOM Java heap spece:经过 Minor GC和Full GC后,堆都还放不下

(5)方法区(1.7永久代/1.8元空间实现):线程共享,也可能导致 OOM;里面的常量或者类的实例都没用到了,就可以被 GC了

  • 运行时常量池:final修饰常量,类元数据如类的信息(全限类名、接口、修饰符等)、方法(名、参数、修饰符等)、属性(类型、名称、修饰符等)信息,字面量,class 文件中符号引用 转为 直接引用,就放到 运行时常量池 里(java8和之后 都放 元空间/本地内存/直接内存/堆外内存
  • 静态变量:static 修饰的(java7和之后 都放堆
  • 字符串常量池:字符串的值(java7和之后 都放堆
  • JIT 编译后的缓存代码:

3、执行引擎

(1)解析器:负责把 程序计数器中 字节码指令解释成 本地机器指令给操作系统才能执行;优点是 直接解释所以 JVM启动快

(2)JIT 编译器:JVM直接将 字节码编译成机器平台相关的 机器语言 而不用一条条去解释,并对 热点代码(经过 热点探测,调用频率较高)进行缓存,优点是后续执行更快

(3)编译执行过程

4、本地方法接口 & 本地方法库

  • native 修饰的方法,可用来比如调用 C语言函数

三、执行过程

1、JVM启动流程

 

2、对象内存分配过程

(1)刚创建的对象一般都先放 Eden区(除非很大放不下就得直接去老年代),当 Eden区满了则触发 Minor GC,把GC 后剩余对象放到 Survivor0区

(2)到了下次触发 Minor/Young GC,上次幸存对象也会参与,GC 剩余对象一起放到 Survivor1区,如此反复

(3)啥时候到了设置的 Promotion 晋升老年代次数,则那部分对象就到 老年代(特殊的是如果Survivor相同年龄对象数超过一半了,就把它们和比它们老的放到老年代)

(4)什么时候 老年代满了就进行 Major/Old GC,如果还放不下,就 OOM了

3、内存对象

(1)对象实例化方式

  • new 
  • 反射,Class 的 newInstance()
  • clone()
  • 序列化和反序列化

(2)对象创建步骤

(3)对象内存存储

  • Mark Word:64位下是 8字节
  • 类型指针:64位下默认开了压缩是 4字节,否则是 8字节
  • Array Length:数组才会有,一般是 4字节
  • Instance Data:看字段类型长度
  • Padding:让整个对象能被 8整除,差多少这里补多少

4、类的生命周期

加载 =》连接 =》初始化 =》使用 =》卸载

四、垃圾回收 GC

1、概念

  • 垃圾:没有任何指针 指向的对象
  • 内存溢出:内存空间不够,不能存放新的对象
  • 内存泄漏:对象没有用,但 GC 回收不了(比如 没有close资源)
  • GC的作用:不回收的话,内存早晚会消耗完,就造成 OOM,同时也为了清楚碎片
  • finalize():垃圾回收之前,调用的方法
  • STW:stop the world,进行垃圾回收时 停止用户线程
  • System.gc():显式触发 Full GC,但不保证能执行
  • 安全点和安全区:能停顿下来做 GC的地方就是 安全点,如果整个代码区域都能则叫它是 安全区
  • 对象三种状态:①可触及的 ②可复活的 ③不可触及的
  • GC 并发和并行:并发是 垃圾回收线程 和 用户线程交替工作(如 CMS、G1);并行是 多个 垃圾回收同时工作,但没法和 用户线程同时(如 ParNew)

2、GC类型

  • Minor GC:Eden区满时执行,STW时间短(Survivor满不会执行)
  • Major GC(基本等同于 Full GC):老年代满时进行执行,STW是 MinorGC的10倍以上
  • Full GC:整个堆和 方法区的垃圾回收

3、引用类型

(1)强引用 StrongReference:使用 new 创建对象并赋值给 变量,该对象是可达的,存在则 GC不会回收

(2)软引用 SoftReference:OOM前即 内存不足就会回收它(比如缓存在内存足则留,不足则清理部分;mybatis 缓存SoftCache有部分用到) 

(3)弱引用 WeakReference:下一次 GC则会回收(ThreadLocal 中就用到)

(4)虚引用 PhantomReference:(管理堆外内存,NIO中用来完成 Netty的 zeroCopy 中有应用)

3、垃圾回收算法

(1)标记阶段

  • 引用计数法:引用计数器 记录指向该对象的引用数量,如果为 0 则认为垃圾(优点:简单、效率高;缺点:增加存储空间、循环引用导致内存泄漏),java没用到
  • 可达性分析/追踪性算法:被根对象集合 GC Roots 连接的目标对象 是否可达/引用链(GC Roots:虚拟机栈引用、本地方法栈引用、static引用、常量引用、锁持有对象、JVM基本类如Class)

(2)清除阶段

  • 标记清除 Mark-Sweep算法:从 GC Roots 开始遍历然后标记 可达对象,接着遍历 堆内存把没有标记为 可达对象的给回收(优点:简单;缺点:内存碎片)。
  • 复制 Copying算法:把空闲内存空间分两块,每次用一块,GC时把 存活对象 复制到另一块,然后清除 目前那块留下来的 非存活对象,如此反复使用两块内存空间(优点:快,无内存碎片;缺点:两块空间、复制耗时);
  • 标记整理 Mark-Compact算法:从 GC Roots 开始遍历然后标记 可达对象,接着把 存活对象压缩到内存一端然后清理剩下的对象(优点:比 Copying内存减半;缺点:效率较低);适合 老年代这种 存活对象多的情况。

(3)适用性

  • 分代收集:Copying 算法适合新生代 存活对象少、频繁回收、讲究效率;Mark-Compact算法适合老年代 存活对象多、回收不频繁
  • 增量收集:GC 时要 STW,应用线程因此暂停,增量收集是采用 GC和应用线程交替进行,分阶段 GC
  • 分区算法:把堆再细分为 小空间region,这样每次 GC可根据 region为单位进行回收,控制个数从而减少 STW的时间

4、垃圾回收方式

  • 串行 & 并行:前者是单 CPU,后者是多 CPU
  • 独占 & 并发:前者 GC就暂停 用户线程,后者 GC和 用户线程交替

5、垃圾回收指标

  • 吞吐量:程序时间 占(程序 + GC)的时间比例
  • 暂停时间:GC 时,用户线程暂停的时间
  • 内存占用:堆占用内存
  • 收集频率:GC 发生的频率

6、垃圾回收器:

(1)串行回收器:

  • Serial:Client模式下默认 Young GC;Copying 算法、串行回收、STW;简单高效
  • Serial Old:标记整理算法、串行回收、STW

(2)并行回收器

  • ParNew:Young GC,Copying 算法、并行回收、STW;
  • Parallel Scavenge:java8默认 Young GC,Copying 算法、并行回收、STW;吞吐量优先如用于批量处理
  • Parallel Old:标记整理算法、并行回收、STW;

(3)并发回收器

  • CMS Concurrent-mark-Sweep:java8默认 Old GC,标记清除算法、并发回收、STW;低延迟如用于用户交互;(经过 初始标记、并发标记、重新标记、并发清理)
  • G1:java9 默认,并行+并发回收,兼顾 新老两代;把堆分 region 而 region内是复制算法 但整体看是 标记整理算法;区域分代;(经过 年轻代GC、老年代并发标记、混合回收)
  • ZGC:新的

7、参数设置

  • -XX:+PrintCommandLineFlags:查看 使用的回收器
  • -XX:+UseSerialGC:使用 Serial & Serial Old 回收器
  • -XX:+UseParNewGC:使用 ParNew 回收器
  • -XX:+UseParallelGC:使用 Parallel 回收器
  • -XX:+UseParallelOldGC:使用 Parallel Old 回收器
  • -XX:MaxGCPauseMillis:垃圾回收停顿时间 STW
  • -XX:GCTimeRatio:垃圾回收占总时间的比例(影响 吞吐量)
  • -XX:+UseAdaptiveSizePolicy:设置 Parallel 回收器 自适应策略
  • -XX:+UseConcMarkSweepGC:使用 CMS 回收器
  • -XX:ParallelCMSThreads:设置 CMS 线程数
  • -XX:+UseG1GC:设置 G1 线程数

https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

8、对比

五、调优

逃逸分析:栈上分配、同步省略

 

六、扩展

1、String

  • 字符串常量池中是不能有两个相同内容的字符串
  • intern() 压入 字符串常量池(若已有则返回地址,若没有则建一个再返回地址),可用于促进 堆垃圾回收(因为返回地址是 常量池中地址,new的可以被销毁)
  • 如果拼接没变量,则结果是在 常量池中,利用 编译器优化;但如果有变量(final的则不一样)则在堆中 new StringBuilder(),所以对象不同

2、分代原因:如果不分代,都放在一起,每次回收都要检测所有对象,效率低

3、堆不是分配对象的唯一选择:JIT编译器 如果经过逃逸分析(方法外不可能被调用则说没有发生逃逸),发现一个对象没有逃逸出方法,则被优化在 栈上分配(不过 HotSpot似乎没这么实现

4、栈、堆、方法区直接的联系:栈中 局部变量表 存了 对象引用,堆中 存了实例化的对象,方法区 存了 类元数据(类本身信息,如该对象是哪个类创建的)

5、方法区演进:迁移字符串常量池 到堆的好处时 触发 GC回收 String的可能性更高,因为 永久代里 String 回收的可能性太低了,但我们代码里经常有很多 String

  • 字符串在永久代,性能差且容易 OOM
  • 永久代大小难确定
  • 永久代 GC效率低

6、触发 Full GC

  • 调用 System.gc(),可能执行,但不必然
  • 老年代或 方法区空间不足

7、版本变化

8、CMS 为什么不用 标记压缩 而是 标记清除?为什么它后续被弃用?

因为 清理 和 用户线程 并发执行,所以 不能去移动对象,但是 标记压缩 是需要移动对象的。它的缺点是 产生内存碎片,可能出现“Concurrent mode failure”

9、G1 工作过程?

10、为什么元空间用本地内存/本地内存?

  • 原来需要 IO -- 直接内存 -- 非直接内存 -- 直接内存 -- IO,元空间用 IO -- 直接内存 -- IO
  • 不会像永久代那样容易 OOM

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值