转载:https://www.jianshu.com/p/ae97b692614e?from=timelinejava
JVM
是一种解释执行class
文件的规范技术。
算法
我翻译的中文图:编程
在JVM中负责装载
.class
文件(一种8位二进制流文件,各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙,经编译器编译.java
源文件后生成,每一个类(或者接口)都单独占有一个class
文件)。数组
当
JVM
使用类装载器定位class
文件,并将其输入到内存中时。会提取class
文件的类型信息,并将这些信息存储到方法区中。同时放入方法区中的还有该类型中的类静态变量。安全
java.io.FileOutputStream
java.io.OutputStream
public
、abstract
、final
)。java.io.Closeable
, java.io.Flushable
。ClassLoader
类的引用。Class
类的引用。JVM
对每一个装载的类型都会创建一个方法表,用于存储该类型对象能够调用的方法的直接引用,这些方法就包括从超类中继承来的。而这张表与Java动态绑定
机制的实现是密切相关的。常量池数据结构
常量池指的是在编译期被肯定,并被保存在已编译的
.class
文件中的一些数据。除了包含代码中所定义的各类基本数据类型和对象型(String及数组)的常量值(final,在编译时肯定,而且编译器会优化)还包含一些以文本形式出现的符号引用(类信息),好比:多线程
虚拟机必须给每一个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合
,包括直接常量(string、integer等)和其余类型,字段和方法的符号引用
。jvm
方法区是多线程共享的。也就是当虚拟机实例开始运行程序时,边运行边加载进class文件
。不一样的Class
文件都会提取出不一样类型信息存放在方法区中。一样,方法区中再也不须要运行的类型信息会被垃圾回收线程丢弃掉。函数
Java
程序在运行时建立的全部类型对象和数组都存储在堆中
。JVM
会根据new
指令在堆中开辟一个肯定类型的对象内存空间。可是堆中开辟对象的空间并无任何人工指令能够回收,而是经过JVM的垃圾回收器负责回收。优化
对于一个运行的Java而言,每个线程都有一个PC寄存器。当线程执行Java程序时,PC寄存器的内容老是下一条将被执行的指令地址。
每启动一个
线程
,JVM
都会为它分配一个Java栈
,用于存放方法中的局部变量,操做数以及异常数据等。当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧
。并将该栈帧压入Java栈
中,方法执行完毕时,JVM
会弹出该栈帧并释放掉。
注意:Java栈中的数据是线程私有的,一个线程是没法访问另外一个线程的Java栈
的数据。这也就是为何多线程编程时,两个相同线程执行同一方法时,对方法内的局部变量是不须要数据同步的缘由。
成员变量有默认值(被final修饰且没有static的必须显式赋值),局部变量不会自动赋值。
运行
Java
的每个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能经过解释或者使用芯片级指令直接执行字节码,或者间接经过JIT
(即时编译器)执行编译过的本地代码。
注意:JVM是进程级别,执行引擎是线程级别。
实际上,
class
文件中方法的字节码流就是有JVM
的指令序列构成的。每一条指令包含一个单字节的操做码,后面跟随0个或多个操做数。
指令由一个操做码
和零个或多个操做数
组成。
iload_0 // 把存储在局部变量区中索引为0的整数压入操做数栈。 iload_1 // 把存储在局部变量区中索引为1的整数压入操做数栈。 iadd // 从操做数栈中弹出两个整数相加,在将结果压入操做数栈。 istore_2 // 从操做数栈中弹出结果
很显然,上面的指令反复用到了Java栈
中的某一个方法栈帧
。实际上执行引擎运行Java字节码
指令不少时候都是在不停的操做Java栈
,也有的时候须要在堆中开辟对象以及运行系统的本地指令等。可是Java栈的操做要比堆中的操做要快的多,所以反复开辟对象是很是耗时的。这也是为何Java程序优化的时候,尽可能减小new对象。
示例分析:
//源代码 Test.java
package edu.hr.jvm; import edu.hr.jvm.bean; public class Test{ public static void main(String[] args){ Act act=new Act(); act.doMathForever(); } } //源代码 Act.java
package edu.hr.jvm.bean; public class Act{ public void doMathForever(){ int i=0; for(;;){ i+=1; i*=2; } } }
首先OS
会建立一个JVM实例
(进行必要的初始化工做,好比:初始启动类装载器
,初始运行时内存数据区
等。
而后经过自定义类装载器加载Test.class
。并提取Test.class字节码
中的信息存放在方法区 中(具体的信息在上面已经讲过)。上图展现了方法区中的Test类信息,其中在常量池中有一个符号引用“Act”
(类的全限定名,注意:这个引用目前尚未真正的类信息的内存地址)。
接着JVM
开始从Test
类的main
字节码处开始解释执行。在运行以前,会在Java栈中组建一个main方法的栈帧 ,如上图Java栈
所示。JVM须要运行任何方法前,经过在Java栈中压入一个帧栈。在这个帧栈的内存区域中进行计算。
如今能够开始执行main
方法的第一条指令 —— JVM
须要为常量池的第一项的类(符号引用Act
)分配内存空间。可是Act
类此时尚未加载进JVM
(由于常量池目前只有一个“Act”
的符号引用)。
JVM
加载进Act.class
,并提取Act类信息
放入方法区中。而后以一个直接指向方法区Act类信息
的直接引用(在栈中)换开始在常量池中的符号引用“Act”
,这个过程就是常量池解析
。之后就能够直接访问Act
的类信息了。
此时JVM
能够根据方法区中的Act类信息
,在堆中开辟一个Act类对象act。
接着开始执行main方法
中的第二条指令调用doMathForever
方法。这个能够经过堆中act对象
所指的方法表中查找,而后定位到方法区中的Act类信息
中的doMathForever
方法字节码
。在运行以前,仍然要组建一个doMathForever栈帧压入Java栈。(注意:JVM
会根据方法区中doMathForever的字节码
来建立栈帧的局部变量区
和操做数栈
的大小)
接下来JVM
开始解释运行Act.doMathForever字节码
的内容了。
编译:源码要运行,必须先转成二进制的机器码。这是编译器的任务。
.class
文件。Java
编译一个类时,若是这个类所依赖的类尚未被编译,编译器就会先编译这个被依赖的类,而后引用,不然直接引用。若是java
编译器在指定目录下找不到该类所其依赖的类的.class
文件或者.java
源文件的话,编译器话报“cant find symbol”
的错误。token(类名,成员变量名等等)
以及符号引用(方法引用,成员变量引用等等)
;方法字节码放的是类中各个方法的字节码。运行:
java
类运行的过程大概可分为两个过程:类的加载,类的执行。须要说明的是:JVM
主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM
并非在一开始就把一个程序就全部的类都加载到内存中,而是到不得不用的时候才把它加载进来,并且只加载一次。
下面是程序运行的详细步骤:
//MainApp.java
public class MainApp { public static void main(String[] args) { Animal animal = new Animal("Puppy"); animal.printName(); } } //Animal.java
public class Animal { public String name; public Animal(String name) { this.name = name; } public void printName() { System.out.println("Animal ["+name+"]"); } }
java
程序获得MainApp.class
文件后,在命令行上敲java AppMain
。系统就会启动一个jvm进程
,jvm进程
从classpath
路径中找到一个名为AppMain.class
的二进制文件,将MainApp
的类信息加载到运行时数据区的方法区内,这个过程叫作MainApp类的加载
。JVM
找到AppMain
的主函数入口,开始执行main函数
。main
函数的第一条命令是Animal animal = new Animal("Puppy");
就是让JVM
建立一个Animal
对象,可是这时候方法区中没有Animal
类的信息,因此JVM
立刻加载Animal
类,把Animal
类的类型信息放到方法区中。Animal
类以后,Java
虚拟机作的第一件事情就是在堆区中为一个新的Animal
实例分配内存,而后调用构造函数初始化Animal实例
,这个Animal实例
持有着指向方法区的Animal类的类型信息
(其中包含有方法表,java动态绑定
的底层实现)的引用。animal.printName()
的时候,JVM
根据animal引用
找到Animal
对象,而后根据Animal对象
持有的引用定位到方法区中Animal类
的类型信息的方法表,得到printName()
函数的字节码的地址。printName()
函数的字节码
(能够把字节码理解为一条条的指令)。特别说明:java类中全部public和protected的实例方法都采用动态绑定机制,全部私有方法
、静态方法
、构造器
及初始化方法<clinit>
都是采用静态绑定机制
。而使用动态绑定机制的时候会用到方法表,静态绑定时并不会用到。
经过前面的两个例子的分析,应该理解了很多了吧。
JVM
主要包含三大核心部分:类加载器,运行时数据区和执行引擎。
虚拟机将描述类的数据从class文件加载到内存,并对数据进行校验,准备,解析和初始化,最终就会造成能够被虚拟机使用的java类型,这就是一个虚拟机的类加载机制。java在类中的类是动态加载的,只有在运行期间使用到该类的时候,才会将该类加载到内存中,java依赖于运行期动态加载和动态连接来实现类的动态使用。
一个类的生命周期:
加载,验证,准备,初始化和卸载在开始的顺序上是固定的,可是能够交叉进行。
在Java中,对于类有且仅有四种状况会对类进行“初始化”。
new
关键字实例化对象的时候,读取或设置一个类的静态字段时候(除final
修饰的static
外),调用类的静态方法时候,都只会初始化该静态字段或者静态方法所定义的类。reflect包
对类进行反射调用的时候,若是类没有进行初始化,则先要初始化该类。main方法的主类
。注意:
加载
加载阶段主要完成三件事,即经过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构,在
Java堆
中生成一个表明此类的Class对象
,做为访问方法区这些数据的入口。这个加载过程主要就是靠类加载器实现的,这个过程能够由用户自定义类的加载过程。
验证
这个阶段目的在于确保才class
文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。
主要包括四种验证:
文件格式验证
:基于字节流验证,验证字节流是否符合Class文件格式的规范
,而且能被当前虚拟机处理。元数据验证
:基于方法区的存储结构验证,对字节码描述信息进行语义验证。字节码验证
:基于方法区的存储结构验证,进行数据流和控制流的验证。符号引用验证
:基于方法区的存储结构验证,发生在解析中,是否能够将符号引用成功解析为直接引用。准备
仅仅为类变量(即static修饰的字段变量)分配内存而且设置该类变量的初始值即零值,这里不包含用final修饰的static,由于final
在编译的时候就会分配了(编译器的优化),同时这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量是会随着对象一块儿分配到Java
堆中。
解析
解析主要就是将常量池中的符号引用替换为直接引用的过程。符号引用
就是一组符号来描述目标,能够是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。
初始化
初始化阶段依旧是初始化类变量和其余资源,这里将执行用户的static字段和静态语句块的赋值操做。这个过程就是执行类构造器< clinit >方法的过程。
< clinit >方法是由编译器收集类中全部类变量的赋值动做和静态语句块的语句生成的,类构造器< clinit >方法与实例构造器< init >方法不一样,这里面不用显示的调用父类的< clinit >方法,父类的< clinit >方法会自动先执行于子类的< clinit >方法。即父类定义的静态语句块和静态字段都要优先子类的变量赋值操做。
类加载器的分类
启动类加载器(Bootstrap ClassLoader)
:主要负责加载<JAVA_HOME>\lib目录
中的'.'
或是-Xbootclasspath
参数指定的路径中的,而且能够被虚拟机识别(仅仅按照文件名识别的)的类库到虚拟机内存中。它加载的是System.getProperty("sun.boot.class.path")
所指定的路径
或jar
。扩展类加载器(Extension ClassLoader)
:主要负责加载<JAVA_HOME>\lib\ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的全部类库。它加载的是System.getProperty("java.ext.dirs")
所指定的路径或jar
。应用程序类加载器(Application ClassLoader)
:也叫系统类加载器,主要负责加载ClassPath路径
上的类库,若是应用程序没有自定义本身类加载器,则这个就是默认的类加载器。它加载的是System.getProperty("java.class.path")
所指定的路径
或jar
。类加载器的特色
Application Loader
(系统类加载器)开始加载指定的类。Bootstrap Loader
(启动类加载器)是最顶级的类加载器了,其父加载器为null
。类加载器的双亲委派模型
类加载器双亲委派模型的工做过程是:若是一个类加载器收到一个类加载的请求,它首先将这个请求委派给父类加载器去完成,每个层次类加载器都是如此,则全部的类加载请求都会传送到顶层的启动类加载器,只有父加载器没法完成这个加载请求(即它的搜索范围中没有找到所要的类),子类才尝试加载。
使用双亲委派模型主要是两个缘由:
Java核心API
,则就会带来安全隐患。下面是一个类加载器双亲委派模型,这里各个类加载器并非继承关系,它们利用组合实现的父类与子类关系。
类加载的几种方式
JVM
初始化加载,加载含有main
的主类。Class.forName("Hello")
方法动态加载类,默认会执行初始化块,这是由于Class.forName("Hello")
其实就是Class.forName("Hello",true,CALLCLASS.getClassLoader())
,第二个参数就是类加载过程当中的链接操做。若是指定了ClassLoader
,则不会执行初始化块。ClassLoader.loadClass("Hello")
方法动态加载类,不会执行初始化块,由于loadClass
方法有两个参数,用户只是用第一个参数,第二个参数默认为false
,即不对该类进行解析则就不会初始化。类加载实例
当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件)
,JVM会将HelloWorld.class
加载到内存中,并在堆中造成一个Class的对象HelloWorld.class
。
基本的加载流程以下:
jre目录
,寻找jvm.dll
,并初始化JVM
;Bootstrap Loader
(启动类加载器);Bootstrap Loader
,该加载器会加载它指定路径下的Java核心API
,而且再自动加载Extended Loader
(标准扩展类加载器),Extended Loader
会加载指定路径下的扩展JavaAPI
,并将其父Loader
设为BootstrapLoader
。Bootstrap Loader
也会同时自动加载AppClass Loader
(系统类加载器),并将其父Loader
设为ExtendedLoader
。AppClass Loader
加载CLASSPATH
目录下定义的类,HelloWorld类
。在Java
应用开发过程当中,可能会须要建立应用本身的类加载器。典型的场景包括实现特定的Java字节代码
查找方式、对字节代码进行加密/解密以及实现同名Java类的隔离等。建立本身的类加载器并非一件复杂的事情,只须要继承自java.lang.ClassLoader
类并覆写对应的方法便可。 java.lang.ClassLoader
中提供的方法有很多,下面介绍几个建立类加载器时须要考虑的:
defineClass()
:这个方法用来完成从Java字节码
的字节数组到java.lang.Class
的转换。这个方法是不能被覆写的,通常是用原生代码来实现的。findLoadedClass()
:这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。findClass()
:这个方法用来根据名称查找并加载Java类
。loadClass()
:这个方法用来根据名称加载Java类
。resolveClass()
:这个方法用来连接一个Java类
。这里比较 容易混淆的是findClass()
方法和loadClass()
方法的做用。前面提到过,在Java类
的连接过程当中,会须要对Java类
进行解析,而解析可能会致使当前Java类
所引用的其它Java类
被加载。在这个时候,JVM
就是经过调用当前类的定义类加载器的loadClass()
方法来加载其它类的。findClass()方法
则是应用建立的类加载器的扩展点。应用本身的类加载器应该覆写findClass()方法
来添加自定义的类加载逻辑。 loadClass()方法
的默认实现会负责调用findClass()方法
。
前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是封装在loadClass()方法
中的。若是但愿修改此策略,就须要覆写loadClass()方法
。
下面的代码给出了自定义的类加载的常见实现模式:
public class MyClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] b = null; //查找或生成Java类的字节代码 return defineClass(name, b, 0, b.length); } }
分代收集
新生代
(Young Generation)
Eden
空间(Eden space,任何实例都经过Eden空间进入运行时内存区域)S0 Survivor
空间(S0 Survivor space,存在时间长的实例将会从Eden空间移动到S0 Survivor空间)S1 Survivor
空间 (存在时间更长的实例将会从S0 Survivor空间移动到S1 Survivor空间)
老年代
(Old Generation)实例将从S1提高到Tenured(终身代)
永久代
(Permanent Generation)包含类、方法等细节的元信息
永久代空间在Java SE8
特性中已经被移除。
年轻代:使用标记复制清理算法
,解决内存碎片问题。由于在年轻代会有大量的内存须要回收,GC
比较频繁。经过这种方式来处理内存碎片化,而后在老年代中经过标记清理算法
来回收内存,由于在老年代须要被回收的内存比较少,提升效率。
Eden 区:当一个实例被建立了,首先会被存储在堆内存年轻代的 Eden
区中。
Survivor 区(S0 和 S1):做为年轻代 GC(Minor GC)
周期的一部分,存活的对象(仍然被引用的)从 Eden
区被移动到 Survivor
区的 S0
中。相似的,垃圾回收器会扫描 S0
而后将存活的实例移动到 S1
中。总会有一个空的survivor区
。
老年代: 老年代(Old or tenured generation
)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC
周期时(对象年龄计数器),在 S1 Survivor
区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。老年代是实例生命周期的最后阶段。Major GC
扫描老年代的垃圾回收过程。若是实例再也不被引用,那么它们会被标记为回收,不然它们会继续留在老年代中。
内存碎片:一旦实例从堆内存中被删除,其位置就会变空而且可用于将来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,须要进行碎片整理。基于垃圾回收器的不一样选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成。
Java语言规范没有明确地说明JVM使用哪一种垃圾回收算法,可是任何一种垃圾收集算法通常要作2件基本的事情:
- 发现无用信息对象
- 回收被无用对象占用的内存空间,使该空间可被程序再次使用。
GC Roots
根集就是正在执行的Java程序能够访问的引用变量的集合
(包括局部变量、参数、类变量)
GC Roots的对象包括
**可达性算法分析 **
经过一系列称为”GC Roots”的对象做为起点,从这些节点开始向下搜索,搜索全部走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到此对象不可达),则证实此对象是不可用的,应该被回收。
根搜索算法:计算可达性,如图:
引用计数法
引用计数法是惟一没有使用根集(GC Roots)的垃圾回收的法,该算法使用引用计数器来区分存活对象和再也不使用的对象。堆中的每一个对象对应一个引用计数器。当每一次建立一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1,当对象出了做用域后(该对象丢弃再也不使用),引用计数器减1,一旦引用计数器为0,对象就知足了垃圾收集的条件。
惟一没有使用根可达性算法
的垃圾回收算法。
缺陷:不能解决循环引用的回收。
tracing算法(tracing collector)
tracing算法
是为了解决引用计数法的问题
而提出,它使用了根集(GC Roots)
概念。垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每一个可达对象设置一个或多个位。在扫描识别过程当中,基于tracing算法
的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器
。
compacting算法(Compacting Collector)
为了
解决堆碎片问题
,在清除的过程当中,算法将全部的对象移到堆的一端,堆的另外一端就变成了一个相邻的空闲内存区,收集器会对它移动的全部对象的全部引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,通常增长句柄和句柄表。
copying算法(Coping Collector)
该算法的提出是为了克服
句柄的开销和解决堆碎片
的垃圾回收。它开始时把堆分红 一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾收集就从根集中扫描活动对象,并将每一个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
generation算法(Generational Collector) :如今的java内存分区
stop-and-copy垃圾收集器
的一个缺陷是收集器必须复制全部的活动对象,这增长了程序等待时间,这是coping算法低效
的缘由。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。所以,generation算法将堆分红两个或多个,每一个子堆做为对象的一代 (generation)。因为多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,因为老一代的子堆不会常常被回收,于是节省了时间。
adaptive算法(Adaptive Collector)
在特定的状况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用状况,并将选择适当算法的垃圾收集器