Java 的JVM简介
JVM是(Java Virtual Machine)Java虚拟机的缩写。
JVM是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
在Java程序运行时,所有的.class类需要加载到JVM中才能执行代码逻辑。不是直接和操作系统交互,需要jvm通过java类库解释给操作系统。
类加载器 ClassLoader
用来查找和加载Class文件到Java虚拟机内存中,并将这些静态数据转换成方法区运行时数据结构,然后在堆中形成代表这个类的Class对象,作为方法区中类数据的访问入口。
类加载器所做的工作实质是把类文件从硬盘读取到内存中。
Java中类加载器主要分为两种,即系统类加载器和自定义类加载器。其中系统类加载器包括3种,分别是Bootstrap ClassLoader、Extensions ClassLoader和Application ClassLoader。
- 引导类加载器(Bootstrap ClassLoader):用C语言实现无法被Java代码访问。负责JDK核心库,比如java.lang、java.util等。
- 拓展类加载器(Extension ClassLoader):负责系统类额外功能,如jar包。
- 应用程序类加载器(Application ClassLoader):实现累AppClassLoader,负责加载当前程序的Classpath目录。
- 自定义类加载器(Custom ClassLoader):由开发人员自己定义。
双亲委派机制
类加载器查找Class采用双亲委派模式,即先判断该Class文件是否已经加载,如果没有则委派给父加载器进行查找,如果最顶层没有查找到则向下尝试加载类。
双亲委派机制优缺点
优点: 避免类的重复加载,保证类加载的安全性。假设自定义String类替换系统String类,显然会造成安全隐患。
缺点: 子加载器可以使用父加载器加载的类,而父加载器不能使用子加载器加载的类。
运行时数据区域
Java虚拟机在执行程序过程中会把它所管理的内存划分为不同的数据区域。
- 线程隔离区为:程序员计数器、Java虚拟机栈、本地方法栈;
- 线程共享区为:Java堆、方法区。
程序计数器(Program Counter Register)
也叫PC寄存器,是一块较小的内存空间。
JVM的多线程,就是在启动时会创建一个程序计数器,保持执行的jvm指令。通过轮流切换并分配处理器执行时间的方式来实现的,在某一时刻只有一个处理器执行一条线程中的指令。
- 程序计数器总是指向下一条将被执行指令的地址。
- 生命周期与线程的生命周期保持一致。
Java 虚拟机栈(Java Virtual Machine Stacks)
每个线程都有一个私有的虚拟机栈,它的生命周期和线程相同,与线程同时创建和结束。
- 线程结束栈内存自动释放,因此不存在垃圾回收问题。
一个虚拟机栈包含多个栈帧。栈帧用来存储局部变量表、动态链接、方法出口等信息。
- 当线程执行一个方法时,压入一个新的栈帧到该线程的虚拟机栈中。
- 如果线程请求分配的栈容量超过虚拟机最大容量,会抛出StackOverflowError;
- 如果创建新的线程或栈扩展时无法申请足够内存,会抛出OutOfMemoryError。
本地方法栈(Native Method Stack)
java里面native
关键字修饰的方法,说明java的作用范围达不到,需要去调用底层c/c++语言的库。会进入本地方法栈,然后到本地方法库。
本地方法栈也会抛出 StackOverflowError和OutOfMemoryError异常。
Java 堆(Java Heap)
堆是线程共享的运行时内存区域,用来存放对象的实例,并且这些对象被垃圾回收器管理。
这些受管理的对象无法显式地销毁,从内存回收角度堆粗略分为新生代、老年代和永久代(不存在垃圾回收,关闭jvm释放内存)。
方法区(Method Area)
方法区是被所有线程共享的运行时内存区域,用来存储的是被虚拟机加载的类结构信息:运行时常量池、静态变量(static)、方法信息(修饰符、方法名、返回值、参数等)、字段等。
方法区是Java堆的逻辑组成部分,它可以选择不实现垃圾收集。方法区并不等同于永久代。
当方法区内存空间不满足内存分配需求时,会抛出OutOfMemoryError。
运行时常量池:并不是JVM运行时数据区域的其中一份子,属于方法区的一部分。
垃圾回收器:GC
Garbage Collection,通常被称作GC。GC主要工作是做内存分配和回收。
GC采用的是分代收集算法来回收垃圾的,Java堆作为GC主要管理区域,被细分为新生代和老年代,再细致一点新生代又可以划分为Eden区、From Survivor空间、To Survivor空间。空间划分后,GC就可以为新对象分配内存空间了。
GC通过垃圾标记算法来区分对象的存活和死亡,如何标记呢?
JVM的垃圾回收是根据可达性分析算法
和引用计数算法
来标记对象是否存活的。
垃圾标记算法:可达性分析算法和引用计数算法
可达性分析算法:也称为根搜索算法。这个算法的基本思想就是选定一些对象作为根GC Roots ,然后以这些"GC Roots"的对象作为起始点,向下去搜索叶节点,如果目标对象到GC Roots是连接着的,就称该目标对象是可达的,否则为不可达,也就是被回收的对象。如下图:
引用计数器算法: 为每个对象都添加一个计数器,每多一个引用指向对象,计数器就加1,当计数器为0的对象,就是可回收的对象。
在JDK1.2之后,Java引用分为强引用、软引用、弱引用、虚引用。
垃圾收集算法 :标记清除算法 、 复制算法、标记压缩算法、分代收集算法
标记清除算法
分为两个阶段:标记和清除。比较好理解,首先标记所有需要回收的对象,然后回收所有被标记的对象。
缺点:效率不高。会产生大量不连续的内存碎片,也就会没有连续内存提供给较大的对象,导致容易触发新的一次垃圾回收动作。
复制算法
将内存划分为两个相等区域,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。
优点:效率比较高,也避免了内存碎片。
缺点:因为另一半内存一直是空的,比较浪费空间。
为什么广泛应用于新生代中?因为复制算法的效率与存活对象数目有直接关系,如果存活对象很少效率就会很高。而新生代中绝大多数对象的生命周期都很短。
标记整理算法
是标记-清除算法的升级版,也叫标记-压缩算法。在标记可回收对象后,将存活对象向着一端移动,使他们紧凑排列在一起,然后清理掉边界以外的内存。
优点:避免了内存碎片和内存利用效率低。广泛应用于老年代中。
缺点:增加了一个移动的成本。
分代收集算法
分代的概念:因为JVM各种对象生命周期各不相同,大部分对象生存时间很短暂,少部分对象生存时间很长,因此GC针对不同生命周期的对象划分不同的区域,并采取不同的收集算法。
新生代:存活率低-复制算法。划分为Eden区、From Survivor空间、To Survivor空间
老年代:区域大存活率高-标记清除+标记整理算法混合实现
注意:在GC之后,还存活的对象,进入幸存区(Survivor),谁空谁是To,可以交换位置,当一个对象经历了15次GC,还存活,就进入老年区。
(控制对象经过Minor GC次数晋升老年代的阈值:-XX:+MaxTenuringThreshold=15)