JVM基础知识整理

生态圈
 
首先,我们先看一下java的生态圈
 
在官网( https://docs.oracle.com/javase/8/docs/index.html)中,如下图显示
刚开始的时候,官方文档上就说了,java SE8 有两个产品,分别是java开发环境(JDk Java SE Development Kit)以及 java 运行环境 (JRE Java SE Runtime Environment
 
从这张图上 我们可以看到 java分为以下几块。java开发环境(JDK) java运行环境(JRE) java SE的相关API接口,精简的JRE(Compact Profiles),最后是HotSpot java运行底层环境
 
 
编译过程
再java文件 编译为class文件的过程中,大体的流程入下所示
 
 
再通过javac 编译完成的class 文件中,我们可以看到他的字节码(用Sublime Text工具)
 
在官网中 有对这块的说明( https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html)这章里面详细讲解字节编码
 
其中 cafe babe 代表它是一个符合class规范的文件
0000 0034 对应10进制中的52 代表一个jdk版本
 
这边依次是可以对应的
 
类装载器 ClassLoader
这张图描述了类装载的一个过程
 
 
装载:
classLoader 根据类的全名 来进行装载类的二进制字节流,本身是借助于类加载器来实现的 
他有以下几个详细步骤
(1) 通过一个类的全限定名获取定义此类的二进制字节流
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口
双亲委派机制
在类加载过程中,不同区域的类对应的加载器不一致
 
这是一张经典的类加载的图解,描述了不同区域的类,由不同的装载器加载。
其中:
 
Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
 
所谓双亲委派机制,即是当我的加载器在进行一次类加载的操作过程中,我会一次往上传递(此时类加载器不会进行加载),直至到最顶层,在顶层进行类的加载,如果在顶层(Bootstrap ClassLoader)中 未能找到该类,无法进行加载,那么会依次往下,找下一个装载器去加载(Extebsion ClassLoader)去进行加载,直至加载成功
 
这样做的好处是,类本身在加载的过程中就带有了一种优先级的关系,比如说Object类,他存在在rt下面,所有的类在加载的时候,都会去优先加载它。这个时候所有的类的Object都是一个类,但是如果没有双亲委派模型,那么会有多个由自己的加载器加载的Object类。
 
ps 重写ClassLoader能够破坏这样一种机制
链接
 
验证
保证类文件被正确的加载 ,其中会进行文件格式验证,元数据验证,字节码验证,符号引用验证
 
准备
为类的静态变量分配内容,并赋值默认值(static int i = 0)
 
解析
为类的符号引用转化为直接引用。类的符号引用是指一个类中引入了其他类,对于编译器来说,就是一串唯一的符号,在解析这一步就是将这个唯一符号转化为 对应的真实的地址
 
初始化
对类的静态成员变量,执行初始化操作(i= 10)
 
JAVA 运行时数据区
 
首先看一张图
 
方法区:
方法区是各个线程共享的内存区域,在虚拟机启动时创建。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
 
在jdk1.8之后,方法区被称为MetaSpace(元空间) 1.7以及之前被称为Perm Space(老年带)
 
Run-Time Constant pool
Class 文件中除了有类的版本、字段、方法、接口等描述 信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在
类加载后进 入方法区的运行时常量池中存放
 
Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
Java对象实例以及数组都在堆上分配。
 
虚拟机栈
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
 
本身是先进后出的逻辑 如下图
 
程序计数器
程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。
 
本地方法栈
执行Native C方面类型的方法
 
 
 
栈帧
java的栈帧(Stack Frame) 中含有 局部变量表(Local Variables)、操作数栈( Operand Stack)  指向运行时常量池的引用 (A reference to
the run-time constant pool) 、方法返回地址 (Return Address) 和附加信息。
每个方法从调用到执行完成的过程,分别对应着一个栈帧在虚拟机中入栈道出栈的过程
 
这是概念图:
 
局部变量表 : 方法中定义的局部变量以及方法的参数存放在这张表中, 在java编译class时,就在方法的Code属性的max_locals数据项中确认了该方法需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽(variable slot)为最小单位,(约占用32个长度,jvm中导向性的说明 每个slot能够存放 boolean byte char short int float reference或者returnAddress类型的数据,对于long或者double这种确认为64位的数据类型,会分割为两个32进行读写)。java虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始到局部变量表的最大的Slot值。如果访问的是32位数据类型的变量,索引n代表了使用第n个Slot,如果是64位的则会使用n与n+1两个Slot.(JVM强制要求,不允许任何方式访问其中一个)
 
操作数栈 :本身也被成为操作栈,这是一个后入先出的栈,操作数栈的最大深度在编译的时候,已经明确。当一个方法刚开始执行的时候,操作数栈是空的,在执行过程中,会与各种字节码指令会对操作数栈进行写入和提取,,这就是入栈与出栈
另外,在概念模型中,两个栈帧是完全独立的,但是实际上往往虚拟机会对整块做一系列的优化,让两个栈帧出现一部分重叠的区域,这样做的好处是在方法调用的时候,公用一部分数据,减少额外的参数赋值传递过程
 
 
动态链接 : 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态 连接 (Dynamic Linking)
 
方法返回地址 : 当一个方法开始执行后 , 只有两种方式可以退出,一种是遇到方法返回的字节码指令(Normal Method Invacation Completion);一种是遇见异常,并且 这个异常没有在方法体内得到处理(Abrupt Method Invacation Completion )。
 
 
演示
源代码
 
编译后,查看字节码
 
 
 
字节码的相关指令的意思 可以查看官网( https://docs.oracle.com/javase/specs/jvms/se8/html/index.html)
其中 iconst_n 指的是 将int类型常量n压入 操作数栈
istore_0 将int类型存入局部变量0
iload_0 从局部变量0中装载int类型入栈
iadd 将栈顶的元素弹出栈 执行加法 结果入栈
ireturn 返回int类型对象
 
 
Java内存对象
 
一个java对象在内存中,一共分为3个部分,分别是对象头,实例数据,以及对齐数据,结果示意图如下:
 
其中 Mark Word(标记字)主要用于表示线程锁状态,GC的分代年龄,以及对象的hashCode
 
其中markWord中 
lock与 biased_lock一起表示锁的状态,其中biased_lock对象是否启用偏向锁标记,占一个二进制位,为1启用,如下图
age 4位java年龄对象。在GC时,对象每在Survivor区域存活一次,年龄加1,达到设定值之后,放入老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15
 
class Pointer 是一个指向方法区种的class的指针,代表着对象可以知道是哪个class的实例
 
length,可选,为数组长度
 
实例数据为对象的主体部分,占用的大小由类型与数量决定
 
对象填充,用来填充数据为8的倍数
 
 
内存模型
 首先,一般数据的存储都是放在heap(堆)中(静态,常量数据会放在常量区中),所以最一开始的图应该是这样的(其中 非堆 指的是方法区,着重讨论方法区和堆的内存分布与垃圾回收,是因为他们的生命周期属于进程级别,计数器与栈是线程级别,他们的生命周期很短,线程消亡对象则死亡,并且他们的内存的开销在编译期的时候基本就可以确认)
 
 
然后,随着随着堆中的数据越来越多,开始进行垃圾回收了(GC),回收那些无效的数据(可达性分析,引用计数)。GC完成之后,数据又新进来了一波,这个时候数据与数据是有差异的,我们将GC了15次(在每个对象的mark wold中,age是4个字符,最大值为16,所以一般阈值为16,来源通过-XX MaxTenuringThreshold设置)的数据理解为这个数据经常被引用,因此将它需要区分开来,因此现在的图形是这样的
 
所以这边划分了old区域 与 yong区域,因为本质上java是通过分代年龄来管理内存的。
但并不是说,所有的对象都是先在yong区 在进行GC(此时的GC指的是Minor GC)后进入到老年区,对于一些特别庞大的数据,当yong区放不下时,会直接放到old区域
因此 内存对象是优先在yong区,大对象在old区,长期存活的对象也在old区
 
PS,大对象是一个不太友好的东西,经常会发生在内存还有一定的情况下,提前进行GC,来空出一片 连续的区域来放置这些大对象(比这个更不友好的是  生命周期很短的大对象)
 
接着往下说,在yong区的对象生生死死,大概长成这幅模样
 
这个时候 又进来了一个对象,他比较大,他要占用三个位置,按照现在对象的分布情况是塞不进去的。因此,为了处理这种情况,我们又把yong区一拆为二,
示意图如下
yong区 一拆为二之后,分为Eden区与Surivior区域,同时拆分Survivor为S1,S2(也称From To),这样操作之后,新对象先进入到Eden区域,再一次GC之后,没有用的数据被回收调,有用的数据从Eden区域挪动到From区域,同时年龄加1(讲单个Survivor没有意义,因为GC之后,Eden区中可能会赛过来一个很大的对象,Survivor区域依旧存在空间碎片不连续问题),此时From区中的对象有两个去处,达到分代年龄阈值的前往Old区域,不达到的前往To区域,此时,Eden区域和From区是空的。(在此过程中,对象之间的空隙就没有了,解决了空间碎片的问题)
 
ps:需要说明的是,当Survivor区域不够用时,我们需要依赖其他的内存(老年代)进行分配担保(Handle Promotion)
 
完整的图如下
 
 
在这样进行内存对象区分之后,GC也进行了区分,一共分为3种,
Minor GC 对新生代进行GC
Major GC 对老年代GC
Full GC 一起GC
 
 
我们要尽量避免GC,因为java的机制是一旦GC就都不能用了,官方称呼他为stop the world。再不济,我们要尽量避免Full GC和Major GC(老年代的GC一般伴随着Minor GC,而两者之间一起GC就是Full GC了)
 
需要说明的是 Eden区和S1,S2区域的占比一般为8:1:1。原因是绝大多数对象是朝生夕死的,只有少部分是在GC之后存活的。同时S1与S2区域,由于相互拷贝的特性,他们的空间大小是完全一致的
 
 
关于垃圾回收 Grabage Collection
java和C++之间有一座内存动态分配和垃圾回收收集技术所围成的高墙,墙外的人想进去,墙里面的人想出来
 
之所以要了解GC,是因为当内存溢出,内存泄漏,当GC成系统瓶颈时,我们要对于这些自动化的GC技术实施必要的监控和调节。
我们一般说的内存GC指的是堆内存的GC,其中程序计数器,虚拟机栈,本地方法栈是随着线程而生死,其中每一个栈帧的分配内存是由类结构确认下来就可知了(不考虑JIT的编译器优化)
 
方法区的回收:Java虚拟机规范中有说明可以不要求虚拟机在方法区实现垃圾回收,而且方法区进行垃圾回收的性价比较低,在堆中,尤其是在新生代,常规一次的垃圾回收,可以回收70到95的空间,而方法区的回收效率远低于此
对于方法区的回收,主要分为两部分 废弃常量与无用的类,这里不做展开。
由此我们将主要的目光放在了堆内存上。
 
垃圾回收一直有三个问题:
哪些对象需要回收
什么时候回收
以及怎么回收
 
判断对象死亡
 
引用计数算法
这是一种比较简单的判断方法,原理是给对象添加一个引用计数器,当有其他对象引用他时,计数器加1,引用失效时,计数器减1
我们认为引用不为0的对象,为有效对象。
客观的来说,这种方式简单并且效率高,但是会存在问题,就是A引用B,B引用A,这样他们的引用会相互依赖,而实际上这两个对象已经没有用了。
在用java代码测试System.gc();之后,内存依旧被释放了,这说明在java中内存对象的回收,不是依赖这种判断方式的
 
可达性分析
可达性分析是将一个GC Root作为起点,从这个节点往下找,搜索过程的路径成为引用链(Reference Chain),当一个对象没有任何引用链与GC Root相连时,认为该对象不可达,也就是无效内存对象
 
示意图如下:Obj1到4为可达,Obj5 Obj6 Obj7 为不可达,会被垃圾回收
 
 
其中GC Root的选取分为以下几种
虚拟机栈(本地方法变量表)的引用对象
方法区中的静态属性的引用对象
方法区中的常量引用对象
本地方法中的Native方法的引用对象
 
需要说明的是就算是对象被标记位不可达,也不是立即死亡,可以通过重写finalize()来拯救自己(GC会进行两次标记,第一次标记需要清除,但是不是立刻清除,放入F-Queue中,当第二次进行清除时,只要这个对象在finalize()中重新有引用,就可以逃脱GC,但是一般不会这么写代码)
 
什么时候回收垃圾
垃圾如何去回收,是在对象进来的时候,发现Eden区不够了 就进行Minor GC,当老年区满了之后,则进行Major GC(通常会调用 Minor GC 也称为Full GC),这个在之前分析内存模型和GC流程的时候已经详细的说明了
 
 
如何进行垃圾回收
垃圾再回收时,主要分为两大块,也就是垃圾回收算法和垃圾回收器。垃圾回收器是对垃圾回收算法的具体实现
 
垃圾回收算法
标记-清除算法(Mark-Sweep)
 
找出内存中需要回收的对象,并且把它们标记出来,下图是具体的标记
清除过后
 
缺点:
(1) 标记和清除两个过程都比较耗时,效率不高
(2) 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无
法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
 
复制算法
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示
 
当其中一块内存使用完了,将存活的内存对象复制到另外一块上,然后把之前的内存空间一次性清除。实际上这样一种GC策略,和内存对象的S1,S2是一致的
复制算法 优点:
1.效率相对高,只需要标记一次,把原本的空间全部删除就可以了
2.获得的内存空间是连续的空间,不存在空间碎片问题
缺点:浪费一半的空间
 
适用场景 新生代的Survivor区,原因是绝大多数的对象 生命周期都比较短暂,而通过GC之后的在新生代依旧还存活的对象是比较少的。同时这样的算法会避免空间碎片的问题,因此,在新生代的Survivor用的是这种算法。当然遇到极端的Survivor区域不够时,我们会向old区域借一定的空间,进行分配担保
 
 
标记整理
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
将所有的存活的内存移动到一边,删除边界以外的所有对象
 
 
分代收集算法
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)
 
 
垃圾收集器
讲完算法之后,那么他们的具体的实现 就交给垃圾收集器了
这是一个垃圾收集器的概览,上面的表示新生代。下面的表示的是老年代。其中G1适用于两者
 
 
这里说一下各种的场景以及优缺点
 
Serial:
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程
 
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用: Client 模式下的默认新生代收集器
ParNew收集器 
可以把这个收集器理解为Serial收集器的多线程版本。
 
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
 
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集 器,看上去和ParNew一样,但是Parallel Scanvenge更关注 系统的吞吐量 。
 
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)   比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序 的运算任务。
 
 
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算 法",运行过程和Serial收集器一样。
 
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理算法"进行垃圾 回收。 优先吞吐量
 
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。 采用的是"标记-清除算法",整个过程分为4步
 
(1)初始标记 CMS initial mark 标记GC Roots能关联到的对象 Stop The World-- ->速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop The  World
(4)并发清除 CMS concurrent sweep
 
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来 说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
 
优点:并发收集、低停顿
缺点:产生大量空间碎 片、并发阶段会降低吞吐量
 
 
G1收集器
 
G1特点
(1)初始标记 CMS initial mark 标记GC Roots能关联到的对象 Stop The World-- ->速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop The  World
(4)并发清除 CMS concurrent sweep
 
优点:并发收集、低停顿
缺点:产生大量空间碎 片、并发阶段会降低吞吐量使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再
是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
 
工作过程可以分为如下几步
 
初始标记(Initial Marking) 标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要暂 停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发 执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需 暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
 
 
 
 
垃圾收集器分类
 
串行收集器->Serial和Serial Old
 
只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备 。
并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old 多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等若交互场 景 。
 
并发收集器[停顿时间优先]->CMS、G1
 
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的 时候不会停顿用户线程的运行。 适用于相对时间有要求的场景,比如Web 。
 
关于吞吐量和停顿时间
停顿时间->垃圾收集器 进行垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)
 
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值