文章目录
JVM是什么?
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚 拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
好处:
- 一次编译,到处运行
- 自动内存管理,垃圾回收。
- 数组下标越界检测
- 多态的实现
简单来说就是Java的二进制运行环境,Java编译后的字节码是依赖JVM不是依赖系统执行的,所以不管在哪个系统只要有JVM的存在都可以无差别的执行。
内存结构
内存结构图
JVM JRE JDK的区别
程序计数器(寄存器)
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。它是一块很小的内存空间,也是运行速度最快的存储区域
作用:
- 程序计数器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- 负责转换二进制字节码。
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 是线程私有的,每根线程都有它自己的程序计数器,程序计数器会存储当前线程正在执行的java方法的JVM指令地址,生命周期和线程周期一致。
- 不会出现内存溢出。(Java虚拟机规范的)
JVM指令—>解释器—>机器码—>CPU
字节码解释器工作时就是通过改变这个计数器的值来选取下一个条需要执行的字节码指令。
为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始执行。
PC寄存器为什么会被设定为线程私有
多线程在特定的时间段只会执行一个线程的方法,CPU会不停的做任务切换,会导致线程终断,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程分配一个pc寄存器让它们独立运算互不干扰。
虚拟机栈
每根线程运行需要的内存被称为虚拟机栈,虚拟机是有栈帧组成的,而栈帧的单位是方法的调用。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
特点:
- 在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。
- 局部变量是每根线程私有的,不存在线程安全问题,除非创建对象脱离方法作用就会存在线程安全。比如参数,返回值操作。
- 物理内存是固定的,如果每根线程内存大了那线程数量就会受到限制,大了无非就是递归能更多。
- -Xss 去调整JVM栈的大小
- 虚拟机栈分配的内存
编译期
确认
栈内存溢出问题:
尽量减少方法递归调用,栈帧过大也会导致(一般不会)
局部变量表
- 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)
注意:
局部变量存储在局部变量表中,随着线程而生,线程而灭。并且线程间数据不共享。但是,如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中。因为在堆中,是线程共享数据的,并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表!
操作数栈(Operand Stack)也称作操作栈
是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
动态链接
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
方法返回
无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
线程运行问题
线程过多会导致cpu占用空间高
迟迟没有结果一般是出现死锁现象
本地方法栈
功能跟虚拟机栈类似。本地方法栈是为虚拟机使用到的Native方法服务。
堆内存
通过new创建的对象都是使用堆内存。
特点:
- 线程共享
- 存在线程安全问题
- 堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。
- 分配的内存是在
运行期
确认的
如果对象一直在使用又没有回收 就会存在内存溢出的问题。
使用Java VisualVm可以监视当前线程的内存情况
方法区
方法区是线程共享的,虚拟机启动时创建,是存放与类相关的,除此之外,还用来存储常量、静态变量,以及一些代码缓存等数据。这些数据都有个特点:独立,几乎不变,几乎不依赖于对象。
方法区只是逻辑上的概念,具体实现方法叫做元空间和永久代
永久代是 JDK 8 之前的实现。
元空间是 JDK 8 及其之后的实现。
1.7之后常量池(StringTable)
转移到堆原因是因为永久代回收效率低,永久代需要等Full GC才能回收。Full GC要等整个老年代空间不足才会触发。这样就导致内存容易溢出。1.8之后永久代被移除新增元空间,元空间使用的是本地内存。
运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的
。
常量池
常量池就是一张表,虚拟机指令根据常量表找到要执行的类名,方法名,参数名。
类加载后,常量池中的数据会在**运行时常量池(串池)**中存放!底层是HashTable
的结构是线程安全的,常量池也会垃圾回收。
String a1 = "a";
String a2 = "b";
String a3 = a1 + a2 ;
String a4 = "a" + "b";
System.out.println(a3 == a4); //false
为什么结果是false?因为a3是变量是组成的,居然是变量将来就有可能发生改变,所以它使用了StringBuild对象进行拼接存在堆内存(1.8)。而a4在编译期间就可以确定是"ab"不会发生改变。
String b = new String("12");
String intern = b.intern();
System.out.println(a == intern); //true
使用intern可以使字符串对象加入串池。没有就放,有就返回串池对象。
1.6版本有的话是拷贝一份回来。
性能调优
-
设置 StringTableSize
jvm的默认桶的大小:
8.000 ``` 添加参数增加桶的个数(最小值可以设置为1009): ```sql -XX StringTableSize=200000 ``` 减少桶的冲突,可以提高jvm的效率
-
当信息重复较多,使用字符串常量效率高可用去重
。 -
当信息需要大量拼接使用StringBuild,因为StringBuild是可变的性能高
。
400万相对多的重复数据测试过,不用常量占300mb内存,用常量占40mb内存
直接内存
- 属于操作系统,常见于
NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
文件读写流程
使用了DirectBuffer
直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
垃圾回收
如何判断对象可以回收?
- 引用计数法:一个对象被其它对象引用那么就给这个对象计数+1,不再引用-1,变为0回收!(Java不用)弊端很明显当对象之间相互引用都回收不了了
- 可达性分析算法: JVM中的垃圾回收器通过可达性分析来探索所有存活的对象,扫描堆中的对象,看那个对象有无被
GcRoots
对象直接或者间接使用,有就回收。
可以作为GcRoots
对象:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。 (一般指被static修饰的对象,加载类的时候就加载到内存中。)
- 方法区中的常量引用的对象。
- 本地方法栈中的
native
方法引用的对象 - 线程活动中,加锁的对象。
可以看成葡萄,丢掉要摘下来对吧
?
五种引用
-
强引用:被
GcRoots
引用的对象。只要存在就不回收
。 -
软引用:间接调用的对象。
将要OOM之前,存在就会被回收软引用所引用的对象
。本身引用所占空间不会被回收,会进入引用队列,通过引用队列来回收。
public class Demo1 { // 要设置堆内存才能看出效果
public static void main(String[] args) {
final int _4M = 4*1024*1024;
//使用引用队列,用于移除引用为空的软引用对象
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
//遍历引用队列,如果有元素,则移除
Reference<? extends byte[]> poll = queue.poll();
while(poll != null) {
//引用队列不为空,则从集合中移除该元素
list.remove(poll);
//移动到引用队列中的下一个元素
poll = queue.poll();
}
}
}
-
弱引用:跟软引用类似。
垃圾回收启动就回收被引用的对象
,也要通过引用队列回收。 -
虚引用: 影响不了对象,也不能创建对象。它必须配合引用队列使用,被回收时会放入队列起到回收时的通知作用。
-
终结器引用:
总结:软弱引用适用缓存场景,因为缓存讲究即时性没有必要占内存太久。其他引用不咋用。
垃圾回收算法
标记清除:
定义:虚拟机执行垃圾回收过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间。腾出空间不是把内存清0,而是将内存起始位记录下来,下次有新的对象进来直接替换掉就是了
。
缺点:不会进行整理,容易产生内存碎片,这样会导致大点的对象装不下去,比如说数组这种连续结构的对象就装不下去了。
标记整理
在标记清除的基础上添加了整理,但是整理过程中涉及引用的对象内存移动影响执行速度。
复制
将内存分为From和To(To定义为空的),把被GcRoot
引用的对象复制到To中,From清除。缺点是占用内存双倍。
以上三种都使用 JVM根据不同情况实现
分代回收
堆中的内存分为新生代和老年代
新创建的对象都被放入新生代中的伊甸园(Eden)中
当伊甸园满了会触发MinorGc(小Gc)
MinorGc(小Gc)
会将伊甸园和幸存区FROM存活的对象先复制到幸存区 TO中并让其寿命+1,然后交换到幸存区From中。
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC
(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作,因为涉及对象移动,不停止会爆炸) 这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1
如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中
只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC
ps:MinorGc
和FullGC
的区别:FullGC
是清理所有堆空间,MinorGc
清理年轻代空间。
大对象直接进入老年代(-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代)个人认为大对象直接进入老年代是为了让新生代空间保持充足的一种方法,因为新生代被频繁使用,如果大对象直接进来占用大部分对象很容易就又调用MinorGc
线程内存溢出 :某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常
垃圾回收器
相关概念
垃圾收集器是算法的落地实现
并行收集: 指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
。
并发收集: 指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
吞吐量: 即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
常用收集器
串行(Serial 收集器)
- 单线程
- 内存较小,个人电脑(CPU核数较少)
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
吞吐量优先 (Parallel)
- 多线程
- 堆内存较大,推荐多核CPU
- 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短,并行回收。
- JDK1.8默认使用的垃圾回收器
特点:属于新生代收集器也是采用复制算法
的收集器(用到了新生代的幸存区),又是并行的多线程收集器。适合高吞吐量(凡是高吞吐都是适合在后台运算不需要太多交互的任务
)
GC自适应调节策略: Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略
。
Parallel Scavenge收集器使用两个参数控制吞吐量:
- XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
- XX:GCRatio 直接设置吞吐量的大小
Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本
特点:多线程,fullgc:采用标记-整理算法(老年代没有幸存区)
简单来说Parallel和Parallel Old都是采用并行高吞吐量的(属于后台计算的交互任务少)方式来处理垃圾
响应时间优先 (CMS收集器)
-XX:+UseConcMarkSweeoGC
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单次STW时间变短(尽量不影响其他线程运行)
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务
执行流程
初始标记: 标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题
并发清除:对标记的对象进行清除回收CMS收集器的内存回收过程是与用户线程一起并发执行的
缺点:
- 会产生大量空间碎片,采用的是标记清除算
- 回收时间长由于我们在执行CMS垃圾回收器的过程中有一部分资源让给了用户线程,那就会导致回收时间长,内存空间没有被及时释放掉也就会导致吞吐量不如Paralle
G1收集器
JDK9默认使用的收集器,而且替代了CMS 收集器
适用场景
- 同时注重吞吐量和低延迟(响应时间)
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
- 整体上是标记-整理算法,两个区域之间是复制算法
回收阶段
新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)
Young Collection
分区算法region
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间
E:伊甸园 S:幸存区 O:老年代
会STW
Young Collection + CM
CM:并发标记
在 Young GC
时会对 GC Root
进行初始标记
在老年代占用堆内存的比例达到阈值时,会进行并发标记(不会STW),阈值可以根据用户来进行设定
Mixed Collection
会对E S O 进行全面的回收
最终标记
拷贝存活
-XX:MaxGCPauseMills:xxx 用于指定最长的停顿时间
为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
简单来说就是采用分区和并发再加单独回收控制STW
Full GC
G1在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
Gc调优
"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
最快的Gc是不发生Gc
。
- 即时性数据使用软弱引用或者使用第三方缓存中间件如Redis,减少堆内存的使用。
- 如果有大量存活对象设置阈值让它尽可能上升为老年对象,因为一直占着幸存区会导致复制算法影响性能。
总结:
一般项目不需要调优,加个xms和xmx参数就够了。因为JVM本身就是为这种低延时高并发大吞吐的服务设计和优化的,在没有数据分析情况下乱调反而出反效果。其实多注重代码层面的优化才是最好的优化了!!
类加载
classloader顾名思义,即是类加载。虚拟机把描述类的数据从class字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
加载:
通过全类名查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象作为方法区的访问入口
验证:
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
准备:
为 static 变量分配空间,设置默认初始值
,而常量
是编译后赋值。不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析:
将常量池内的符号引用替换为直接引用的过程。
举个例子来说,现在调用方法hello0,这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的
内存地址或偏移量,也就是直接引用。
初始化阶段:
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的『构造方法』的线程安全
(多线程保证类只初始化一次),为类的静态变量赋予正确的初始值。
- clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
类的初始化的懒惰的
以下情况会初始化
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
以下情况不会初始化
- 访问类的 static final 静态常量(基本类型和字符串)
- 类对象.class 不会触发初始化
- 创建该类对象的数组
- 类加载器的.loadClass方法
- Class.forNamed的参数2为false时
- 验证类是否被初始化,可以看该类的静态代码块是否被执行
卸载
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类加载器
作用:通过一个类的全限定名来获取描述该类的二进制字节流
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
Bootstrap ClassLoader(启动类加载器): JAVA_HOME/jre/lib
无法直接访问,显示null
Extension ClassLoader(拓展类加载器): JAVA_HOME/jre/lib/ext
上级为Bootstrap
Application ClassLoader(应用程序类加载器): classpath
上级为Extension
自定义类加载器 自定义
上级为Application
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委托的优势:避免重复加载 + 避免核心类篡改。假设网络传入java.lang.Integer
类,起动类加载器在核心Java API发现这个名字的类,发现该类已被加载就不会重新加载
,而是直接返回原先有的Integer类,这样就很有效避免重复,避免被修改。
自定义类加载器
使用场景
- 想加载非 classpath 随意路径中的类文件
- 通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
- 实现依赖隔离,解决包冲突
步骤
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写 findClass 方法
- 重写loadClass方法,可以不走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
JVM运行期优化
JVM 将执行状态分成了 5 个层次:
- 0层:解释执行,用解释器将字节码翻译为机器码
- 1层:使用 C1 即时编译器编译执行(不带 profiling)
- 2层:使用 C1 即时编译器编译执行(带基本的profiling)
- 3层:使用 C1 即时编译器编译执行(带完全的profiling)
- 4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
即时编译器(JIT)与解释器的区别
解释器
- 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- 是将字节码解释为针对所有平台都通用的机器码
即时编译器
- 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 根据平台类型,生成平台特定的机器码
当字节码被反复使用到达一定的阈值会采用即使编译器。以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2
,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码。
JVM加载class文件的原理
- 隐式加载: 只有new创建的对象就会加载。
- 显示加载: 通过class.forname()等方法,显示加载需要的类(自定义要加载的类),用到了才会加载。
泄漏与溢出的区别
- 溢出: 比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
- 内存泄漏:无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。