初识JAM
JVM就是JAVA虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行JAVA字节码文件.
下面是java代码执行过程
JVM的功能
1.解释和运行
- 对字节码文件中的指令实时的解释成机器码
2.内存管理
- 自动为对象,方法等分配内存
- 自动的垃圾回收机制,回收不再使用的对象
3.即时编译
- 对热点代码进行优化,提升执行效率
即时编译解释:
Java需要将字节码文件的字节码指令用JVM实时编译成机器码交给计算机运行,实现了跨平台特性,在不做优化性能不如c语言
虚拟机如果发现字节码文件中有多次调用的热点代码,会主动将其优化成机器码,并保存在内存中,当下次执行可以直接调用。
常见的JVM选择:
JVM的组成
1.类加载器
首先要把字节码文件加载到内存中
2.运行时数据区域(JAM管理的内存)
虚拟机要准备一块内存空间去管理字节码文件中的类,对象,接口
3.执行引擎(即时编译器,解释器,垃圾回收器等)
将字节码文件的指令解释成机器码,同时使用编译器优化性能
4.本地接口
虚拟机使用的是c/c++来编写,虚拟机在运行是要调用底层虚拟机实现的c编写的不存在于字节码文件中的方法
字节码文件
字节码文件的组成
1.基础信息
魔数,字节码文件对应的java版本号,访问标识(public,final等等),父类和接口
魔数:
文件是通过文件头校验文件类型,魔数是class字节码文件的校验类型文件头
版本号:
2.常量池
保存了字符串常量,类或接口名,字段名 等,主要在字节码指令中使用
作用:避免重复定义内容,节省空间。
- 常量池的数据有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
- 字节码指令中通过编号引用到常量池的过程称为符号引用
举个例子:先定义三个字符串
我们来查看以下字节码文件:
我们可以看到内容是一个索引跟进后发现还是一个索引
再次跟进找到了常量abc的值
这是将字符串常量先进行了一个分类。能够将其他常量进行区分,例如下面变量名也是abc的情况
3.字段
类和接口声明的字段信息
4.方法
当前类或接口声明的方法信息,字节码指令
5.其他
类的属性,比如源码的文件名,内部类的列表等
类的生命周期
1.加载阶段
- 1.加载阶段第一步是类加载器通过全限定名通过不同的渠道以二进制流的方式获取字节码信息。
- 2.类加载器加载完类之后,java虚拟机会将字节码中的信息保存到方法区中,生成一个InstanceKlass对象,保存类的所有信息,里面还包含特定功能比如多态的信息
- 3.java虚拟机还会在堆中生成一份与方法区中类似的java.lang.class对象,作用是反射时候获取信息和存储静态字段的数据。
对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。
这样Java虚拟机就能很好地控制开发者访问数据的范围。例如堆区中的对象不包含虚方法表
2.连接阶段
- 连接的第一个环节是验证,验证的主要目的是检测java字节码文件是否遵守了《java虚拟机规范》中的约束。这如果不规范就会类加载失败,并且抛出异常。验证选项有:文件格式验证,字节码验证,符号引用验证。
- 给静态变量分配内存,并且设置类变量的初始值。
- 解析阶段主要是将常量池中的符号引用替换为直接引用。符号引用就是在字节码文件中使用编号来访问常量池中的内容。直接引用是使用内存中的地址进行访问具体的数据
3.初始化
- 初始化阶段会执行静态代码块中的代码,并为静态变量赋值。
- 初始化阶段会执行字节码文件中clinit部分的字节码指令。
以下几种方式会导致类的初始化:
1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
2.调用Class.forName(String className)。
3.new一个该类的对象时。
4.执行Main方法的当前类。
clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的。
1.无静态代码块且无静态变量赋值语边句。
2.有静态变量的声明,但是没有赋值语句。
3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
- 类加载阶段会进行 静态代码块 的执行,想要创建实例,势必要先进行类加载。
- 静态代码块只是在类加载阶段执行一次。
- 构造方法和构造代码块,每次实例化都会执行,构造代码块在构造方法前面。
类加载器
类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
类加载器的分类:
类加载器分为两类,一类是在java代码中实现的,一类是java虚拟机底层源码实现的
虚拟机底层源码实现的有一个类加载器叫启动类加载器(用来加载java中最核心的类)
java代码中实现的有扩展类加载器 和**应用类加载器,**它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。
启动类加载器
- 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。
- 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar, resources.jar#.
如图我们使用getClassLoader方法获取类加载器,可以看到输出结果为null,因为启动类加载器由虚拟机底层原理实现,出于安全考虑不能获取到。
让启动类加载器去加载用户jar包:
1.放入jre/lib下进行扩展
不推荐,尽可能要不去更收JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载
2.使用参数扩展
推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展
扩展类加载器
扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
默认加载Java安装目录下/jre/lib/ext下的类文件
通过扩展类加载器去加载用户jar包:
1.放入/jre/lib/ext下进行扩展不推荐,尽可能不要去更改JDK安装目录中的内容
2.使用参数进行扩展推荐,使用-D java.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
加载时地址最好用双引号引起来防止当成特殊字符干扰
应用程序加载器
加载classpath下的类文件,包括了用户自己编写和导入jar包的类和文件。
JDK8之后的类加载器
由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。
1.启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
2、扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader, BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。
双亲委派机制
双亲委派机制概念
1、当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返
回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。
2、应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加
载器。
3、双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如
java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
运行时数据区
程序计数器
- 程序计数器是线程不共享的 ,每个线程都有单独的一个程序计数器
- 作用是保存下一条指令的地址在哪 里。
- 在多线程执行情况下,java虚拟机需要通过程序计数器记录cpu切换前解释执行到的那一条指令并继续解释运行
栈
Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先
进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。
Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线
程中执行,每个线程都会包含一个自己的虚拟机栈。
java 遇到异常时候就是将栈帧从栈中弹出,打印栈帧的信息,所以打印出的方法顺序是跟执行使劲啊相反
栈帧组成:
1.局部变量表
- 作用是保存在运行过程中存放的局部变量.编译成字节码文件时就可以确定局部变量表的内容
- 栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽。
- 方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。
- 局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。
- 为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。
public void test4(int k, int m){
{
int a = 1;
int b = 2;
}
{
int c = 1;
}
int i = 0;
long j = 1;
}
上述代码占用了5个槽。
2.操作数栈
- 操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。
- 在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。
3.帧数据
- 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
- 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
- 异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
栈内存溢出
- Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出。
- Java虚拟机栈内存溢出时会出现StackOverflowError的错误
java虚拟机栈设置大小
- 要修改Java虚拟机栈的大小,可以使用虚拟机参数-Xss。
- 语法 :- Xss栈大小
- 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
堆
- 每个线程只有一份堆,多个进程公用一个堆,创建出来的对象都在堆上。
- 栈上的局部变量表中可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享
- 堆空间有三个需要关注的值,used total max。used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
- 随着堆中对象的增多当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆
- 不是used=max=total的时候堆内存溢出,将在后面的垃圾回收机制进行讲解
- 如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64。在实际应用中一般都需要设置total和max的值。
堆设置大小
- 要修改堆的大小,可以使用虚拟机参数-Xmx(max最大值)和-Xms(初始的total)。
- 语法 :- Xmx值 -Xms值
- 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
- 限制:Xmx必须大于 2 MB,Xms必须大于1MB
方法区
类的元信息
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象。在类的加载阶段完成。
运行时常量池
- 存放的是字节码中的常量池内容
- 字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
字符串常量池
字符串常量池存储在代码中定义的常量字符串内容。比如"123"这个123就会被放入字符串常量池。
JDK6之前的版本中,静态变量是存放在方法区中的,JDK7之后,静态变量存放在堆中的Class对象中
直接内存
直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。
在JDK 1.4中引入了NIO机制,使用了直接内存,主要为了解决以下两个问题:
1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
- 要创建直接内存上的数据,可以使用ByteBuffer。
- 语法: ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);如果需要手动调整直接内存的大小,可以使用-XX:MaxDirectMemorySize=大小
- 单位k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。默认不设置该参数情况下,JVM自动选择最大分配的大小。
垃圾回收
算法评判标准
1.stw
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
2.吞吐量
吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量=执行用户代码时间/(执行用户代码时间+GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
3.最大暂停时间
就是stw时间最大值
3.堆使用效率
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
方法区回收
方法区能回收的内容需要同时满足以下三个条件
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
- 加载该类的类加载器已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用。
手动触发垃圾回收
- 如果需要手动触发垃圾回收,可以调用System.gc()方法。
- 语法:System.gc()
- 注意事项:调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。
- 可以通过添加参数查看类加载卸载的日志
堆回收
垃圾回收的两个大阶段:
- 找垃圾,判定垃圾。
- 释放垃圾。
判断垃圾方法
1.引用计数器
就是针对每个对象,都会额外引入一小块内存,保存这个对象有多少个引用指向它。
引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但是它也存在缺点,主要有两点:
- 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
- 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。如下代码
java
class A{
A a;
}
a1=new A();
a2=new A();
a1.a=a2;
a2.a=a1;
a1=null;
a2=null;
2.可达性分析
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GCRoot)和普通对象,对象与对象之间存在引用关系。
如图:e f 未跟root建立可达关系,所以ef是垃圾
以下对象是GC Root对象
a)栈上的局部变量
b)常量池中的引用指向的对象
c)方法区中的家庭成员指向的对象
回收垃圾算法
1.标记清除法
标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
清除阶段,从内存中删除没有被标记也就是非存活对象。
回收前
回收后
优点:
实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:
1.碎片化问题
由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
2.分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才 能获得合适的内存空间。
2.复制算法
1.准备两块空间from和to。
2.对象首先放入from空间
3.垃圾回收后,存活对象放入to空间
4.将两块空间名字互换
优点:
吞吐量高
复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动
不会发生碎片化
复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可
用空间,不存在碎片化内存空间。
缺点:
内存使用效率低
每次只能让一半的内存空间来为建对象使用
3.标记整理算法
1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
2.整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
优点:
1.内存使用效率高
整个堆内存都可以使用,不会像复制算法只能使用半个堆内存
2.不会发生碎片化
在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
缺点:
整理效率不高
4.分代GC
分代垃圾回收会将上面几个算法组合使用。
分代垃圾回收将整个内存区域分为年轻代 和老年代,老年代存放存货时间比较长的对象。年轻代又划分成了伊甸园区和幸存者区。幸存者区又分为两块区域,用来实现复制算法,我们称为s0和s1
1.首先创建出的对象会放入伊甸园区
2.伊甸园区满了,就会触发一次年轻代的垃圾回收,称为Minor GC,会用复制算法将伊甸园区和From需要回收的对象回收,把没有回收的对象放入To区。
3.继续往伊甸园区放入对象,当伊甸园区满了之后继续触发2流程,每次Minor GC都会为对象记录他的年龄,初始值为0,每次GC完加1。如果年龄达到阈值,对象会被晋升为老年代。
4.当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
垃圾回收器的实现
垃圾回收器是垃圾回收算法的具体实现。
由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。
**1.Serial 收集器(新生代)/ Serial Old 收集器(老年代)**jvm配置参数:-XX:+UseSerialGC 新生代、老年代都使用串行回收器。
这俩都是串行收集,Serial使用的是复制算法,Seral Old使用的是标记整理算法。在进行垃圾扫描和释放的时候,业务线程要停止工作。就是这边停止工作,他先扫描完,再去进行释放,然后业务线再继续工作。这种方式扫描的慢,释放的也慢,也会产生严重的 STW 问题。
2.ParNew 收集器(年轻代)
使用的是复制算法,JVM 配置参数: -XX:+UseParNewGC 新生代使用ParNew回收器,老年代使用串行回收器。
这个回收器是并发收集的,引入了多线程,Parallel Scavenge 比 ParNew 多出了一些参数,可以用来控制 STW 的时间
3.CMS垃圾回收器(老年代)
标记清除算法,参数:XX:+UseConcMarkSweepGC
执行步骤:
1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。
2.并发标记,标记所有的对象,用户线程不需要暂停。
3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
4.并发清理,清理死亡的对象,用户线程不需要暂停。
缺点:
1.CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N参数(默认0)调整N次Full GC之后再整理。
2.无法处理在并发清理过程中产生的"浮动垃圾",不能做到完全的垃圾回收。
3.如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
4.Parallel Scavenge垃圾回收器(年轻代 )
最大暂停时间
-XX:MaxGCPauseMillis=n
设置每次垃圾回收时的最大停顿毫秒数
吞吐量-XX:GCTimeRatio=n
设置吞吐量为n(用户线程执行时间=n/n+1)
自动调整内存大小-XX:+UseAdaptiveSizePolicyi
可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小
复制算法,这个回收器是并发收集的,引入了多线程,Parallel Scavenge 比 ParNew 多出了一个参数,可以用来控制 STW 的时间
优点
吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数
缺点
不能保证单次的停顿时间
适用场景
后台任务,不需要与用户交互,并且容易产生大量的对象比如:大数据的处理,大文件导出
5.Parallel Old垃圾回收器(老年代)
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。参数 :- XX:+UseParallelGC 或-XX:+UseParallelOldGC可以使用Parallel Scavenge+Parallel Old这种组合。
优点
并发收集,在多核CPU下效率较高
缺点
暂停时间会比较长
6.G1垃圾回收器
参数1 :- XX:+UseG1GC 打开G1的开关,
JDK9之后默认不需要打开
参数2 :- XX:MaxGCPauseMillis=毫秒值
最大暂停的时间
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小CMS关注暂停时间,但是吞吐量方面会下降。而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
JDK9之后强烈建议使用G1垃圾回收器。
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数
-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
G1回收器有两种方式
1.年轻代回收2.混合回收
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
执行流程
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC.
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。