初识JVM
jvm就是java虚拟机,是一个运行在计算机上的文件,职责是运行java字节码文件

jvm的功能

jvm的组成
主要有四个部分
类加载器(ClassLoader)
运行时数据区
执行引擎
本地方法栈
字节码文件的组成
(1)基础信息:魔数,编译时使用的jdk版本号,访问标识(private,public...),父类和接口
(2)常量池:字符串常量,类或接口名,字段名
(3)字段:当前的类或接口声明的字段信息
(4)方法:当前类或方法声明的接口信息
(5)属性:类的属性,比如码源的文件名或内部类列表等
字节码文件常见工具使用
1.javap -v
是jdk自带的反编译工具,他的展示效果一般,但是操作方式相对简单
可以查看服务器里的字节码文件
2.idea的jclasslib插件

3.Arthas
是阿里开源的向上诊断工具,可不重启服务排查:CPU 高、死锁、慢接口、内存泄漏、查看方法入参 / 返回值、热更新代码等。
相关的命令


类的生命周期
加载阶段
具体步骤
1.类加载器根据类的全限定名通过各种渠道以二进制流的方式获取字节码信息
2.类加载完成之后,jvm会将信息保存到方法区中
3.类加载完成之后,jvm会将信息保存到内存的方法区中
4.同时,jvm还会在堆中生成一份与方法区类似的java.lang.Class对象

连接阶段

初始化阶段
1.会执行静态代码块中的代码,并为静态变量赋值
2.会执行字节码文件中的clinit部分字节码指令
需要注意的是

使用阶段
卸载阶段
类加载器ClassLoader
是jvm提供给应用程序去实现获取类和接口获取字节码数据的技术

应用场景

类加载器的分类
主要分为两类,java实现的,jvm底层源码实现的;

启动类加载器
是由hotspot虚拟机提供的使用C++编译的类加载器
默认加载java安装目录/jre/lib下的类文件

扩展类加载器
应用程序类加载器


双亲委派机制

1.保证类加载的安全性
可以避免恶意代码替换jdk中的核心类库
2.避免重复加载
可以避免同一个类被重复加载
如何使用代码的方式去主动加载一个类?
1.使用Class.forName()方法
2.获取类加载器,使用类加载器的loadClass方法指定某个类加载器加载
打破双亲委派机制
使用场景

1.自定义类加载器
loadClass包含了四个核心方法

自定义类加载器需要继承ClassLoader然后重写findClass方法,这样不会破坏双亲委派机制
如果重写loadClass就会打破双亲委派机制
2.线程上下文类加载器

3.Osgi框架的类加载器
JDK9之后的类加载器
jdk8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sum.misc.Launcher.java

jdk9引入了module的概念,类加载器设计上发生了很多变化
1.启动类加载器由java编写,位于jdk.internal.loader.ClassLoader类中
2.扩展类加载器被替换成了平台类加载器
运行时数据区

主要分为线程共享和线程不共享两部分
主要的应用场景


程序计数器
也叫PC寄存器,每个线程都会通过程序计数器记录当前要执行的字节码指令的地址
不会出现内存溢出的问题,并且是线程私有的

栈
java虚拟机栈
采用了栈的数据结构,先进后出,每一个调用使用一个栈帧来保存


栈帧的组成

栈帧中的局部变量表是一个数组,数组中的每一个位置称之为槽,long,double占两个槽,其他类型占一个槽
操作数栈用来存放临时数据或中间数据的
java虚拟机栈会出现内存溢出,StackOverflowError错误

栈的默认大小

可以使用
-Xss栈大小 指定栈的大小

本地方法栈
为native修饰的方法提供的栈帧
堆
是空间最大的一块内存区域,创建出来的对象都存与堆上
栈上面的局部变量表可以存储堆上的对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。

如果创建的对象太多了,超出了堆内存的空间,就会抛出OutOfMemoryError
对空间有三个需要关注的值,used total max
可以通过dashboard命令看到

可以使用
-Xms值 (total)
-Xmx值 (max)
设置最大堆内存的值
方法区
是一块线程共享的区域,存储类的元信息(在类的加载阶段加载进来),运行时常量池,字符串常量池



方法区中的字符串常量池
方法区中除了类的元信息,运行时常量池之外,还有一块区域叫字符串常量池(StringTable)
用来存储再代码中定义的常量字符串内容。

方法去的发展过程

例题


intern
String.intern()可以手动将字符串放入字符串常量池中,分别在JDK6 JDK8下执行代码, JDK6中的结果是false false,JDK8中是true false

注意,JVM启动时就会把java加入到常量池中
jdk6版本中的intern()方法会把第一次遇到的字符串实例复制到永久代的字符串常量池中,返回的也是永久代里面这个字符串实例的引用。JVM启动时就会把java加载到常量池中
jdk7及之后版本由于字符串常量池在堆上,所以intern()方法会把第一次遇到的字符串的引用放入字符串常量池。
静态变量的存储
jdk6存储在方法区中,jdk7及之后的版本存放在堆中的class对象中,脱离了永久代
直接内存
在JDK1.4中引入了NIO机制,使用了直接内存,主要时=是为了解决两个问题:
1.java堆中的对象如果不在使用要回收,回收时会影响对象的创建和使用。
2.IO操作比如读文件,需要先把文件读入直接内存再把数据复制到java堆中
自动垃圾回收机制
内存泄露指的是不在使用的对象在系统中没有被回收,内存泄漏的积累可能导致内存溢出
Java为了简化对象的释放,引入了自动的垃圾回收机制。主要负责堆上进行垃圾回收
方法区的回收
类的生命周期回顾

判定一个方法区中的类可以被卸载,需要同时满足以下三个条件:
1.此类所有实例都已被回收,在堆中不存在任何该类的实例对象以及子类对象。
2.加载该类的类加载器已经被回收。
3.该类对应的java.lang.Class对象没有在任何地方被引用。
堆回收
引用计数法&可达性分析法
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收
如何判断堆上的对象又没有被引用:常有两种判断方法:引用计数法和可达性分析
引用计数法:每当一个对象被引用之后计数加一,有两个缺点
1.每次引用和取消引用都需要维护计数器,对系统性能有一定的影响
2.存在循环引用的问题,a引用b,b引用a,计数器就会一致增长,导致a,b无法回收,出现了内存泄漏
可达性分析算法:将对象分为两类,垃圾回收的根对象和普通对象,对象与对象之间存在引用关系
可达性分析算法就是如果某个到GC Root对象是可达的,对象就不可被回收

哪些对象可以被成为根对象?
1.线程Thread对象
2.系统类加载器加载的java.lang.Class对象

3.监视器对象,用来保存同步锁synchronized关键字持有的对象

4.本地方法调用时使用的全局对象
arthas和eclipse Memory Analyzer(MAT)工具可以查看GC root,MAT工具时eclipse推出的Java堆内存检测工具;
1.使用arthas工具的heapdump命令将堆内存快照保存到本地磁盘中。
2.使用MAT工具打开堆内存快照文件
MAT中的GC root

3.选择GC root功能查看所有GC root
五种对像引用
1.强引用
可达性算法中描述的对象引用,一般指的是强引用,即是GC Root对象对普通对象的引用关系,只要这层关系存在,普通对象就不会被回收。
2.软引用
如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。JDK1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。
软引用的执行过程如下:
-
将对象使用软引用包装起来,new SoftReference <对象类型>(对象)。
-
内存不足时,虚拟机尝试进行垃圾回收。
-
如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
-
如果依然内存不足,抛出 OutOfMemory 异常。
3.弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含对象在垃圾回收时,不管内存够不够都会被直接回收。
在JDK1.2之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。
弱引用对象本身也可以使用引用队列进行回收。

4.虚引用
虚引用也叫幽灵引用 / 幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java 中使用 PhantomReference 实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
5.终结器引用
终结器引用指的是在对象需要被回收时,对象将会被放置在 Finalizer 类中的引用队列中,并在稍后由一条由 FinalizerThread 线程从队列中获取对象,然后执行对象的 finalize 方法。在这个过程中可以在 finalize 方法中再将自身对象使用强引用关联上,但是不建议这样做,如果耗时过长会影响其他对象的回收。
垃圾回收算法
垃圾回收要做的两件事
1.使用可达性分析算法找到内存中存活的垃圾
2.释放不存活对象的内存,使程序能够再次使用这部分内存
垃圾回收算法的历史和分类
1.标记-清除算法
核心思想分为两步:
标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象
清除阶段:从内存中删除所有没有被标记的对象

缺点:
碎片化问题:由于内存是连续的,所以对象被删除之后,内存中会出现很多细小的可用的内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小无法进行分配

分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次遍历到链表的最后才能获得合适的内存空间

2.复制算法
核心思想是
一.准备两块空间From和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)
二.在垃圾回收GC阶段,将From中存货对象复制到To空间
三.将两块空间的From和To名字互换

优点:吞吐量高 ,复制算法只需要遍历一次存活的对象复制到To空间即可,标记-整理算法少了一次遍历的过程,因而性能比较好,但不如标记-清除算法,因为标记清除算法不需要进行对象的移动。不会发生碎片化。复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
缺点:内存使用率低。每次只能让一半的内存空间来为创建对象使用
3.标记-整理算法
标记阶段:使用可达性分析算法标记所有存活的对象
整理阶段,将存活的对象移动到堆的一端。清理掉存活对象的内存空间

优点:内存使用率高
不会发生碎片化
缺点:整理阶段的效率不高
4.分代GC
会将整块内存划分为两块区域,分为年轻代,老年代

首先,创建出来的对象会被放入Eden,随着Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或Young GC
Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区,交换From和To的名称
每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1,当对象的年龄达到15后对象就会晋升到老年代(这个晋升年龄和垃圾回收器有关)
当老年代空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
垃圾回收过程会通过单独的GC线程来完成。但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The Word,简称为STW,如果STW时间过长则会影响用户的使用。

判断GC算法是否优秀,可以从三个方面来考虑:
1.吞吐量
吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值。即吞吐量=执行用户代码时间/(执行用户代码时间+GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
比如虚拟机总共运行了100分钟,其中GC花掉了1分钟,那么吞吐量就是99%
2.最大暂停时间
指的是在所有垃圾回收过程中的STW的最大值
3.堆只用效率
不同的垃圾回收算法,对堆的内存使用方式是不同的。比如标记算法,可以使用完整的堆内存,而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用率,最大暂停时间,吞吐量不可兼得
一般来说,堆内存越大,最大暂停时间越长,想要减少最大暂停时间,就会降低吞吐量
所以不同的垃圾回收算法适用于不同的场景
常见的垃圾回收器

第一种组合Serial-SerialOld
Serial
年轻代Serial是一种单线程回收年轻代的垃圾回收器

SerialOld
老年代Serial是Serial垃圾回收器的老年代版本,采用单线程串行回收

第二种组合ParNew-CMS
ParNew
ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收

CMS
关注的是系统暂停的时间,允许用户线程和垃圾回收线程在某些步骤中同时进行,减少了用户线程的等待时间

第三种组合Parallel Scavenge-Parallel Old
**年轻代Parallel Scavenge-**是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点

老年代Parallel Old垃圾回收器是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。

G1垃圾回收器
JDK9之后的默认垃圾回收器是G1垃圾回收器,Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间大小。CMS关注暂停时间,但是吞吐量方面会下降。而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的对空间回收,并由较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
G1垃圾回收器的内存图
G1的整个堆会把划分为多个大小相等的区域,称之为区Region,区域不要求是连续的,分为Eden,Survivor,Old区。Region的大小通过对空间/2048计算得到,也可以通过参数指定,Region size必须是2的指数幂,取值范围从1M-32M

G1的垃圾回收有两种:
1.年轻代回收
回收Eden,Survivor中不用的对象。会导致STW,G1中可以设置参数调整每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间
2.年代回收
G1垃圾回收器执行流程
1.新创建的对象会放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象是需要回收时会执行Young GC
2.标记出Eden和Survivor区域中的存活对象
3.根据最大的暂停时间选择某些区域将存活的对象复制到一个新的Survivor中(年龄+1),清空这些区域
4.后续的Young GC与之前相同,只不过Survivor区中存活的对象会被搬运到另一Survivor区
5.当某个存货对象的年龄到达阈值(默认15),将被放入老年代
6.如果有些对象大小超过了Region的一半,会直接放入老年代,这类老年代被称为Humonmous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。

7.多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。