JVM
是什么?
JVM是一种规范.
JVM
用来干什么?
Java虚拟机将字节码文件(.class)编译成操作系统可以识别的机器码.
Java
程序的执行过程
java
程序首先经过javac
编译成.class
文件,然后jvm
将其翻译成操作系统可以识别的机器码.
JVM、JRE、JDK
之间的关系
JVM
只是一个翻译,将字节码文件翻译成机器识别的代码
JRE
除了包含JVM
外,提供了很多类库(jar
包)
JDK
除了包含JRE
外,还提供了一些非常好用的小工具,如:javac
(编译代码)、javap
(反编译代码)、jar
(打包代码)、java
等
运行时数据区(除此之外还有:直接内存
)
方法区
(线程共享区)堆
(线程共享区)- 虚拟机栈 (线程私有区)
- 本地方法栈 (线程私有区)
- 程序计数器 (线程私有区)
每个私有线程
包含一个虚拟机栈
,每个虚拟机栈
中可以包含多个栈帧
,我们代码中执行的每个方法
会被封装成一个个的栈帧
栈帧的组成部分
-
局部变量表
-
操作数栈
-
动态连接
-
返回地址
程序计数器的作用
(只会记录虚拟机栈,不会记录本地方法栈)
程序计数器用于记录栈帧中字节码代码执行的偏移量,有些字节码代码可能会占多行,所以计数器在记录的时候可能中间存在缺失某个行号的情况,如12345679,中间没有执行第8行代码,因为第7行代码占2个偏移量,所以后面接9
操作数栈的作用
操作数栈用于在栈帧中定义变量和运算时,将定义的变量放到操作数栈,然后在将这个变量从操作数栈取出,存放到局部变量表中.局部变量表的作用
局部变量表用于将操作数栈的数据存入到局部变量表中,局部变量表是以索引和变量的值的形式来做存储的.返回地址的作用
一个方法在执行完成后,会将局部变量表中的索引给返回出来,通过这个索引就可以拿到该位置存储的变量的值.
方法区
存字节码文件(如:Tearcher.class,类加载的时候放方法区)、静态变量、常量
JVM内存处理全流程
- JVM申请内存
- 初始化运行时数据区
- 类加载
- 执行方法
- 创建对象
堆
堆空间的分代划分
Java对象的分配与垃圾回收机制
JVM
中对象的创建过程
- 检查加载
- 分配内存 (划分内存方式、并发安全问题)
- (内存空间)初始化 (‘零’值)
- 设置 (对象头)
- 对象初始化 (构造方法)
划分内存的方式
- 指针碰撞
- 空闲列表
解决并发安全
- CAS加失败重试
- 本地线程加缓冲
对象
- 对象头
- 存储对象自身的运行时数据(
Mark Word
)- 哈希码
- GC分代年龄
- 锁状态标识
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针
- 若对象为数组,还应该有记录数组长度的数据
- 存储对象自身的运行时数据(
- 实例数据
- 对其填充(非必须)
对象的访问定位 到对象类型数据的指针、对象的实例数据
- 使用句柄 (
搞不明白
) 堆内存包括:句柄池
和实例池 - 直接指针 (
搞不明白
) 堆内存
判断对象的存活
- 引用计数法 (缺陷:对象相互引用)
- Class回收条件 (
回收条件比较苛刻,需要满足以下所有条件
)- class new出的所有对象都要被回收掉(反射创建的对象)
- 对应的类加载器也要被回收掉
- 类,java.lang.class对象
- 任何地方都没有被引用,并且无法通过反射调用这个类的方法
- 参数控制(-Xnoclassgc参数需要关闭)
- 可达性分析(根可达GC roots)
- 静态变量
- 线程栈变量(局部变量)
- 常量池
- JNI指针
内部引用:Class对象、异常对象Exception、类加载器
同步锁,synchronized对象
内部对象,JMXBean
临时对象:跨代引用
- finalize
堆内存划分(新时代占1/3,老年代占2/3)
- 新时代
Eden
、From(S0)
和To(S1 )
内存占用比例:8:1:1 - 老年代
Tenured
对象的分配策略
- 对象的分配原则
- 对象优先分配到Eden区
- 空间分配担保
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定
- 虚拟机的优化技术
- 逃逸分析 + 触发JIT(热点数据)
- 本地线程分配缓冲
垃圾回收算法
- 复制回收算法 (
Appel
式复制回收算法(利用Eden、S0、S1):提高空间利用率和空间分配担保)实现简单、运行高效
没有内存碎片(将GC Roots对象复制到另外一半空间,然后清除之前的那一半空间
)
空间利用率只有一半 - 标记-清除算法
位置不连续、产生碎片(
清除垃圾对象后剩余空间不连续
)
可以做到不暂停(在清理垃圾对象时,GC Roots对象仍然可以正常操作
) - 标记-整理算法
没有内存碎片(
标记-整理-清除,将GC Roots对象整理成连续的对象,然后再清除垃圾对象,剩余的空间就是连续的
)
指针需要移动
CMS(Concurrent Mark Sweep)
-并发垃圾回收器
标记清除算法,用于减少Stop The World(SWT)
响应时间
专门用来处理堆内存中老年代垃圾
清理垃圾步骤
- 初始标记
- 并发标记 (可能包含了预清理、并发可中断预清理、CMS日志查看)
- 重新标记
- 并发清除
CMS中的问题
- CPU敏感 (
需要4核CPU及以上的处理器
) - 浮动垃圾 (
在并发清理阶段可能产生新的垃圾,而这些新的垃圾只能等到下一次垃圾清理的时候再处理,所以CMS需要预留一片内存空间,用于存放并发清理时产生的垃圾.
) - 内存碎片 (
标记清除算法导致的
)
JVM硬核调优技巧
1.JVM内存的分代划分?
新生代、老年代和持久代(永久代/元空间占1~1.5
倍活跃数据大小)
依据活跃数据来分配新生代、老年代的大小,堆内存大小为活跃数据的4倍,新生代为活跃数据的1-1.5倍,老年代为活跃数据的2-3倍大小
如:
活跃数据占300M
总堆: 300M * (3~4倍) = 1.2G
新生代: 300M * (1~1.5倍) = 450M
老年代: 300M * (2~3倍) = 750M
2.扩充新生代或 Eden区能提高GC效
率吗?
一般情况下是可以提高GC效率的,因为新生代扩容后,内存达到一原来2倍的容量时才会进行垃圾回收,所以GC回收的时间间隔会变长;而大部分对象都是新生代的,由于新生代扩容,可能这些对象在增长的这段GC回收时间间隔内,由GC Roots对象变成了可回收的垃圾对象,所以能更高的对垃圾对象进行回收.
还有因为容量小时候,短时间内对对象进行GC回收,会扫描Eden区中的GC Roots对象,然后将他们复制到S0或S0,复制相较与扫描,会花费更多的时间;扩容新生代后明显能减少复制的时间.
3.JVM如何避免Minor GC
会扫描全堆的?
利用卡表
(card table
)存储老年代中是否有新生代的引用,解决新生代与老年代跨代
扫描问题,就只需要扫描新生代和卡表,而不需要再扫描整个堆内存(新生代+老年代)
常量池?
- Class常量池 (
通过javap反编译.class文件可以看到Constant pool中的数据
) - 运行时常量池 (
符号引用->直接引用
) - 字符串常量池 (
String
)
String
- 为什么说String是不可改变的?
这个类final类型的
public final class String
同时里面的数组value也是final类型的
private final char[] value;
- String的创建方式
String str = “abc”;
代码在编译加载时,会在常量池中创建常量“abc”,运行时,返回常量池中字符串的引用.String str = new String("abc");
代码编译加载时,会在常量池中创建常量“abc”;
在调用new时,会在堆中创建String对象,并引用常量池中的字符串对象char[]数组(String源码中本就是char[] value
),并返回String对象的引用