JVM的基本介绍以及垃圾回收

目录
  • JVM java虚拟机
    • JVM
    • jvm主要组成部分及其作用
    • JVM Stack: jvm栈
  • 堆:
    • Jvm heap内存空间划分
  • Full GC
    • 一、OOM含义:
    • 二、监控GC命令
  • 总结

    JVM java虚拟机

    JVM

    java虚拟机是一个可执行java字节码的虚拟机进程。Java虚拟机本质上就是一个程序,java源文件被编译成能被java虚拟机执行的字节码文件,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

    java文件,通过编译器变成了.class文件,接下来类加载器又将这些class文件加载到JVM中。其实可以一句话来解释,类的加载指的是将类的.class文件二进制数据读入到内存中,将其放在运行时数据区的方法区内,

    然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

    jvm主要组成部分及其作用

    我们常说的jvm的主要组成,指的是运行时数据区。但是,其他它还包括其他部分,执行引擎、本地接口、类加载子系统

     

    jvm主要构成:方法区、Java堆、Java虚拟机栈、本地方法区、程序计数器等。

    JAVA Heap:堆,放的是new的对象,数组,大数据的东西;运行时的数据

    JVM Stack: jvm栈,控制方法怎么调度,控制应用程序怎么运行的

    Native Method Stack:本地方法栈,本地的方法栈,调用本地服务

    Method Area:方法区,放的是运行时的方法

    其中方法区和堆是由所有线程共享,

    而Java栈、本地方法区、程序计数器是线程私有。

    我们常说的内存溢出,指的就是 java堆(Heap) 内存不够了。

    JVM Stack jvm栈区,每启动个线程,jvm就为该线程分配一个栈区,线程调用方法时和方法返回时进行入栈和出栈的操作。

    Native Stack 本地方法栈区,与jvm stack类似,不过此区域是为调用本地方法服务的

    Java Heap java的所有对象实例,数组等。

    程序计数寄存器 ,每个线程自己的计数寄存器,存储当前线程执行字节码的地址。

    Program Counter Register:程序计数器:

    每个线程在创建后,都会产生自己的程序计数器和栈帧。程序计数器用来存放执行指令的偏移量和行号指示器等。

    线程执行或者恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响。此区域也不会发生内存溢出异常,程序计数器是占用空间最小的内存区域,不会出现OOM。

    主要记录当前线程正在执行的方法的指令地址。方便线程切换后能恢复到下一条指令的位置,如果是执行native方法,则该计数器为空。

    程序计数器(后文简称PCR)有两个作用:

    • 字节码解释器通过改变PCR依次读取指令,实现代码的流程控制,如顺序执行、选择、循环、异常处理
    • 多线程情况下,PCR用于记录当前线程的执行位置,从而当线程被切换回来的时候,能够知道该线程上次运行到哪儿了。

    JVM Stack: jvm栈

    java虚拟机栈也叫线程栈

    虚拟机栈包含:

    1、局部变量表

    2、动态连接

    3、操作栈

    4、方法返回地址

    1、局部变量表

    存放方法参数和局部变量

    相对于类属性的变量准备阶段和初始化阶段,局部变量没有准备阶段,必需显式初始化。

    如果是非静态方法,则在index[0]位置存储的是方法所属对象的实例引用,随后存储的是参数和局部变量。

    2、动态连接

    每个栈帧中包含一个在常量池中,对当前方法的引用。目的是支持方法调用过程的动态连接。

    3、方法返回地址:

    方法执行的时候,有两种退出情况:

    正常退出

    正常执行到任何方法的返回字节码指令,如RETURN、IRETURN等。

    异常退出

    无论何种,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧。

    3、操作栈

    本地方法栈基本上等同于java虚拟机栈。

    堆和栈是程序运行的关键,很有必要把他们癿关系说清楚。

    栈是运行时的单位,而堆是存储的单位。

    栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;

    堆解决的是数据存储的问题,即数 据怎么放、放在哪儿。

    在 Java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有 所不同,因此需要一个独立的线程栈。

    而堆则是所有线程共享的。

    栈因为是运行单位,因此里面存储 的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而 堆只负责存储对象信息。

    元数据区(Matespace):

    包括:

    常量池、类元信息、方法元信息

    园区

    JDK1.8之前叫持久代;1.8之后叫园区。

    园区放的是方法常量

    (以下非必需)

    为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

    第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清 晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。

    第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。 这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方 面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。

    第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上 增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此 栈与堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。

    第四,面向对象就是堆与栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没 有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。

    当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法), 就是运行逻辑,放在栈中。

    我们在编写对象的时候,其实即编写了数据结构,也编写了处理数据的逻辑。

    在 Java 中,Main 函数就是栈的起始点,也是程序的起始点。

    程序要运行总是有一个起点的。同 C 语言一样,java 中的 Main 就是那个起点。无论什么java 程序, 找到 main 就找到了程序执行的入口:

    堆:

    运行时的数据,也就是new的对象和一些数组。

    运行时最大的数据,就在堆内。

    Jvm heap内存空间划分

    堆内(放的是对象,数组)=临时数据,用完就没啦

    年轻代:eden +s1 +s0(eden:s1 :s0默认比例:8:1:1 )

    老年代:(年轻代:老年代默认比例:1:2)

    s0与s1大小相等,位置互换

    堆外(放的是方法,类,常量)

    Eden(伊甸园区):

    所有新new的对象和数组,是在伊甸园区产生的,内存分配是在伊甸园区进行的。

    在伊甸园区new 一个对象,如果这个对象被用到了,那么他的引用就+1。应用的越多,占内存越多。

    当伊甸园区内存被占满时,就不能再new对象,程序依赖对象,所以程序也就不能运行。java应用程序为了能继续运行,就要干掉那些不被引用的对象,对这些不被引用的对象进行垃圾回收。

    现代虚拟机一般使用的内存回收策略是分代收集,即把对象分为两代,新生代(年轻代)使用复制算法回收内存,老年代使用标志-整理算方法回收内存。

    垃圾回收的时候,如何判断该对象是否可以被回收:

    有2种算法

    算法一:引用计数(已经被废弃了)

    引用计数,对象被创建后,有个计数器。只要对象被引用,那么计数器就+1,再次被引用,就再+1,如果方法执行完成,对象不被引用了,计数器就-1。

    当计数器为0时,该对象没有被引用,就可以被垃圾回收了。

    引用计数算法,有一个弊端,就是在执行递归的时候,就没有办法计算了。

    算法二:寻根判断

    寻根判断,从对象的根结点开始去找,是否被引用,如果跟节点都没有引用,那么就是没有被引用。

    被引用的对象,称为存活对象。

    未被引用的对象,称为非存活对象。

    当伊甸园区被占满时,触发GC(垃圾回收 Garbage Collection)。

    GC分两步,第1步标记,对非存活对象进行标记。第2步清扫,将非存活对象从内存中干掉,释放内存,同时将存活的对象移到存活区中。

    YGC

    发生在年轻代。

    在YGC之前,伊甸园区是满的,无法new对象,程序无法运行。

    YGC之后,伊甸园区就空了,就可以继续new对象。

    YGC整个流程:

    第一次gc,,对存活的对象进行标记,然后将Eden区的存活对象,存活区S0

    第二次gc,使用寻根判断的算法,对存活的对象进行标记,然后将Eden区的存活对象,移到存活区S0

    步骤1:new的对象、数组,直接进入eden园区。

    步骤2:eden园区满了,触发ygc,对【eden园区】的对象和数组进行标记(使用寻根判断的算法),清扫(将没有引用的对象干掉,被引用的移到其中一个存活区s0);

    eden园区第二次满了,再次触发ygc,对【eden园区和存活区s0】的对象和数组进行标记,清扫将没有引用的干掉,被引用的对象和数组移到在另一个存活区s1(这里用的是复制算法)

    步骤3:fgc,全gc,堆内存和持久代均gc。将没有的干掉,有用的放到老年代。

    年轻代里面的两个存活区,大小相等,且同一时间只有一个存活区有对象,另一个存活区为空。gc之前,存活区s0有对象,s1为空,一次gc之后,s0为空,s1存放对象。

    对象进入老年代原则:

    大对象直接进入老年代长期存活的对象进入老年代动态年龄判断一次Young GC时数据放到存活区,但是存活区满了导致放不下去此时直接进入老年代(尽可能避免这种情况)

    1、大对象直接进入老年代

    在堆中分配的大对象直接挪到老年代

    大对象是指需要大量连续内存空间的对象,例如很长的字符串以及数组。

    虚拟机设置了一个-XX:PretenureSizeThreshold参数,有个默认值,令大于这个设置的对象直接在老年代分配,这个值是可以修改的。

    目的就是为了防止大对象在Eden空间和Survivor空间来回大量复制,大对象很容易把伊甸园区占满,导致YGC频繁。

    2、长期存活的对象进入老年代(伴随YGC产生的)

    虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden区出生并经过第一次YGC/Mintor GC后仍然存活,并且能被Survivor接纳,并被移动到Survivor空间上,那么该对象年龄将被设置为1。

    对象在Survivor区中每熬过一次YGC/Minor GC,年龄就加一,当他的年龄增加到一定程度,就会被移动到老年代(年龄值默认为15)。

    对象晋升老年代的阈值可以通过-XX:MaxTenuringThreshold设置。

    3、动态年龄判断并进入老年代(伴随YGC产生的)

    为了更好的适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才会晋升到老年代。

    如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到MaxTensuringThreshold的要求年龄。

    (相同age对象大小之和>1/2存活区(即s0或者s1)则所有>=age的对象就会进入老年代)

    举个栗子:

    比如:MaxTenuringThreshold 为15,Survivor内存大小为100M。age 为1的,所有对象大小之和为10M。age 为5的,所有对象大小之和为51M。age为6的,所有对象大小之和为5M.

    因为age 为5的对象所占内存之和已经超过了Survivor空间的一半,所以age为5,和age大于5的对象,都要移到老年代(没有age达到15的限制)

    4、一次Young GC时数据放到存活区,但是存活区满了导致放不下去此时直接进入老年代

    尽可能避免这种情况,如果对象直接从Eden区到老年代,那么存活区就没有什么存在的意义了,之所以设置存活区,就是为了将没有引用的对象更早的回收掉,将内存腾出来。

    Full GC

    当老年代满了之后,触发Full GC。

    Full GC 范围:

    整个堆内存(年轻代+老年代)+园数据区

    对堆的回收:

    标记--清扫

    对堆标记,对象是否被引,被引用的为存活对象。

    对园数据回收:

    标记--清扫

    标记类,哪些类失效了。

    回收,将失效的类卸载掉。

    FGC时长,大小跟内存大小有关系,内存大,则FGC时间长。

    时长不太好控制,但是我们可以控制FGC频次。

    当老年代和年轻代,被存活的对象占满时(GC也不能将这些存活的对象清理掉),在Eden就不能再创建新的对象,导致OOM。

    http://www.itcdns.cn/skill/detail/1586233667857

    一、OOM含义:

    OOM,全称“Out Of Memory”,意思是“内存用完了”。 它来源于java.lang.OutOfMemoryError。

    官方介绍为当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出 java.lang.OutOfMemoryError :···

    (注意:这是个很严重的问题,因为这个问题已经严重到不足以被应用处理)。 

    gc的触发条件

    触发条件:

    YGC:

    有且只有这一种情况,eden满了,触发gc

    FGC:

    1、老年代满了

    2、园区某些类已经失效了,在加载的找不到这个类,也会触发FGC,去卸载这个类,释放这个类。

    3、空间担保原则(主要触发fgc的方式)

    4、代码里面显示调用

    5、jmap dump

    内存担保机制

    现代虚拟机把新生代分为三个区域,一个Eden区域,两个Survivor区域,Eden区域与Survivor区域的比例大小是8:1,虚拟机在YGC/Minor GC时在新生代采用复制算法,将存活对象复制到一个Survivor上面,如果Survivor空间不够用时,就需要老年代进行分配担保。

    在发生Minor GC之前虚拟机会先检查老年代最大可用的连续空间是否大于新生代对象的总空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,虚拟机会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均年龄(因为事先不知道存活对象的内存空间,所以取了平均值)。若果大于,虚拟机会尝试进行一次Minor GC,但是这次Minor GC存在风险。如果小于,或者HandlePromotionFailure不允许担保,那这次也要改为Full GC

    空间担保原则:

    每次ygc的时候,都会往老年代里放对象。

    根据历史每次往老年代放的对象大小,根据一个算法,估算这一次要放对象大小。估算的数值,小于老年代剩余内存,就执行ygc。

    如果大于老年代剩余空间,放弃本次ygc,直接fgc。

    代码里面显示调用

    代码里写了system.gc或者get run time .gc

    fgc特别消耗cpu,尽可能把FGC频次减少,最起码要一小时以上

    java进程之间以及跟JVM是什么关系

    1.程序的运行是以进程在内存中的运行形式体现的。当你启动一个程序时,系统会调用其对应进程进入内存运行,图中进程的pid即为进程的唯一标识符。然后进程之间是并发执行的。准确的说,你启动的是java程序,但系统运行的是进程,因为程序是静态的,进程才是动态的,也就是程序并不会进入内存运行,而是其对应进程进入内存运行。

    2.是公用一个JVM的,这个就类似你电脑自己的操作系统,打开两个程序肯定是在同一个系统内存中运行的,原因就是我问题一中说的进程是并发执行的。

    命令行启动的java程序是共用一个jvm的,启动一个程序就是在jvm中开启一个进程,每个进程至少有一个线程,当然可以有多个线程,

    线程之间通信比较简单,就像java书上讲的一样,但进程间的通信复杂点,如管道、内存映射、内存共享、消息队列、socket等,你可以简单理解为两个进程间没关系

    2个进程肯定是2个jvm实例

    二、监控GC命令

    1、jstat监控Java进程的GC情况

    登陆服务器

    ssh root@47.95.122.13

    查看java进程

    ps -ef|grep java

    监控fgc次数和时间

    jstat -gcutil 22893 2000

    jstat  -gcutil  [Java的pid] 2000 20 (间隔2s,总共打印20次)

    jstat -gcutil 22278 2000

    查看gc的情况(内存使用百分比及gc的总时间,gc次数)

    总结

    本文转自网络,如有侵权请联系客服删除。