深入理解java虚拟机知识整理
什么是java虚拟机
作为一个java程序员,我们每天都在写java代码,而我们写的每一行代码都在一个叫java虚拟机的东西上执行的,但是如果要问什么是虚拟机,恐怕很多人就会模棱两可了。
网上搜索到的比较靠谱的解释:
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
jvm运行原理
Java文件经过编译器编译后编译成字节码(class文件),字节码由虚拟机解释执行,并通过虚拟机上的解释器将字节码翻译成特定机器上的机器码,然后在特定的机器上运行。
jvm组成结构
jvm虚拟机所管理的内存包括几大部分:方法区、虚拟机栈、本地方法栈、堆、程序计数器、执行引擎、本地库接口。
方法区:用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器的代码等数据。
虚拟机栈:用于存储局部变量、操作数栈、动态链接、方法出口等信息。
本地方法栈:与虚拟机栈相似,虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。可自助实现。
java堆:所有线程共享的一块内存区域,在虚拟机启动时创建,此内存有唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
垃圾收集器及算法
算法:
标记清除算法:最基础的一种算法。分为两个阶段:标记、清除。
不足:1,存在效率问题,标记和清除效率都不高 2,会产生不连续的内存碎片,会导致多次垃圾收集动作。
复制算法:效率比较高。
不足:失去一半内存为代价;在对象存活率较高时,需要进行较多的复制操作,效率将会变低。
标记整理算法:与标记清除算法一样,但后续步骤不是直接对可回收的对象进行清理,而是所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。产生的内存都是连续的。
分代收集算法:根据对象生命周期的不同将内存分为几块。一般把java堆分为新生代和老年代。新生代对象存活率低,选择复制算法,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,则使用标记整理算法。
收集器:
新生代收集器包括:Serial、PraNew、Parallel Scavenge
老年代收集器包括:Serial Old、Parallel Old、CMS
回收整个Java堆(新生代和老年代):G1收集器
新生代垃圾收集器:
Serial收集器串行收集器 - 复制算法。是新生代单线程收集器。
优点:简单高效,最基本,历史最悠久的收集器。它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集完成。
ParNew收集器 - 复制算法。是新生代并行收集器,其实就是Serial收集器的多线程版本。
Parallel Scavenge(并行回收)收集器 - 复制算法。是新生代并行收集器,追求高吞吐量,高效利用CPU。
老年代垃圾收集器:
Servial Old收集器 - 标记整理算法。是Serial收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。
Parallel Old收集器 - 标记整理算法。是Parallel Scavenge收集器的老年代版本,使用多线程和“标记整理”算法。这个收集器在1.6中才开始提供。
CMS收集器 - 标记整理算法。是一种以获取最短回收停顿时间为目标的收集器。是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更负责一些,整个过程分为4个步骤,包括:
1)初始标记 2)并发标记 3)重新标记 4)并发清除
其中初始标记、重新标记这两个步骤仍然需要“Stop The Wold”。
CMS收集器优点:并发收集、低停顿。
CMS个个明显的缺点:
1)CMS收集器对CPU资源非常敏感。 2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Fail”失败而导致另一次Full GC的产生。 3)CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多时,会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
新生代和老年代垃圾收集器:
G1收集器 - 标记整理算法。JDK1.7后全新的回收器,用于取代CMS收集器。
G1收集器的优势:
独特的分代垃圾回收器,分代GC:分代收集器,同时兼顾年轻代和老年代。
使用分区算法,不要求Eden,年轻代或老年代的空间都连续。
并行性:回收期间,可由多个线程同时工作,有效利用多核CPU资源。
空间整理:回收过程中,会进行适当对象移动,减少空间碎片。
可预见性:G1可选取部分区域进行回收,可以缩小回收范围,减少全局停顿。
G1收集器的阶段分以下几个步骤:
初始标记:它标记了从GC Root开始直接可达的对象。
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。
最终标记:标记那些在并发标记阶段发生变化的对象,将被收回。
筛选回收:首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Regin。
配置回收器时,经常使用的参数:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器。 -XX:+UseParNewGC:在新生代使用并行收集器。 -XX:+UseParallelGC:新生代使用并行回收收集器,更加关注吞吐量。 -XX:+UseParallelOldGC:老年代使用并行回收收集器。 -XX:+UseParallelGCThreads:设置用于垃圾回收的线程数。 -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器。 -XX:+UseParallelCMSThreads:设定CMS的线程数。 -XX:+UseG1GC:启用G1垃圾回收器。
虚拟机类加载机制
虚拟机的类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的整个生命周期:加载、验证、准备、解析、初始化、使用、卸载。
类加载的过程:
加载:需要完成以下3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1)文件格式验证 2)元数据验证 3)字节码验证:通过数据流和控制流分析,确定程序是合法的、符合逻辑的。 4)符号引用验证:可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
准备:是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
解析:是虚拟机常量池内的符号引用替换为直接引用的过程。
初始化:在准备阶段,变量已经赋过一次系统要求的初始值,而在初始阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
双亲委派模型
双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试去自己加载。
破坏双亲委派模型:需要重写loadClass方法。
虚拟机字节码执行引擎
栈帧(Stack Frame):用于支持虚拟机进行方法调用和方法执行的数据结构。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
一个栈帧包括:
1)局部变量表,一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。 2)操作栈 3)动态链接 4)返回地址
Java内存模型和线程
Java内存模型(Java Memory Model,JMM):用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。JMM是围绕原子性、有序性、可见性扩展的。
JMM包括:主内存、工作内存。
Java内存模型中定义了以下8中操作:
lock(锁定):作用于主内存的变量。 unlock(解锁):作用于主内存的变量。 read(读取):作用于主内存的变量。 load(载入):作用于工作内存的变量。 use(使用):作用于工作内存的变量。 assign(赋值):作用于工作内存的变量。 store(存储):作用于工作内存的变量。 write(写入):作用于主内存的变量。
线程的实现:
使用内核线程实现。1:1线程模型。1)基于内核实现,需要进行系统调用 2)支持轻量级进程数量有限
使用用户线程实现。1:N线程模型。1)不需要内核支援(优势) 2)所有线程操作都需要用户程序自己处理(劣势)
使用用户线程加轻量级线程混合实现。N:M线程模型,即多对多线程模型。
相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项: 1)等待可中断 2)可实现公平锁 3)锁可以绑定多个条件
公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
synchronized的锁是非公平锁。
ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。