JVM相关

作者: 萍水相逢_程序员 | 来源:发表于2018-11-03 06:37 被阅读0次

    java程序执行过程

    java执行过程.jpg

    运行时数据区域划分

    数据区.jpg
    线程私有区域
    1. 程序计数器:

    1.1. 这块内存区域很小,是当前线程所执行的字节码行号的指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    1.2. 线程执行java方法,程序计数器才有值,保存当前需要执行的指令地址,如果执行的是native方法,则这个计数器是未定义即是空的。

    1.3. 在jvm中,多线程是通过线程轮流切换来获得cpu执行时间。在任何一具体时刻,一个cpu的内核只会执行一条线程中的指令。所以为了使得每个线程在线程切换后能够恢复到切换前的状态,每个线程都要需要自己独立且互不干扰的程序计数器。

    2. java栈/虚拟机栈

    2.1. 生命周期和线程生命周期相同,每个方法执行都会创建一个栈帧。
    2.2. 存储局部变量表,操作数栈,指向当前方法所属的类的运行时常量池的引用,方法返回地址和一些额外的附加信息。每一个方法从调用到执行完毕的过程,对应一个栈帧在虚拟机中入栈到出栈的过程。(局部变量表就是存储局部变量,包过在方法中声明的非静态变量以及函数形参。对于基础类型直接存储值,对于引用类型则存的是指向对象的引用)

    1. 本地方法栈

    作用和原理与java栈相似,不同是为执行本地方法服务的。

    线程间共享的区域
    1. 堆

    大多数应用程序中,堆都是虚拟机所管理内存中最大的一块,唯一目的存放对象的实例和数组(数组的引用存放在java栈中)

    2. 方法区

    在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等

    3. 运行时常量池

    是方法区中有一个非常重要的部分,Class文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中

    对象创建过程

    虚拟机上运到new指令时创建对象的步骤:

    1. 类加载检查
      去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化,如果没有,那么必须先执行类的初始化过程
    2. 虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定。
    3. 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)
    4. 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中
    5. 执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

    怎么保证new对象的安全

    虚拟机采用了CAS配上失败重试的方式保证更新更新操作的原子性和TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)

    TLAB这是一个线程专用的内存分配区域。
    由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

    对象内存分配的两种方法

    为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    指针碰撞(Serial、ParNew等带Compact过程的收集器)
    假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

    空闲列表(CMS这种基于Mark-Sweep算法的收集器)
    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。

    GC回收机制

    1. 那些内存需要回收

    堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区

    2. 什么时候回收

    2.1 对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

    2.2. 当老年代中没有足够的内存空间来存放对象时,虚拟机会发起一次Major GC/Full GC,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full CG。

    2.3. 手动调用System.gc()方法,通常这样会触发一次的Full GC以及至少一次的Minor GC.

    3. 对什么回收,如何找到这些需要回收的垃圾
    3.1 堆回收
    1. 引用计数法 :Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。

    2. 可达性分析法:通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

    Java语言中可以作为GC Roots的对象,只有引用类型的变量才被认为是根,值类型的变量永远不被认为是根包括:
    a. 虚拟机栈中引用的对象
    b. 方法区中静态属性引用的对象
    c. 方法区中常量引用的对象
    d.本地方法栈中JNI(即Native方法)引用的对象

    3.2 方法区回收

    虚拟机规范中不要求方法区一定要实现垃圾回收,而且方法区中进行垃圾回收的效率也确实比较低,但是HotSpot对方法区也是进行回收的,主要回收的是废弃常量和无用的类两部分。判断一个常量是否“废弃常量”比较简单,只要当前系统中没有任何一处引用该常量就好了,但是要判定一个类是否“无用的类”条件就要苛刻很多,类需要同时满足以下三个条件:

    1、该类所有实例都已经被回收,也就是说Java堆中不存在该类的任何实例

    2、加载该类的ClassLoader已经被回收

    3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证方法区不会溢出。

    4. 垃圾回收算法

    4.1 标记-清除(Mark-Sweep)算法

    这是最基础的算法,标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作

    4.2 复制(Copying)算法

    复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可.

    新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)

    4.3 标记-整理(Mark-Compact)算法

    过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存

    GC实现机制-Java虚拟机具体实现流程

    a虚拟机在进行垃圾回收时,将Eden和Survivor中还存活着的对象进行一次性地复制到另一块Survivor空间上,直到其两个区域中对象被回收完成,当Survivor空间不够用时,需要依赖其他老年代的内存进行分配担保。当另外一块Survivor中没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老生代,在老生代中不仅存放着这一种类型的对象,还存放着大对象(需要很多连续的内存的对象),当Java程序运行时,如果遇到大对象将会被直接存放到老生代中,长期存活的对象也会直接进入老年代。如果老生代的空间也被占满,当来自新生代的对象再次请求进入老生代时就会报OutOfMemory异常。

    6 GC判断对象死亡过程

    Java虚拟机在进行死亡对象判定时,会经历两个过程。如果对象在进行可达性分析后没有与GC Roots相关联的引用链,则该对象会被JVM进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,如果当前对象没有覆盖该方法,或者finalize方法已经被JVM调用过都会被虚拟机判定为“没有必要执行”。如果该对象被判定为没有必要执行,那么该对象将会被放置在一个叫做F-Queue的队列当中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,在执行过程中JVM可能不会等待该线程执行完毕,因为如果一个对象在finalize方法中执行缓慢,或者发生死循环,将很有可能导致F-Queue队列中其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。如果在finalize方法中该对象重新与引用链上的任何一个对象建立了关联,即该对象连上了任何一个对象的引用链,例如this关键字,那么该对象就会逃脱垃圾回收系统;如果该对象在finalize方法中没有与任何一个对象进行关联操作,那么该对象会被虚拟机进行第二次标记,该对象就会被垃圾回收系统回收。值得注意的是finaliza方法JVM系统只会自动调用一次,如果对象面临下一次回收,它的finalize方法不会被再次执行。

    7.内存分配 主要规律

    7.1. 本地线程分配缓冲
    每个线程在Java堆中预先分配一小块内存,TLAB比较小,直接在TLAB上分配内存的方式称为快速分配方式,而TLAB大小不够,导致内存被分配在Eden区的内存分配方式称为慢速分配方式。

    7.2. 对象优先分配在Eden区上

    7.3. 大对象直接进入老年代

    7.4. 长期存活的对象将进入老年代。Eden区中的对象在一次Minor GC后没有被回收,则对象年龄+1,当对象年龄达到“-XX:MaxTenuringThreshold”设置的值的时候,对象就会被晋升到老年代中。

    7.5. Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到“-XX:MaxTenuringThreshold”设置要求的年龄

    8 Java内存模型

    主内存和工作内存

    Java内存模型的主要目的是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。注意一下,此处的变量并不包括局部变量与方法参数,因为它们是线程私有的,不会被共享,自然也不会存在竞争,此处的变量应该是实例字段、静态字段和构成数组对象的元素。

    Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量和主内存副本拷贝(是一些在线程中用到的对象中的字段罢了),线程对变量所有的操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

    内存间相互交互

    关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面体积的每一种操作都是原子的、不可再分的:

    1、lock(锁定):作用于主内存中的变量,它把一个变量标识为一条线程独占的状态

    2、unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

    3、read(读取):作用于主内存中的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

    4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

    5、use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,没当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作

    6、assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

    7、store(存储):作用于工作内存中的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用

    8、write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中

    Java内存模型还规定了在执行上述8种基本操作时必须满足以下规则:

    1、不允许read和load、store和write操作之一单独出现

    2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了滞后必须把该变化同步回主内存

    3、不允许一个线程无原因地把数据从线程的工作内存同步回主内存中

    4、一个新的变量只能从主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量

    5、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁

    6、如果对同一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

    7、如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定的变量

    8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中

    volatile型变量的特殊规则

    1、在工作内存中,每次使用某个变量的时候都必须线从主内存刷新最新的值,用于保证能看见其他线程对该变量所做的修改之后的值

    2、在工作内存中,每次修改完某个变量后都必须立刻同步回主内存中,用于保证其他线程能够看见自己对该变量所做的修改

    3、volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序顺序相同

    相关文章

      网友评论

          本文标题:JVM相关

          本文链接:https://www.haomeiwen.com/subject/ngghxqtx.html