java virtual machine_learn

Java虚拟机学习


JVM


JVM是一个虚构出来的计算机,有自己的处理器,堆栈,寄存器以及相应的指令系统等。JVM是JRE的一部分,通过在实际的计算机上仿真模拟各种计算机功能,这样就能使Java在跨平台上运行。



JVM内存区域划分



JVM的内部体系结构分为三个部分,分别为类装载器子系统,运行时数据区和执行引擎。



类装载器子系统(ClassLoader)


每个Java虚拟机都有一个类加载器,负责查找并加载程序中的类,接口,并给其确定唯一的名字。Java虚拟机有两种类装载器:系统类装载器和用户自定义类装载器,系统类装载器是JVM实现的一部分,用户自定义类装载器是Java程序的一部分,其必须是类装载器ClassLoader类的子类。


  • 启动类装载器(bootstrap calss loader): 其用来加载Java的核心库,用原生代码来实现的,没有继承java.lang.ClassLoader
  • 扩展类装载器(extensions class loader): 其用来加载Java的扩展库,Jav虚拟机的实现会提供一个扩展库目录,该装载器就是在这个目录下查找加载类。
  • 应用程序类装载器(application class loader): 其根据java应用的类路径(classpath)来加载Java应用的类。通过ClassLoader.getSystemClassLoader()获取它。
  • 用户自定义装载器(user class loader): 除了系统提供的类装载器之外,我们还可以通过继承java.lang.ClassLoader类的方式来实现自己的类装载器来满足一些特殊的需求。


类装载器子系统涉及Java虚拟机的其它组成部分和来自java.lang库的类。ClassLoader类定义的方法为程序提供了访问类装载器机制的接口。对于每个被装载的类型,Java虚拟机都会给它创建一个java.lang.Class类的实例来代表该类型。和其它对象一样,用户自定义的类装载器以及Class类的实例放在内存的堆区,装载的类型信息位于方法区。
类装载器子系统除了要查找定位导入二进制class文件外,还需要负责验证被导入的类的正确性,为类的类变量分配并初始化内存,以及解析符号引用。顺序是:
装载(查找并装载类型的二进制数据)——>连接(验证:确保被导入类型的正确性,准备:为类变量分配内存,并将其初始化为默认值,解析:把类型中的符号引用转换为直接引用)——>初始化(将类变量初始化为正确的初始值)
+—-2017-12-16—–+
1. 装载:装载是指将编译后的Java类文件(.class文件)中的二进制数据读入内存,并将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用其来封装类在方法区的数据结构.即加载后最后得到的是Class对象,该对象是单实例的,即无论这个类创建了多少个对象,他的Class对象是唯一的.通过Class.forName(类的全路径), 实例对象.class, 实例对象getClass() 这三种可以加载并获取到该类的Class对象.
类装载时类中的静态代码会被执行,例如:Class.forName()加载JDBC驱动
2. 连接:静态变量的第一次赋值—-默认值
3. 初始化:静态变量第二次赋值—-真正的初始值 类的初始化发生在Java程序对类的首次主动使用中,主动使用有(创建类的实例,访问操作类或接口的静态变量,调用类的静态方法,反射如:Class.forName(类全路径),初始化此类的子类,Java虚拟机启动时被表明为启动类的类:java Test),除以上外其他对类的被动使用是不会导致类的初始化.




执行引擎:执行字节码或者执行调用的本地方法—-执行引擎的行为由指令集定义


指令集:Java方法的字节码流由Java虚拟机的指令序列构成。每条指令包含:一个单字节的操作码(表示需要执行的操作),0或多个操作数(操作数向Java虚拟机提供执行操作码的额外信息,使指令使用的值可能来自当前常量池中的项,当前帧的局部变量中的值或者当前操作数栈顶端的值)。
运行中的Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。
主要的执行技术有:解释,及时编译,自适应优化,芯片级直接执行。自适应优化吸取解释和及时编译的优点,采取两种结合的方式。 自适应优化——开始对所有的代码都采取解释执行的方式并监视代码的执行情况,然后对那些经常调用的方法启动一个后台线程,将其及时编译为本地代码进行调用,并进行仔细优化。当该方法不再频繁的被调用,则取消编译过的代码,将其归为解释执行。




运行时数据区:方法区,堆,Java栈,PC寄存器,本地方法栈



  • 方法区——线程共享


当虚拟机装载某个类型时,它使用类装载器定位载入相应的.class文件到虚拟机中,接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。当开发人员在程序中通过Class对象的getName(),isInterface()等方法来获取信息时,这些数据都会来源于方法区,同时方法区也是全局共享的,在一定条件下它也会被GC掉(虚拟机允许通过用户自定义的类装载器来动态扩展Java程序,此时方法区也可以被垃圾回收器收集),当方法区需要使用的内存超过最大允许时,会抛出OutOfMemory。

方法区存放内容如下:

  • 已经被虚拟机所加载的类信息(类名称,类类型(接口还是类),修饰符,类的直接超类名称),
  • 类中的静态(类)变量,
  • 运行常量池runtime constant pool (类中定义的final类型的常量,一个有序集合,包括直接常量(string,integer,floating 常量)和对其他类型,字段,方法的符号引用)——class文件除了有类信息外,还有一项是常量池(constant pool table),常量池用于存放编译期生成的各种字面量和符号的引用,这部分内容将在类加载后进入方法区的运行时常量池中存储。——运行时常量池相对于class文件常量池的一个重要特征是具备动态性:即除了可以存储class文件常量池中的内容外,运行期间也可能将新的常量放入到池中,比如String类的intern()方法,,参考: 深入理解java虚拟机(三):String.intern()-字符串常量池
  • 类中的方法信息(方法名,返回类型,参数数量和类型,修饰符),
  • 字段信息(字段名,类型,修饰符),
  • 指向ClassLoader类的引用(每个类型被装载时,虚拟机必须跟踪确定它是由系统类装载器还是由用户自定义装载器装载的),
  • 指向Class类的引用(对于每隔一个被装载的类型,虚拟机都为其相应的创建了一个java.lang.Class类实例)



  • 堆(heap)——线程共享


堆是JVM用来存储对象实例以及数组值(数组在Java虚拟机中是一个真正的对象)的区域,可以认为Java中所有通过new创建的对象的内存都在堆中分配,堆中的对象所占的内存是需要等待GC进行回收的(JVM没有释放内存的指令,需要将释放内存的任务交给垃圾收集器处理)。堆是JVM中所有线程共享的。




  • Java栈(Java stack) ——线程私有,生命周期与线程相同


每当启动一个线程时,Java虚拟机就会为他分配一个Java栈。Java栈由许多栈帧组成,一个栈帧包含一个对应的Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到Java栈中,当该方法返回时,这个栈帧就会从Java栈中弹出。

栈帧:由局部变量区,操作数栈和帧数据区组成。当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈大小,并根据此来分配帧的内存,然后压入栈中。

  • 局部变量区

    局部变量区被组织为以字长为单位,从0开始计数的数组。字节码指令通过从0开始的索引使用其中的数据。类型为int, float, reference和returnAddress的值在数组中占据一项,而类型为byte, short和char的值在存入数组前都被转换为int值,也占据一项。但类型为long和double的值在数组中却占据连续的两项。如下图:

  • 操作数栈

    与局部变量区一样,操作数栈被组织成一个以字长为单位的数组,其通过标准的栈操作访问。

  • 帧数据区

    Java栈帧需要帧数据区来支持常量池的解析——(每当虚拟机要执行一个需要操作常量池数据的指令时,就会通过帧数据区中指向常量池的指针来访问常量池),正常方法返回——(帧数据区还要帮助虚拟机处理Java方法的正常结束或异常中止。如果通过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置程序计数器指向发起调用方法的下一个指令;如果方法有返回值,虚拟机需要将它压入到发起调用的方法的操作数栈)以及异常派发机制——(为了处理Java方法执行期间的异常退出情况,帧数据区还保存一个对此方法异常表的引用)。 - - -



  • PC寄存器(程序计数器 program counter)——线程私有,生命周期与线程相同


每一个线程都有它自己的PC寄存器,也是在该线程启动时创建的。PC寄存器的内容总是指向下一条即将被执行的指令的地址,这里的地址可以是一个本地地址,也可以是在方法区中相对于该方法的起始指令的偏移量。
如果线程执行Java方法,则PC寄存器保存的是下一条执行指令的地址。若线程执行的是本地方法,那么此时PC寄存器的值是”undefined”。




  • 本地方法栈(native method stack)——线程私有,生命周期与线程相同


当线程调用Java方法时,虚拟机会创建一个新的栈帧并将其压入到对应线程的Java栈。当线程调用的是本地方法时,虚拟机会保持Java栈不变,不再向Java栈中压入新的栈帧,虚拟机只是简单的动态连接并调用指定的本地方法。
依赖于本地方法的实现,如某个JVM实现的本地方法接口使用C连接模型,则本地方法栈就是C栈,可以说某线程在调用本地方法时,就进入了一个不受JVM限制的领域,也就是JVM可以利用本地方法来动态扩展本身。






JVM垃圾回收(Generational Collecting)


GC通过确定对象是否被活动对象引用来确定是否收集回收该对象。

触发GC的条件


  • Java内存不足时,GC被调用。当应用程序在运行时在运行过程中创建新的对象,若此时内存空间不足,就会强制调用GC线程。若GC一次扔不能满足内存分配,会再次调用GC,若仍无法满足要求,则会报错”out of memory”,Java应用停止。
  • GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行的时候被调用。


两个重要的方法


  • System.gc()

    使用System.gc()直接请求Java的垃圾回收。

  • finalize()

    在jvm垃圾回收之前调用的方法。之所有要使用finalize(),是存在着垃圾回收器不能处理的情况:1) 在本地方法native method调用中,可能由于在分配内存的时候可能采用了类似C语言的做法,而非Java的new做法,比如本地方法调用了C++的malloc()来分配内存而没有调用free()来释放掉内存,这时候就可能造成内存泄露。这时就可以在finalize()方法中用本地方法调用free()来释放掉这些特殊的内存空间。 2)又或者是打开了文件资源,这些资源不属于垃圾回收器能回收的范围,则需要在finalize()中调用对应的本地方法来回收文件资源。


减少GC开销的措施


  • 不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。
  • 减少对临时对象的使用。临时对象在方法结束后会成为垃圾,很快创建很快结束,增加了GC开销。
  • 对象不用时最好置为NULL。NULL对象一般都会作为垃圾处理,把不用的对象置为NULL有利于GC判定垃圾效率。
  • 能用基本类型int long,就不要new Integet,new Long对象。基本类型占用内存资源相应较小。
  • 少用静态对象变量。静态对象变量属于全局变量,不会被GC回收,他们会一直占用内存空间。
  • 字符串修改用StringBuffer,StringBuilder,不用String。
  • 避免大量集中new新对象。


对象在jvm堆区的状态


  • 可触及状态:程序中还有变量引用,那么此对象为可触及状态。
  • 可复活状态:当程序中已经没有变量引用这个对象,那么此对象由可触及状态转为可复活状态。CG线程将在一定的时间准备调用此对象的finalize方法(finalize方法继承或重写子Object),finalize方法内的代码有可能将对象转为可触及状态,否则对象转化为不可触及状态。
  • 不可触及状态:只有当对象处于不可触及状态时,GC线程才能回收此对象的内存。


常用垃圾收集器


  • 标记-清除收集器 mark-sweep
  • 复制收集器 copying
  • 标记-压缩收集器 mark-compact
  • 分代收集器 generational


垃圾收集算法

tracing算法

基于tracing算法的垃圾回收也称为标记和清除(mark-sweep)垃圾收集器。
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有的需要被回收的对象,清除阶段就是回收被标记的对象所占用的内存空间。
此算法缺陷就是很容易产生内存碎片,在产生大量的内存碎片后就可能无法对新的大对象所需要的内存空间进行分配。


copying算法

为了解决标记-清除算法的缺陷,coping算法是将内存按内存容量划分为大小相等的两块,每次对新对象的内存分配只使用其中的一块。当这一块内存用完的时候,就将在这块内存上还存活下来的对象复制到另以空闲块内存上面,然后把那块已经使用的内存空间全部清理。
此算法虽然不会产生内存碎片,但是每次只能使用一半的内存空间,降低了内存实际使用率。而且当存活的对象还很多的时候,需要将它们全部复制到另一块内存上,这也使效率降低。

compating算法

为了解决compying算法的缺陷而充分的利用内存空间,提出了mark-compact 算法,即标记-压缩。该算法的标记阶段和mark-sweep一样将所有需要被回收的对象进行标记。但是标记完成后,它不是直接清理可回收对象,而是将存活的对象都向一端移动,然后清理掉存活对象边界以外的内存空间。这样即不会产生内碎片,也充分利用了内存空间。


generation算法

分代收集算法是目前大部分jvm的垃圾回收器所采用的算法。它的核心思想是根据对象存活的生命周期来划分不同的区域,对每个区域进行不用的垃圾回收策略。一般情况将堆区分为老年代(tenured generation)和新生代(young generation) ,老年代的特点是每次垃圾回收时只会有少量的对象需要被回收,而新生代的特点是每次垃圾回收时都会有大量的对象需要被回收掉。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。


  • 新生代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“minor GC”。
  • 老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模,GC发生的次数比在年轻代的少。对象从老年代消失时,我们说 “major GC”(“full GC”)。
  • 永久代(permanent generation)也称为“方法区(method area)”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC。






参考来自
深入理解Java虚拟机体系结构
什么是JVM?
面试准备之JVM的组成、垃圾回收机制
深入理解Java虚拟机
深入理解JVM–JVM垃圾回收机制

打赏

取消

感谢您的支持!

扫码支持
扫码支持