JVM(Hotspot)
一.介绍
- JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
- 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
##二.JVM结构图
2.1程序计数器(PC寄存器)
程序计数器是一块很小的空间,它可以看作是当前线程所执行的字节码的行号指示器。因为代码是在线程中运行的,线程有可能被挂起,即CPU一会儿执行线程A,但是线程A还没有执行完被挂起了,接着它又去执行B,最后又来执行线程A,CPU必须得知道执行线程A的哪一部分指令,而线程计数器会告诉CPU。
2.2 虚拟机栈(java栈)
虚拟机栈存储当前线程运行方法所需要的数据,指令,返回地址。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
2.3 本地方法栈
本地方法栈与虚拟机栈锁发挥的作用是非长相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
2.4 Java堆
Java堆是java虚拟机锁管理的内存中最大的一块,所谓的JVM调优,绝大多数情况下是在这里面调它的参数。Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
2.5 方法区
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
三.双亲委派
3.1ClassLoader
JVM中提供了三层的ClassLoader:
- Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
- ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
- AppClassLoader:主要负责加载应用程序的主函数类
3.2 加载流程
(此处的父类子类并不是真正的父类子类,只是名义上的)
从上图中我们就容易理解了, 当Hello.class文件要被加载时,首先会在AppClassLoader中检查是否加载过,如果有那就无需加载了,如果没有的话,那么就会拿到父类的加载器(ExtClassLoader),然后调用父类加载器的loadClass方法。同理,父类中也会先检查自己是否加载过,如果没有就往上走,这就类似于一个递归的过程,直到到达Bootstrap classLoader之前,都是在检查自己是否加载过,并不会去自己加载。直到Bootstrap classLoader,已经没有父类加载器了,这时候就考虑自己是否能加载,如果自己可以加载,那么就直接自己加载此类,如果不能,则会下沉到子加载器中去加载,子加载器同样会判断自己是否能加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
3.3 为什么要设置这种机制
这种设计有个好处是,如果有人想替换系统级别的类,如String.java,想篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了。为什么? 因为当一个类需要加载的时候,首先去尝试加载的就是Bootstrap classLoader,所以其它类加载器没有机会再去加载,因此从一定程度上防止了恶意代码的植入。
3.4 这种机制的弊端
双亲委派很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的类加载器进行加载),基础类之所以称为"基础",是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调回用户的代码,那该怎么办?
3.5 如何打破这种机制
典型的两个方法:
- 自定义类加载器,重写loadClass()方法和findClass()方法 (继承ClassLoader)
- 使用线程上下文类加载器
方式一:自定义类加载器
重写loadClass()就是重新指定全限定类名加载class,重写了这个方法以后就能自定义使用什么加载器了,也可以自定义加载委派机制,也就打破了双亲委派的模型。
方式二:线程上下文类加载器(Thread Context ClassLoader)
这类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置,如果不特殊设置就会从父类继承,一般默认使用的是应用程序类加载器(也就是系统加载器),很明显,线程上下文类加载器让父级类加载器能够通过子级类加载器来加载类,这就打破了双亲委派模型的原则。
JDBC就比较典型
第一:获取线程上下文类加载器,从而也就获得了应用程序类加载器,也可能是自定义的类加载器。
第二:从META-INF/service/java.sql.Driver文件中获取具体的实现类名 “com.mysql.jdbc.Driver”。
第三:通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端。
四.JDK、JRE、JVM的关系
五.内存模型(堆内存)
- 堆空间 = 新生代(1/3)+ 老年代(2/3)
- 新生代 = Eden(8/10) + From(1/8) + To(1/8)
- 元空间可以直接理解为物理内存
六.Java GC
6.1 介绍
如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。 那么哪些内存需要回收,就是垃圾回收机制第一个要考虑的问题!
6.2 GC运行流程
大多数情况下,对象在Eden区分配,当Eden没有足够的内存空间时触发一次Minor GC ,会将存活的对象从Eden区移动到S0内存区域,并清空Eden区, 当再次发生Minor GC时, 将Eden和S0中存活的对象移到S1区;存活对象反复在S0和S1移动(也就是From到To),当对象在Surivivor之间移动或者从Eden移动到Surivivor区时,对象的GC年龄会不断增加,当GC年龄超过默认阈值15会进入老年代 ,老年代用于存放经过几次MinorGC后依然存活的对象,当老年代空间不足时会触发Full GC/Major GC,速度比MinorGC慢十多倍 。
6.3 如何判断对象是否存活
-
引用计数法
在对象上添加一个引用计数器,每当一个地方引用这个对象时 ,计数器值+1;当引用失效时,计数器值-1,任何时刻计数值为0的对象就是不可能再被使用的。
优点:实现简单,判定高效
缺点:很难解决对象之间相互引用的情况
-
可达性分析法
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
6.4 垃圾回收算法
- Mark-Sweep(标记-清除)算法, 标记需要回收的对象,然后清除,会造成许多内存碎片。
- Copying(复制)算法 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。
- Mark-Compact(标记-整理)算法(压缩法) 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。
- Generational Collection(分代收集)算法 分为年轻代和老年代,年轻代时比较活跃的对象,使用复制算法做垃圾回收。老年代每次回收只回收少量对象,使用标记整理法。
6.5典型的垃圾回收器
- CMS
- G1
onal Collection(分代收集)算法 分为年轻代和老年代,年轻代时比较活跃的对象,使用复制算法做垃圾回收。老年代每次回收只回收少量对象,使用标记整理法。
6.5典型的垃圾回收器
- CMS
- G1