JVM
JVM
1、JVM组成
1)类加载
- 根据给定的全限定名类名来装载class文件到Runtime data area中的method area。
2)执行引擎
- 执行classes中的指令。
3)本地接口
- 与native libraries交互,是其它编程语言交互的接口。
4)运行时数据区
- JVM的内存。

每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
2、java 对象
1)对象创建
创建方式:
- 调用了构造函数
- 使用new关键字
- 使用Class的newInstance方法
- 使用Constructor类的newInstance方法
- 没有调用构造函数
- 使用clone方法
- 使用反序列化
创建流程:
- 类加载
- 分配内存
- 内存规整——指针碰撞
- 内存不规整——空闲列表
- 并发处理
- 同步处理——使用CAS+失败重试来保障更新操作的原子性
- 本地线程分配缓冲—— 每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。
- 初始化
- 将分配到的内存空间都初始化为零值
2)对象访问定位
- 句柄访问
- 堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息
- 直接指针
- 引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息
3、垃圾收集器
1)GC判断是否可以回收
- 引用计数器算法
- 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加一,当引用失效时,计数器的值就减一,当该对象的计数器的值为0时,标志该对象失效。
- 根搜索算法
- 通过一系列的名为"GCRoots"的对象作为起始点,从这些结点开始向下搜索,搜索过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连时,则证明对象是不可用的。
2)四种引用类型
- 强引用—— 发生 gc 的时候不会被回收。
- 软引用—— 有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用—— 有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用—— 无法通过虚引用获得对象,用途是在 gc 时返回一个通知。( 主要作用是跟踪对象被垃圾回收的状态。 )
3)垃圾收集算法
-
标记-清除
- 优点:实现简单,不需要移动对象
- 缺点:效率低,会产生大量内存碎片
-
复制
- 优点:按顺序分配内存,运行高效,不用考虑内存碎片
- 缺点:可用的内存大小缩小为一半,对象存活率高时会频繁进行复制
-
标记-整理
- 优点:无内存碎片
- 缺点:需要进行局部对象移动,降低了运行效率
分代收集:
-
新生代 —— 占堆内存的1/3 , Eden : survivor From : survivor To 8:1:1
-
老年代 —— 占堆内存的2/3
-
过程
-
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
-
清空 Eden 和 From Survivor 分区;
-
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
-
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代
-
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。
-

- 永久代
- 在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用
本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native
memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由
MaxPermSize 控制, 而由系统的实际可用空间来控制。
- 在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间
4)GC
a. Full GC
- 老年代空间不足
- 持久代空间不足
- YGC出现promotion failure
- 统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
- 显示调用System.gc
b. Minor GC
-
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间
-
MinorGC 采用复制算法。
c. Major GC
- Major GC 的速度往往会比 Minor GC 慢 10 倍
- 出现了 Major GC 通常会伴随至少一次 Minor GC
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没
有标记的对象。
5)JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
Serial 垃圾收集器(单线程、复制算法)
Serial 是最基本垃圾收集器,使用复制算法。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
Serial 虽然需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,
没有线程交互的开销,可以获得最高的单线程垃圾收集效率。
特点:CPU利用率最高,停顿时间即用户等待时间比较长。
适用场景:小型应用通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃
圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也
要暂停所有其他的工作线程。
参数控制:-XX:+UseParNewGC ParNew收集器 -XX:ParallelGCThreads 限制线程数量
Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收
集器,它重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用 CPU 时间,尽快
地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是
ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
采用多线程来通过扫描并压缩堆 特点:停顿时间短,回收效率高,对吞吐量要求高。
适用场景:大型应用,科学计算,大规模数据采集等。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,
Parallel Old收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6
才开始提供。
Concurrent mark sweep(CMS)收集器器(多线程标记清除算法)
其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
优点:并发收集、低停顿 缺点:产生大量空间碎片、并发阶段会降低吞吐量
特点:响应时间优先,减少垃圾收集停顿时间
适应场景:大型服务器等。 通过JVM参数 -XX:+UseConcMarkSweepGC设置
G1收集器
基于标记-整理算法,不产生内存碎片。
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
特点:支持很大的堆,高吞吐量
通过JVM参数 -XX:+UseG1GC 使用G1垃圾回收器
4、内存分配策略
1、对象优先分配在Eden区 , 如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2、大对象直接进入老年区
- 避免在Eden区和两个Survivor区之间发生大量的内存拷贝
3、长期存活的对象进入老年代
- 每复制一次年龄加一,达到阈值(15)的对象进入老年区
4、动态判断对象的年龄
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5、空间分配担保
- 每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
- JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
5、类加载机制
1)什么是类的加载
类加载指的是将类 Class 文件读入内存,并为之创建一个 java.lang.Class 对象, class 文件被载入到了内存之后,才能被其它 class 所引用
jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要去动态加载
java 类加载器是 jre 的一部分,负责动态加载 java 类到 java 虚拟机的内存
类的唯一性由类加载器和类共同决定(双亲委派机制)
2)加载过程
加载 —> 验证 —> 准备 —> 解析 —> 初始化
加载:
通过一个类的全限定名加载该类对应的二进制字节流,将字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法去各个类访问该类的入口。
验证:
检查导入类或接口的二进制数据的正确性,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:
准备阶段主要是将类变量(被static修饰符修饰)在方法区进行内存分配并进行初始化。 (public static int value = 3; 值初始化为0 ; public static final int value = 3; 值初始化为3 )
解析:
将符号引用转成直接引用
初始化:
激活类的静态变量的初始化Java代码和静态Java代码块。
- 注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
3)类加载器
- 启动类加载器 :Bootstrap ClassLoader —— 负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
- 扩展类加载器 :extensions class loader —— 负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器 :Application ClassLoader —— 负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
4)双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
为什么需要?
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
如何打破?
- 要继承ClassLoader类,还要重写loadClass和findClass方法
6、调优
常用的 JVM 调优的参数都有哪些?
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
参考链接:https://blog.csdn.net/qq_43619628/article/details/118574113

浙公网安备 33010602011771号