一、什么是JVM
1.引言
jvm即Java Virtual Machine(java虚拟机)的简写,如果你在面试的时候被面试官问到什么是JVM时候,你回答这句话,那么恭喜你,你就GG了,面试官问你JVM时候,作为一个面试者,至少得和面试官谈15分钟左右,这里将通过六章来阐述JVM以及分析。
2.为什么需要JVM
当我们第一天学习java的时候,就听说这个词语了,jvm主要是作为一个翻译官的角色,Java虚拟机会将字节码,即class文件加载到JVM中。由JVM进行解释和执行。除了 Java 外,Scala、Clojure、Groovy,以及时下热门的 Kotlin,这些语言都可以运行在 Java 虚拟机之上。
3.JVM运行流程
4.常见的JVM
1.HotSpot VM(目前应用最广的JVM),有点是热点代码探测技术
2.Sun Classis VM(世界上第一款商用虚拟机,已淘汰)
3.Jrockit(IBM公司开发,曾广泛应用于IBM公司系统内部及IBM小型机上)
二、JVM内存体系
1.类加载器与类加载过程
a.类加载器的作用
当我们将java文件编译为字节码文件(class File)之后,就需要类加载器将字节码文件加载到JVM中,类加载器(即ClassLoader),它负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
b.类加载器分类
那么,类加在器有哪些呢:
1.启动类加载器(Bootstrap):由C/C++实现,加载jre中的最为基础、最为重要的类,如rt.jar
2.扩展类加载器(Extension):由Java代码实现,用于加载相对次要、但又通用的类,如ext 目录下 jar 包中的类
3.应用程序类加载器(AppClassLoader):由Java代码实现, 它负责加载应用程序路径下的类,即用户自己写的类由应用程序类加载器加载
4.用户自定义类加载器:用户可以定制类的加载方式,例如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。
c.类的加载过程
类的加载主要分为加载–>链接–>初始化
1.加载:将(硬盘/网络/数据库中的)字节码文件加载到JVM内存中。
2.链接:链接主要分为3步:
①验证:并不是所有的字节码文件都会被加载,加载前会先进行验证,例如:字节码是否是合法的字节码文件(例如是否以cafe babe开头),当前JVM运行的JDK版本是都可以运行该字节码文件的数据(例如JDK1.8可以运行JDK1.7编译的版本,反过来不行,即向前兼容)
②准备:给成员变量(类变量/静态变量)默认初始值,把常量(final)等值在方法区准备好
③虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程(把类中的对应的类型名替换为该类型的Class对象地址)
3.初始化
1.类初始化调用clinit方法(静态变量的显式赋值代码,静态代码块代码)
2.当一个类初始化时,发现他的父类没有初始化,会先初始化父类
3.每个类只会初始化一次,类初始化过程是线程安全的[如果有一个线程在初始化类,其他类则会等着]
4.类的加载不一定会发生类的初始化,可能是1.2.3一起完成(大多数时候),也可能是先完成1.2,某个时候才开始初始化
d.哪些情况会导致初始化?
1.main方法所在的类加载时,直接就先初初始化
2.new一个对象,一定会先完成类的初始化
3.调用该类的静态变量(除了final外)或静态方法(即在A类调用B类的静态方法,则也会初始化B类)
4.使用java.lang.reflect包的方法对类进行反射调用
5.当一个类初始化时,发现他的父类没有初始化,会先初始化父类
*e.哪些情况不会导致初始化?
1.调用静态常量(final)时,不会初始化。
2.当访问一个静态域时,只有真正申明这个域的类才会被初始化(子类访问父类的静态域(例如静态属性)时,只会初始化父类,不会初始化子类)
3.创建某个类的数组对象时,不会初始化该类(例如A[] arr=new A[];不会初始化A类,并且arr的类型是A[]类型,或者说是[L类型)
* 对于不会导致类的初始化,下面将通过三遍代码来解释
1.类型一 代码演示:
public class Test01 {
public static void main(String[] args) {
System.out.println(User.NAME);//只输出 zhangsan
//System.out.println(User.id);//输出 静态代码块1 zhangsan
}
}
class User{
public static final String NAME="zhangsan";
public static String id="1001";
static {
System.out.println("静态代码块1");
}
public User(){
System.out.println("构造方法1");
}
}
2.类型二代码演示
public class Test02 {
public static void main(String[] args) {
/*
如果子类调用父类的静态属性,只会初始化父类,不会初始化子类
*/
System.out.println(B.name); //输出: 父类A的静态代码块 AAAAA
}
}
class A{
public static String name="AAAAA";
static {
System.out.println("父类A的静态代码块");
}
}
class B extends A{
static {
System.out.println("子类B的静态代码块");
}
}
3.类型三代码演示
public class Test03 {
public static void main(String[] args) {
/*
* 创建某个类的数组对象时,不会初始化该类
* */
Stu[] stus=new Stu[10];
}
}
class Stu{
public static String name="stu";
static {
System.out.println("stu的静态代码块");
}
}
f.类加载结果
在方法区会有一个唯一的的Class对象,每一种类型都有一个Class对象,包含基本数据类型与void
2.栈
一个线程的每个方法在调用时都会在栈上划分一块区域(线程私有),用于存储方法所需要的变量等信息,这块区域称之为栈帧(stack frame)。栈由多个栈帧构成,一个方法调用几个方法就会有几个栈帧,栈中的数据都是以栈帧(Stack Frame)为载体存在。在栈中,方法的调用顺序遵循“先进后出”/“后进先出”原则。
3.堆
堆(java虚拟机栈)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存的大小是可以调节的(通过 -Xmx 和 -Xms 控制)。几乎所有的对象实例以及数组都要在堆上分配。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”(Garbage Collected Heap)。
那么,所有的对象一定是在堆中吗?
不一定,有些时候,对象也可能存在栈中,这种现象称为栈上分配,哪些对象可以栈上分配,那么就需要逃逸分析。
为什么需要栈上分配?
有些时候,我们对象可能只用到了一次,然后就不用了,如果在堆中,如果一个对象没有引用被指向时,就会成为垃圾,等待垃圾回收器的回收,但是垃圾回收器并不会立即回收,这样就造成了内存浪费,但是栈就能很好解决这个问题,因为栈中方法执行完毕会自动弹栈,那么将对象放在栈中,用完之后,就会被弹栈,释放内存。
逃逸分析?
逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外。
堆的分类:新生代与老年代
1.新生代(占1/3的堆空间),新生代又分为:
a. 伊甸区(Eden) 此区域占新生代的8/10:
1.刚new出来的对象会放在伊甸区
2.伊甸区空间块满时,会触发垃圾回收,当一个对象在没有堆外的引用指向时,就会被判定时垃圾
3.触发垃圾回收后,没有引用的对象将被垃圾回收,有引用的对象将放置到幸存者0区
4.当伊甸区在此被放满时,需要再次垃圾回收
5.此时将回收整个新生代(也就是伊甸区和幸存者1区)
6.此时将回收整个新生代(也就是伊甸区和幸存者1区(from区))中没有被引用的对象
b.幸存者0区(from 区) 此区域占新生代的1/10:
1.幸存者0区就会放置很多有引用的对象
2.此时,幸存者1区(form区)中没有被引用的对象将会被回收,有引用的对象将会复制幸存者2区(to区)
3.此时,整个from区将整个被清空,然后和幸存者1区(to区)交换指针,此时from区就变成了to区,to区就变成了from区
c.幸存者1区(to 区) 此区域占堆内存的1/10:
1.交换指针之后,from区就变成了to区,to区就变成了from区,所以to区依然为空,这也就解释了为什么to区永远是空的(相当于一个过渡区,中间区)。
2.注意:幸存者1区(to区)永远是空的,将不会放置任何对象,0区与1区大小完全一样,将轮流变换为from区与to区(通过交换指针变化)。谁是from区,谁是to区,取决于里面有没有东西,有就是from区,否则就是to区
注意:新生代发生的GC叫MiniGC
2.老年代(占2/3的堆空间)
1.如果一个对象年龄为15岁,则会到老年代(即经历了15次GC,仍然存活下来)
2.如果新创建的对象伊甸区放不下,而老年代可以放下,则会放在老年代
3.如果from区空间已放满,而伊甸区经过垃圾回收之后又有新的幸存者对象需要放到from区,则JVM会将from区中年龄较大的对象放到老年代(即使没到15岁,但是年龄是最大的)
注意:经常引用的对象就在老年代,老年代发生的GC叫FullG
4.方法区
方法区在1.8也称之为元空间,放置类、接口的元数据,也就是类信息,被装载进此区域的数据不会轻易的被垃圾回收器回收,关闭JVM时才会释放此区域所占用的内存,如果出现OutOfMemoryError:meta space,说明永久代/元空间内存设置不够,字符串常量池也在元空间
5.程序计数器
当前线程所执行的字节码行号指示器
6.本地方法栈
为本地方法栈为虚拟机使用到的native方法服务