理解JVM工作原理总结

时间:2022-12-28 10:17:51

说明:博文是对一些博友的总结

先看一下,Java文件编译和执行全程

 理解JVM工作原理总结

JVM的抽象架构

 理解JVM工作原理总结

一、JVM的生命周期这里jvmsun HotSpot)

1. JVM实例对应了一个独立运行的java程序它是进程级别

a) 启动启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点

b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程

c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出

2. JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的

二、JVM的体系结构

1. 类装载器(ClassLoader(用来装载.class文件)

  备注:Classloader 类加载器,用来加载 Java 类到 Java 虚拟机中。与普通程序不同的是。Java程序(class文件)并不是本地的可执行程序。当运行Java程序时,首先运行JVMJava虚拟机),然后Java class加载到JVM头运行,负责加载Java class的这部分就叫做Class Loader

 

2.运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)

 

3.执行引擎执行字节码,或者执行本地方法),jvmcpu,不断地取指令,JIT编译翻译,执行

 

三、JVM类加载器

JVM整个类加载过程的步骤:

 

1. 装载

    装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+

包名+ClassLoader实例ID

 

2. 链接

    链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。

完成校验后,JVM初始化类中的静态变量,并将其值赋为默认值。

最后对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如publicprivate域权限等),会造成NoSuchMethodErrorNoSuchFieldError等错误信息。

 

3. 初始化

    初始化过程即为执行类中的静态初始化代码构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:

调用了new

反射调用了类中的方法;

子类调用了初始化;

JVM启动过程中指定的初始化类。


JVM类加载顺序:

  JVM两种类装载器包括:启动类装载器用户自定义类装载器

启动类装载器是JVM实现的一部分

用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类

 

JVM装载顺序:

    Jvm启动时,由BootstrapUser-Defined方向加载类;

    应用进行ClassLoader时,由User-DefinedBootstrap方向查找并加载类;

 

1. Bootstrap ClassLoader

    这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOMEjre/lib/rt.jarSun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。这个类装载器是在JVM启动的时候创建的。它负责装载Java API,包含Object对象。和其他的类装载器不同的地方在于这个装载器是通过native code来实现的,而不是用Java代码。


2. Extension ClassLoader

    它装载除了基本的Java API以外的扩展类。它也负责装载其他的安全扩展功能。

 

3. System ClassLoader

   JVM用此classloader加载启动参数中指定的Classpath中的jar以及目录,在Sun JDKClassLoader对应的类名为AppClassLoader

 

4. User-Defined ClassLoader

User-DefinedClassLoaderJava开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。

备注:类加载器的工作原理基于三个机制:委托、可见性和单一性。

委托机制:

    当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求由回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找。记住classpath定义的是类文件的加载目录,而PATH是定义的是可执行程序如javacjava等的执行路径。

 

可见性机制:

    根据可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行。所以下面的例子中,当Abc.class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。

 

单一性机制:

    根据这个机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。你写自己的类加载器的时候应该严格遵守这三条机制。

 理解JVM工作原理总结

ClassLoader抽象类的几个关键方法:

1loadClass

    此方法负责加载指定名字的类ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法

 

2findLoadedClass

    此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法

 

3findClass

   此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。

 

4findSystemClass

    此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null

 

5defineClass

    此方法负责将二进制的字节码转换Class对象

 

6resolveClass

   此方法负责完成Class对象的链接,如已链接过,则会直接返回。

 

四、JVM执行引擎 

在执行方法时JVM提供了四种指令来执行:

1invokestatic调用类的static方法

2invokevirtual调用对象实例的方法

3invokeinterface将属性定义为接口来进行调用

4invokespecialJVM对于初始化对象(Java构造器的方法为:<init>)以及调用对象实例中的私有方法时。

 

主要的执行技术有:

 解释,即时编译,自适应优化、芯片级直接执行

1)解释属于第一代JVM

2)即时编译JIT属于第二代JVMJust-In-Time Compiler,即时编译器Java字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。

3自适应优化(目前SunHotspotJVM采用这种技术)则吸取第一代JVM和第二代

 

JVM的经验,采用两者结合的方式

   开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。

 

五、JVM运行时数据区

   Runtime Data Areas当运行一个JVM示例时,系统将分配给它一块内存区域(这块内存区域的大小可以设置的),这一内存区域由JVM自己来管理。从这一块内存中分出一块用来存储一些运行数据,例如创建的对象,传递给方法的参数,局部变量,返回值等等。分出来的这一块就称为运行数据区域。

   运行数据区域可以划分为6大块Java栈、程序计数寄存器(PC寄存器)、本地方法栈(Native Method Stack)、Java堆、方法区域、运行常量池(Runtime Constant Pool)。运行常量池本应该属于方法区,但是由于其重要性,JVM规范将其独立出来说明。其中,前面3各区域(PC寄存器、Java栈、本地方法栈)是每个线程独自拥有的,后三者则是整个JVM实例中的所有线程共有的。这六大块如下图所示:

 理解JVM工作原理总结

 

第一块:PC寄存器

    PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。

 

第二块:JVM

    JVM栈是线程私有的,每个线程创建的同时都会创建JVMJVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:booleancharbyteshortintlongfloatdouble)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址

 

第三块:堆(Heap

    它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收

 

1)堆是JVM所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的

 

2Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLABThread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配

 

3TLAB仅作用于新生代的Eden SpaceJVM GC调优一则增大Eden Space提高性能),因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

 

第四块:方法区域(Method Area

1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。

 

2方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getNameisInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

 

第五块:运行时常量池(Runtime Constant Pool

   存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

 

第六块:本地方法堆栈(Native Method Stacks

  JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

 

六、JVM垃圾回收

 

1.GC的基本原理:

   将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

1)对新生代的对象的收集称为minor GC;

2)对旧生代的对象的收集称为Full GC;

3)程序中主动调用System.gc()强制执行的GCFull GC

 

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

 

1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)

2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC

3)弱引用:在GC时一定会被GC回收

(4)虚引用:由于虚引用只是用来得知对象是否被GC

 

 导致Gc的情况:
1、tenured被写满
2、perm被写满
3、System.gc()的显式调用。
4、上一次GC之后heap的各域分配策略动态变化。

 

2.Java GC基本算法

1、引用计数(reference counting)
    原理:此对象有一个引用,则+1;删除一个引用,则-1。只用收集计数为0的对象。
    缺点:无法处理循环引用的问题。如:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。 

2、复制(copying)
    原理:把内存空间划分为2个相等的区域,每次只使用一个区域。垃圾回收时,遍历当前使用区域,把正在使用的对象复制到另外一个区域。
    优点:不会出现碎片问题。
    缺点:1、暂停整个应用。2、需要2倍的内存空间。

3、标记-清扫(Mark-and-sweep)---sun前期版本就是用这个技术。
    原理:对于“活”的对象,一定可以追溯到其存活在堆栈、静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。第一阶段:从GC roots开始遍历所有的引用,对有活的对象进行标记。第二阶段:对堆进行遍历,把未标记的对象进行清除。这个解决了循环引用的问题。
    缺点:1、暂停整个应用;2、会产生内存碎片。

4、标记-压缩(Mark-Compact)自适应
    原理:第一阶段标记活的对象,第二阶段把为标记的对象压缩到堆的其中一块,按顺序放。
    优点:1、避免标记扫描的碎片问题;2、避免停止复制的空间问题。
    
    具体使用什么方法GC,Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-扫描”方式;同样,Java虚拟机会跟踪“标记-扫描”的效果,要是堆空间碎片出现很多碎片,就会切换回“停止-复制”模式。这就是自适应的技术。

5、分代(generational collecting)-----J2SE1.2以后使用此算法(Sun hotSpot或sun jvm)
    原理:基于对象生命周期分析得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同的生命周期使用不同的算法(2-3方法中的一个即4自适应)进行回收。

  Thinking in java给java gc取了一个罗嗦的称呼:“自适应、分代的、停止-复制、标记-扫描”式的垃圾回收器。

 

下面就详细讲讲5这个算法:

   首先,内存管理和垃圾回收是JVM非常关键的点,对Java性能的剖析而言,了解内存管理和垃圾回收的基本策略非常重要。本篇对Sun JVM 6.0的内存管理和垃圾回收做大概的描述。

(一)内存管理
      在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测。为解决这种矛盾,Sun JVM的内存管理采用分代的策略。

 

 SUN Jvm内存区域总体分两类,heap区和非heap区。

heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区)。

非heap区又分:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。

 

jvm内存池各区的作用:

Eden Space (heap):内存最初从这个线程池分配给大部分对象。

Survivor Space (heap):用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。

Tenured Generation (heap):用于保持已经在survivor space内存池中存在了一段时间的对象。

Permanent Generation (non-heap):保存虚拟机自己的静态(reflective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的。

Code Cache (non-heap):HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)。

 

分区的目的:

   新生区由于对象产生的比较多并且大都是朝生夕灭的,只有少量存活,所以采用采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而生命力很强,对象存活率高、没有额外的空间对它进行分配担保,采用标记-清理算法或者标记-清整理法进行回收。

   非heap区域中Perm Gen中放着类、方法的定义,jvm Stack区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。

 理解JVM工作原理总结


     1.年轻代(Young Gen):(复制算法)

   年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。

  (1当对象在堆创建时,将进入年轻代的Eden Space。

  (2垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制 Old Gen。

  (3扫描A Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。

  (4扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivor Space,反复交换扫描,默认值是31,即长期存活的对象在AB两空间被复制31后,被复制到Old Gen

    若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作

      我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保不存在内存碎片采用空间换时间的方式来加速内存垃圾回收


     2.年老代(Tenured Gen):(标记-整理或压缩算法)

年老代主要存放JVM认为比较old的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边),当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。


     3.持久代(Perm Gen):

持久代主要存放类定义字节码常量等很少会变更的信息,关于这块的垃圾回收策略可以参考另一篇BLOG http://ayufox.iteye.com/blog/646125。持久代大小通过-XX:MaxPermSize=N进行设置。


      Class data sharing (CDS)( http://java.sun.com/j2se/1.5.0/docs/guide/vm/class-data-sharing.html)是JDK5新引入的特性,采用在虚拟机之间共享一些class定义信息(bootstrapClassLoader加载的类)的方式提速JVM的启动和内存的占用,主要用于客户端,如果需要对类进行instrutment,最好把CDS关闭。(默认情况下,JVM的server模式会关闭CDS,client模式会开启CDS)

-Xshare:off
Disable class data sharing.
-Xshare:on
Require class data sharing to be enabled. If it could not be enabled for various reasons, print an error message and exit.
-Xshare:auto
The default; enable class data sharing whenever possible.

       我们通过JConsole截图看看上面这几个区的显示(下图),从左到右分别是EdenSpace、A Suvivor Space、Tenured Gen、Code Cache、Perm Gen(shared-wr)、Perm Gen(shared-ro)、Perm Gen
理解JVM工作原理总结

(二)垃圾回收策略

 1.评估垃圾回收策略的两个重要度量是:

 (1)吞吐量(Throughput ):JVM花费在垃圾回收上的时间越长,则吞吐量越低

 (2)暂停时间(Pause time):JVM垃圾回收过程当中有一个暂停期,在暂停期间,应用程序不能运行,暂停时间是暂停期的长度。

      非常遗憾的是,一般这两个指标是相互冲突的,改善其中一个会影响到另外一个,根据情景的不同我们决定是优先考虑吞吐量还是暂停时间,对于需要实时响应的应用,我们需要优先考虑暂停时间,对于后台运行应用,我们需要优先考虑吞吐量。


   2. 在考察各种垃圾回收器之前,我们需要了解一下几个重要的策略

 (1)并行(Parallel):并行表示使用多个线程同时进行垃圾回收的工作,此策略一般会从同时改善暂停时间和吞吐量,在有多CPU内核的服务器上,这是基本上我们要使用的策略。

 (2)并发(Concurrent):并行表示垃圾回收器的一些工作(譬如垃圾标记)与应用程序同时进行,这将更进一步缩短暂停时间,需要注意的是,同时垃圾回收器的复杂性会大大增大,基本上是会降低吞吐量,

 (3)内存碎片处理:不压缩、压缩和拷贝三种策略,从空间上讲,拷贝将花费更多的内存(譬如如上内存管理的Young Gen,需要维持一个额外的Suvivor空间),从时间上来讲,不压缩会减低创建对象时的内存分配效率,在垃圾回收上,拷贝策略会比压缩策略更高效。

      

 3.Sun JVM有4垃圾回收器:

 (1)Serial Collector:序列垃圾回收器,垃圾回收器对Young Gen和Tenured Gen都是使用单线的垃圾回收方式,对Young Gen,会使用拷贝策略避免内存碎片,对Old Gen,会使用压缩策略避免内存碎片。基本上,在对内核的服务器上应该避免使用这种方式。在JVM启动参数中使用-XX:+UseSerialGC启用Serial Collector。

 (2)Parallel Collector:并发垃圾回收器,垃圾回收器对Young Gen和Tenured Gen都是使用多线程并行垃圾回收的方式,对Young Gen,会使用拷贝策略避免内存碎片,对Old Gen,会使用压缩策略避免内存碎片。在JVM启动参数中使用-XX:+UseParallelGC启用Parallel Collector。

 (3)Parallel Compacting Collector:并行压缩垃圾回收器,与Parallel Collector垃圾回收类似,但对Tenured Gen会使用一种更有效的垃圾回收策略,此垃圾回收器在暂停时间上会更短。在JVM启动参数中使用-XX:+UseParallelOldGC启用Parallel Compacting Collector。

 (4)Concurrent Mark-Sweep (CMS) Collector:并发标志清除垃圾回收器,对Young Gen会使用与Parallel Collector同样的垃圾回收策略,对Tenured Gen,垃圾回收的垃圾标志线程与应用线程同时进行,而垃圾清除则需要暂停应用线程,但暂停时间会大大缩减,需要注意的是,由于垃圾回收过程更加复杂,会降低总体的吞吐量。

 

本帖内容来自于

jVM 工作原理学习笔记

Sun JVM内存管理和垃圾回收

点击打开链接