一、什么是JVM?
JVM(Java Virtual Machine,Java虚拟机)
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
- 简单来说JVM是用来解析和运行Java程序的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
二、JVM的体系结构
1.JVM的位置
2.JVM简单体系结构图:
JVM详细体系结构图:
三、类装载器(Class Loader)
1.类装载器是什么?
负责加载class文件,class文件 **在文件开头有特定的文件标识** ,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载, 至于它是否可以运行,则由Execution Engine决定。
2.用来干嘛?
用上面的Car实例来解释:
- Car.class 是由 .java 文件 经过编译而得来的 .class文件,存在本地磁盘;
- ClassLoader: 类装载器,作用就是加载并初始化 .class文件 ,得到真正的 Class 类,即模板;
- Car Class : 由 Car.class 字节码文件,通过ClassLoader 加载并初始化而得,那么此时 这个 Car 就是当前类的模板,这个Car Class 模板就存在 【方法区】;
- car1,car2,car3 : 是由Car模板经过实例化而得,即 new出来的 --> Car car1 = new Car();
- car1.getClass 可以得到其模板Car 类,Car.getClassLoader() 可得到其装载器。
3.类装载器的种类
- 启动类加载器 也叫根加载器 (Bootstrap),由C++编写 ,程序中自带的类, 存储在$JAVAHOME/jre/lib/rt.jar中,如object类等;
- 扩展类加载器 (Extension),Java 编写 ,在我们平时看到的类路径中,凡是以javax 开头的,都是拓展包,存储在$JAVAHOME/jre/lib/ext/*.jar 中;
- 应用程序类加载器 (AppClassLoader),即平时程序中自定义的类 new出来的。
4.java类的加载机制
启动类加载器 --> 拓展类加载器 --> 应用程序类加载器
四、双亲委派机制
1.概念:
- 类加载器收到类加载请求;
- 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器;
- 启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前加载器;否则,抛出异常,通知子加载器进行加载;
- 重复步骤3.
- 采用双亲委派的一个好处就是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是会委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个Object对象。
五、沙箱安全机制
1.是什么?
限制程序运行的一些环境。
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。
六、本地方法接口 (JNI :Native Interface)
1.native是什么?
native:在Java中是一个关键字,有声明,无实现。
凡是带了native关键字的,说明Java的作用达不到了,会去调用底层C语言的库(native libraies)。
- 先进入本地方法栈;
- 再调用本地方法本地接口JNI;
2.JNI作用
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是想融合C/C++ 程序,Java诞生之初,正式C/C++ 盛行之时,因此,Java要想立足,则必须要调用C/C++程序,于是在内存中专门开辟了一块区域 处理标记为native的代码,它的具体做法就是 Native Method Stack 中登记 native 方法,在 Exection Engine 执行时加载 native libraies。 目前该方法使用的越来越少,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理的生产设备,在企业级应用中比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket 通讯,也可以使用Web Service等等。
七、PC寄存器(程序计数器)(PC Registers)
1.是什么?
记录了方法之间的调用和执行情况,类似班级的值日表,用来存储指向下一条指令的地址,也即将要执行的指令代码。(每一个线程都有一个私有的程序计数器)
八、方法区(Method Area)
1.是什么?
供各线程共享的运行时内存区域。存储了每一个类的结构信息(模板)。
什么数据会存在方法区: 静态变量static、常量final、类信息class(构造方法、接口定义),运行时的常量池都存在方法区中,但实例变量存储在堆内存中,和方法区无关。
九、栈(Stack)
1.是什么?
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,他的生命周期是跟随线程的生命周期,线程结束那么栈内存也就随之释放, 对于栈来说不存在垃圾回收问题 ,只要线程已结束该栈就over了, 是线程私有的。
8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。
2.栈存储什么?
栈帧(Stack Frame)中主要保存 3 类数据 : (何为栈帧:即Java中的方法,只是在jvm中叫做栈帧)
- 本地变量 (Local Variables) : 入参和出参 以及方法内的变量;
- 栈操作 (Operand Stack) : 记录出栈 和 入栈的操作;(可理解为pc寄存器的指针);
- 栈帧数据 (Frame Data) : 包括类文件、方法等。
3.栈的运行原理——“先进后出,后进先出”
- 栈中的数据都是以栈帧 (Stack Frame) 的格式存在,栈帧是一个内存区块,是一个有关方法和运行期数据的数据集;
- 每个方法执行的同时都会创建一个栈帧,用于存储局部变量表,操作数据栈,动态连接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的操作过程。
十、堆(Heap)
1.是什么?
堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
2.堆区
- 存储的全部都是对象,每个对象包含了一个与之对应的 class 类的信息。
- jvm 只有一个堆区(steap),它会被所有线程共享,堆中不存放基本数据类型和对象引用,它只存放对象本身。
3.堆的划分
Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代Young、老年代Old和永久代Permanent(对HotSpot虚拟机而言,JDK1.8之后为元空间metaspace替代永久代),这就是JVM的内存分代策略。
- 新生代(Young Generation):
新生代含有3个区域,1个Eden区,2个Survivor区,可以分别称为from以及to区,from与to的大小相同。它 们占用的大小比例默认为Eden:from:to=8:1:1,当然也可以通过参数-XX:SurvivorRatio来自定义比例。 新创建的对象都会处在Eden区,大对象会直接进入老年代,可以使用参数-XX:+PretenuerSizeThreshold来 指定多大的对象。 新创建的对象优先在Eden区上分配,Eden区满了之后,会触发一次Minor GC,虚拟机会采用复制算法(不会 产生内存碎片)先进行释放内存,回收死亡对象,然后将存活的对象一次性复制进from区域中。若from区域不 够,则使用分派担保机制将部分对象直接推入老年代。老年代空间不够,将触发一次Full GC。 发生过一次Minor GC之后,Eden区域基本空闲,新创建的对象依然在这块区域上分配,当Eden区域又满了之 后,同样还是出发第二次Minor GC,这次GC将会回收Eden与from区域,将存活的对象一次性地复制进to区域中。 这些对象在新生代中每熬过一次Minor GC,对象的年龄就会加1,当年龄达到某一个阈值时(默认为15),将 会被直接移到老年代中,可以使用参数-XX:MaxTenuringThreshold来指定这个阈值。 因为新生代区域上对象,创建频繁,死亡也频繁,因此Minor GC也会变得十分频繁,但是这种GC方式效率 高,持续时间短。
- 老年代(Old Generationn):
-
老年代中主要有老年对象(超过年龄阈值的对象)以及大对象。当老年代空间不足时,会触发一次Full GC, 由于各个垃圾收集器的实现不同或处于效率考虑,可能会采用标记清除算法,也可能采用标记整理的算法回收死亡 对象。 当然,也不是对象必须达到年龄阈值才会进入老年代中,如果Survivor内某个相同年龄下(比如10岁)的所 有对象的大小总和超过Survivor区的一半,那么年龄≥10岁的对象将直接进入老年代中。 Full GC不像Minor GC那么频繁,但持续时间长。倘若频繁发生Full GC,会严重阻碍应用程序的执行。
- 永久代(Permanent Generationn):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。关闭VM就会释放这个区域内存。
4.堆区的作用
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。
有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。
十一、垃圾回收(GC)(只在堆和方法区中有)
1.GC-引用计数法
:利用计数器对每个对象的使用次数进行记录,对象的次数为0则被垃圾回收。但这种算法不好,有消耗,很少使用。
2.GC- 复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:没有内存碎片;
缺点:浪费了内存空间;
3.GC- 标记清除算法
清除算法由标记阶段和清楚阶段构成,在标记阶段会把所有的活动对象都做上标记,然后在清除阶段会把没有标记的对象,也就是非活动对象回收。
优点:不需要额外空间;
缺点:两次扫描严重浪费时间,会产生内存碎片;
标记压缩(防止内存碎片产生)
缺点:多了一个移动成本;
4.GC算法总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度);
内存整齐度:复制算法 = 标记压缩算法 >标记清除算法;
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法;
年轻代:存活率低 --> 复制算法;
老年代:区域大 --> 标记清除算法(内存碎片不是太多的时候用)+标记压缩算法 混合实现;