基本概念:JVM是可运行Java代码的假象计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方法域。JVM是运行在操作系统之上的它与硬件没有直接的交互。
Java代码的执行:
当一个程序开始运行,虚拟机就实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭虚拟机就会消亡,多个虚拟机之间数据不能共享。
JVM允许一个应用并发执行多个线程,每个线程都是虚拟机执行过程中的一个线程实体。Hotspot JVM中的Java线程与原生操作系统有直接的映射关系。
操作系统负责调度所有线程,并把它们分配到任何可以使用的cpu上。当原生线程初始化完毕,就会调用Java run()方法。当线程结束时,就会释放原生线程和Java线程的所有资源。
Hotspot后台运行线程
线程 | 说明 |
---|---|
虚拟机线程(VM thread) | 这个线程等待JVM到达安全点操作出现。这些操作必须要在独立的线程中执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。这些操作有:stop-the-world垃圾回收,线程栈dump,线程暂停,线程偏向锁(biased blocking)解除。 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的进行。 |
GC线程 | 这些线程支持JVM中不同的垃圾回收活动。 |
编译期线程 | 这些线程在JVM运行期间,将字节码动态编译为平台相关的机器码。 |
信号分发线程 | 这个线程在接收发送到JVM的信号并调用适当的JVM方法处理。 |
JVM中线程主要分为线程私有区域【程序计数器,虚拟机栈,本地方法区】,线程共享区【JAVA堆,方法区】,直接内存。
直接内存不是Java运行时数据区的一部分不归JVM管理,在NIO中存在DirectByteBuffer可以使用堆外内存,避免了从内核空间(Native堆)到用户空间(Java堆)的频繁复制
程序计数器(线程私有)
是一块较小的内存区域,是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型中,字节码解释器的工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器完成。(参考计算机中的pc指针)
由于Jaava虚拟机的多线程是通过多线程轮流切换,分配处理器执行时间的方式来实现,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换的正确进行,每条线程都需要一个独立的程序计数器,每个计数器之间互不影响,独立储存(这类内存区域被称为线程私有的区域)
如果正在执行Java方法,计数器记录的是当前的指令地址(虚拟机字节码指令地址),如果是Native方法则为空。
程序计数器是唯一一个没有内存溢出错误(OutOfMemoryError)的区域
虚拟机栈(线程私有)
虚拟机栈也是线程私有的,她的生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
栈帧(Stack Frame):用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking),方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁(抛出异常也算结束)。
虚拟机栈有两种异常情况:
当线程请求深度大于虚拟机所允许深度,抛出:StackOverflowError异常
如果Java虚拟机栈容量可以动态扩展,当栈无法申请到足够的内存会抛出OutofMemoryError异常
HotSpot 虚拟机的栈容量是不可以动态扩展的,以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然会出现OOM异常)
本地方法区(线程私有)
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和 Java 虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowEror(栈溢出) 和 OutOfMemoryEror(堆溢出) 异常。
Hotspot虚拟机的栈容量是不允许动态扩展的(一个线程的虚拟机栈申请到的空间是多少就是固定的),以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常——只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然会出现OOM异常)
堆(Heap-线程共享)-运行时数据区
对于Java应用程序来说,Java堆是虚拟机管理的内存中最大的一块。
Java堆(Java Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。
Java堆(Java Heap)内存的唯一目的就是存放对象实例,Java中几乎所有对象实例都在这里分配内存。(创建的对象和数组都保存在Java堆内存中)
("所有对象实例以及数组都应当在堆上分配")但随着即使编译技术和逃逸技术分析的日渐强大,栈上分配,标量替换优化手段已经导致一些微妙的变化悄然发生,现在Java对象都在栈上分配已经不那么绝对了。
Java堆是垃圾收集器管理的内存区域(是垃圾收集器进行垃圾收集的最重要的内存区域),因此一些资料中称Java堆(Java Heap)为GC堆(Garbage Collected Heap)
现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为新生代(Eden区,From Survivor区和To Survivor区)和老年代。
(Thread Local Allocation Buffer,TLAB)缓存区:从内存分配的角度看,所有的线程共享的Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。不过不管如何划分Java堆中存储的都只能是对象的实例,将Java Heap细分的目的只是为了更好的回收内存或者分配内存。
Java堆可以处于物理上的不连续内存空间中,但在逻辑上应该被视为连续的(抽象出来就是俩数据结构)。
Java堆可以被实现成固定大小的,也可以是可扩展的(参数-Xmx (memory max最大可分配),-Xms (memory start启动时内存)设定内存大小)。当内存不够也无法扩展时会抛出OutOfMemoryError异常。
运行时数据区的GC
作为垃圾回收器管理的区域,Java堆从GC的角度可以分为新生代(Eden区,From Survivor区和To Survivor区)和老年代。
新生代:是用来存放新生的对象,一般占据堆的1/3空间。用于频繁创建对象,所以新生代会频繁触发MinorGC进行回收。新生代又分为Eden区,ServivorFrom、ServivorTo三个区
Eden区:Java新对象的出身地若(若新创建的对象占用内存很大,直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代进行一次垃圾回收。
SurvivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。
SurvivorTo:保留了一次MirrorGC过程中的幸存者。
MirrorGC:采用复制算法(复制->清空->互换)
Eden,SurvivroFrom复制到SurvivorTo,年龄+1:首先吧Eden和Survivor中的存活的对象复制到SurvivorTo区域(如果有对象的年龄达到了老年代的标准则复制到老年代),同时吧这些对象年龄+1(若Survivor不够位置就放到老年代区)
清空Eden,SurvivorFrom中的对象。
最后将SurvivorTo和SurvivorFrom互换,源ServivorTo中的对象称为下一次GC时的ServivorFrom区
老年代:主要存放应用程序中生命周期长的内存对象。
永久代(一种方法区的实现方式):主要存放Class和Meta(元数据)的信息
方法区/永久代(线程共享)
基本信息:
方法区/元空间(线程共享)
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区中的一部分(jdk1.8之后方法区实现由永久代变为元空间 Metaspace),在加载类和接口到虚拟机后就会创建运行时常量池。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量(static final),也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
Class的常量池将在类加载存放到方法区的运行时常量池中。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
方法区结构
虚拟机加载类会在方法区保存已加载的类信息(不是Class对象,Class对象保存在堆中是加载的最终产品,方法区的类信息中保存一个Class对象和ClassLoader的引用),类信息存放形式根据不同虚拟机不同实现。
类信息:
类型信息:
类型的常量池:
- 为什么需要常量池?
- 一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。
- 常量池表(Constant Pool Table)是Class字节码文件的一部分,
字段信息(实例域中的字段信息):
方法信息:
类变量(static静态变量,静态域中的字段信息)
静态域非final变量随着类的加载而加载,他们成为类数据在逻辑上的一部分。
JDK1.7 字符串常量池在堆,运行时常量池在方法区(永久代) 。
JDK1.8 字符串常量池在堆,运行时常量池在方法区(元空间)。
被声明为final的类变量在编译的时候就会被分配了,被保存在类的常量池中,在加载类的时候复制进运行时常量池中,每一个使用它的类保存着一个对其的引用(复用)。
对类加载器的引用
对Class类的引用
方法表
字符串常量池 StringTable 为什么要调整位置?
垃圾回收之前应该搞清楚三点:
在Java内存模型中,程序计数器、虚拟机栈、本地房发栈随线程的而生,随线程而亡,栈中的栈帧随着方法的进入和退出有条不紊的进行着入栈和出栈。因此这几个线程私有的区域的内存分配和回收都具备确定性。当方法退出时内存就随着回收了。
而Java堆和方法区这两个区域具有着很显著的不确定性:同一个接口的多个实现类可能需要内存不同,只有处于运行期间我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存回收和管理是动态的。
如何确定垃圾
引用计数法
可达性分析
为了解决循环引用的问题,Java使用了可达性分析的算法。通过一系列的“GC roots”对象作为起点搜索。(如果在“GC roots”和一个对象之间没有可达路径(引用链),则称对象是不可达的)。
GC roots:
标记清除算法
最基础的垃圾回收算法,分为两个阶段:标记和清除(在标记期间标记出需要回收的对象,在清除期间清除需要回收的对象)
存在内存碎片化严重的问题(标记出的需要回收的内存是零散的,可能导致大对象找不到可利用连续空间的情况)
复制算法
标记整理算法
先将内存中需要回收的内存进行标记,然后将存活的对象移动到内存的一端。
移动活动对象,特别是老年代有大量对象存活的区域是一种负担极大的操作。
分代收集算法
目前大部分JVM都采用分代收集算法,根据对象的存活不同生命周期将内存划为不同的域(如:老年代,新生代)。老年代每次回收的对象较少,而新生代回收的对象较多,所以两个域可以使用不同的垃圾回收算法。
GC分代收集算法VS分区收集算法
强引用
软引用
弱引用
虚引用
Java内存被划分为了新生代和老年代两部分,新生代主要使用复制标记算法,老年代主要使用复制整理算法。所以,Java为这两个块提供了许多不同的垃圾收集器。
Serial收集器
Serial是一个单线程收集器,他不但只使用一个线程去完成垃圾回收工作,而且必须暂停所有其他线程直到垃圾回收结束。
Serial Old新生代使用单线程复制算法,老年代使用单线程标记整理算法。
ParNew收集器
Serial的多线程版本,也是用复制算法,除了多线程以外和之前的Serial收集器完全一样。
Parallel Scavenge收集器
Parallel Scavenge的关注点是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间),同时Parallel Scavenge具有自适应调节策略。
Parallel Old的老版本中新生代采用多线程复制算法,老年代采用多线程标记整理算法。
CMS收集器
CMS整个过程分为四个阶段
初始标记
并发标记
重新标记
并发清除
CMS收集器特点:
尽可能降低停顿
会影响系统整体吞吐量和性能:比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够)
CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。
G1收集器
基于标记整理算法,不产生内存碎片。
可以预测停顿时间,在不牺牲吞吐量的情况下,实现低停顿垃圾回收(局部回收)。
G1收集器开创了面向局部收集的设计思路和基于Region的内存布局形式。
G1不再坚持固定大小以及固定数量的的分代内存区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden,Survivor空间,或者老年代空间。这样新生代和老生代就成为了一系列的区域的动态集合
Humongous区域:专门用于存放大对象,,G1认为只要大小超过了Region容量的一半大小就会被认为是大对象。
G1的垃圾回收每次回收的都是Region大小的整数倍(这样就使得停顿时间可以预测),同时会对跟踪各个Region中垃圾堆的价值大小并维护一个优先级表。优先回收最有价值的区域(最垃圾的区域)。
G1将完整的堆局部化为了一个一个的小堆(或许能和缓冲区关联起来),每个小堆都可以扮演一个域角色(域不再固定,而是动态的局部化的)
Region 区域划分和优先级区域回收策略确保G1收集器可以在有限时间内获得最高的垃圾回收集效率。
一个类型从被加载到虚拟机内存中开始,到卸载为止,她的整个生命周期将会经历加载,验证,准备,解析,初始化,使用,卸载七个阶段。
其中加载,验证,准备,初始化,卸载的顺序是确定的。
Java虚拟机规范中规定了只有6中情况必须立即对类进行初始化(此前已经加载验证准备过了)。
加载(Loading)
在类加载过程的加载阶段,Java虚拟机需要完成以下三件事情:
对于数组类型的加载创建需要遵循以下过程:
加载结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中(方法区数据存储格式依据JVM不同而不同),类型数据存放在方法区之后,会在Java堆内存中实例化一个对应的Class对象,这个对象将作为程序访问方法区中的类型数据的外部接口。
验证(Verification)
这一阶段的目的是确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并不会危害虚拟机的自身安全。
文件格式验证
元数据验证
字节码验证
符号引用验证
...
准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段(静态变量),即在方法区中分配这些变量所使用的内存空间。
JDK1.7时将static区移动到了Java Heap中
而静态常量(final)在编译阶段会生成ConstantValue属性,在准备阶段会根据ConstantValue属性将v赋值为8080。
解析(Resolution)
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用:CONSTANT_Class_info,CONSTANT_Filed_info,CONSTANT_Method_info等类型的常量。
直接引用:
除invokeddynamic指令外,虚拟机可以实现对第一次解析结果进行缓存(如运行时直接引用常量池中的记录),并把常量标识为已解析状态。
初始化(Initialization)
初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器之外,其他操作都由JVM主导,到了初始阶段才真正开始执行类中定义的Java程序代码。
Java虚拟机将加载动作放到JVM外部实现,让程序员可以自己决定如何获取所需的类(通过类加载器实现)。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。
对于两个类只要类加载器不同那么久不相等
启动类加载器/引导类加载器
负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可的(按文件名识别,如rt.jar)的类。
扩展类加载器(JDK9中被平台类加载器所取代)
负责加载JAVA_HOME\lib\ext目录中的,或者通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器/系统类加载器
负责加载用户路径(classpath)上的类库。
JVM通过双亲委派模型进行类的加载,当然可以可以通过java.lang.ClassLoader实现自定义的类加载器。
双亲委派模型
从Java虚拟机角度来看,只存在两种不同的类加载器:一种是启动类加载器(由C++实现,是虚拟机的一部分),一种是其他加载器(由Java实现,独立于虚拟机之外,并全部继承自java.lang.ClassLoader)。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己尝试加载这个类,而是把这个请求委派给父类去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中。只有当父加载器发现自己无法完成这个请求的时候(在她的加载路径之下没有发现要加载的Class文件),子类才会尝试去加载。
双亲委派模型的实现:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); //判断请求的类是否已经被加载过了 if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //若父加载器加载路径下没有目标类(抛出ClassNotFoundException) // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { //若父类无法加载 // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); //子类加载器自身的findClass // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); //增加寻找到得类数量 } } if (resolve) { resolveClass(c); } return c; } }
OSGi(Open Service Gateway Initiative):动态模型系统(使得Java应用程序能像电脑外设一样随意更换,即插即用等),是面向Java的动态模型系统,是Java动态模块化系统的一系列规范。
动态改变构造
模块化编程与热插拔