论 JVM 简述

博主专注于做Java程序开发相关技术分享,旨在与各路大神做技术交流,觉得不错的朋友,点个关注,有想深度交流,也可参考博主其他文章:java架构师知识技能图谱-CSDN博客

前言

学习一门语言,我们必须得了解这门语言是如何运行的。所以对于java来说,我们首先要了解jvm。

所谓jvm,故名思义,即java虚拟机,提供了java代码执行的环境,jvm有各个版本,本质上来说,其实是一个在内存中的虚拟机,所以java程序所有的代码,也就都是在内存中运行的。

想了解jvm,离不开三个问题的学习:jvm的内存模型,GC垃圾回收机制,jvm调优

1.JVM的内存模型

一般,一个jvm都会包含以下几个部分

1.1Class Loader 类加载器

1.1.1作用

a.仅工作在Class装载的加载阶段,ClassLoader负责将由java文件编译好的class文件,以二进制流(byte数组)的方式加载到runtime data area运行数据区,从而转化成一个个Class对象,最终这些Class对象可以被jvm用来链接,初始化成各种具体对象。

b.注意ClassLoader只负责加载,而class是否可以正常运行,由 Execution Engine判断;

1.1.2类的装载过程

a.加载

通过classLoader加载class字节码文件,将该二进制流代表的静态存储结构,转换为方法区的运行时数据结构,生成对应的Class对象作为这个类各个数据的访问入口. 

b.链接

校验-检查加载class的正确性和安全性

准备-为类变量分配存储空间并设置类变量初始值

解析-jvm将常量池内的符号引用转换为直接引用 

c.初始化

类变量赋值 

静态代码块

1.1.3 类加载器的分类

a.BootStrapClassLoader

C++编写,加载核心库的java.*下的类,比如java.lang下的类都由该加载器加载,其他加载器的Class也是由该加载器加载的

b.ExtClassLoader

java编写,加载扩展库javax.*,其实是加载java.ext.dirs对应的目录下的class文件 

c.AppClassLoader

java编写,加载程序所在目录(classpath下的类) 

d.自定义ClassLoader

java编写,定制化加载,自己实现,甚至可以加载非class文件。

常用方法

1)findClass:需要主要实现的方法,在方法中需要调用defineClass,这个方法已经由ClassLoader实现好了,传入一个流即可

2)defineClass:

3)loadClass:loadClass方法就是加载方法,即给定一个类名,最终返回一个Class的实例

应用:由于java类加载器规范是很灵活的,只要能获取到合法的流即可,很多举足轻重的java技术都是在建立在这一基础上。

1.1.4ClassLoader双亲委派机制

当用到某个类时,先自下而上查看类加载器是否曾经加载过,如果到了最上层都没曾经加载过,再自上而下去类加载器对负责的目录中加载(注意ExtClassLoader的父类加载器是null,是因为BootStrapClassLoader是C++编写的)

好处:可以避免Class字节码对象被重复加载.

1.1.5类(Class)的加载方式

隐式加载 :new 对象

显示加载:

a.类加载器调用loadClass方法

loadClass调用时,Class对象还没有进行链接,所以通过loadClass可以做到延时加载,比如SringIoc

b.Class的静态方法forName时

forName得到的Class对象是初始化好的

1.2Execution Engine 执行引擎

也叫做解释器(Interpreter),负责解析class文件中的字节码,解析完成后,将解析后的命令提交操作系统执行。

1.3Native Interface 本地接口

1.3.1 作用

融合不同的编程语言为Java所用,它的初衷是融合C/C++程序

1.3.2 方式

在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

1.3.3 应用

与硬件有关的应用场景,比如通过Java程序驱动打印机,或者Java系统管理生产设备,由于现在的异构领域间的通信很发达,比如Socket通信,Web Service等,目前应用面减少

1.4 Runtime Data Area 运行数据区

作用:运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,即由Execution Engine执行引擎负责解释命令,提交操作系统执行。

1.4.1Java Stack 虚拟机栈 

栈也叫栈内存,是 Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over。一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配

1.4.2 Native Method Stack 本地方法栈 

Native Method Stack中登记native方法,在Execution Engine 执行时加载native libraies 

1.4.3 PC Register 程序计数器

a.也叫PC 寄存器,每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。

b.分支,循环,跳转,异常处理,线程恢复都需要该计数器

c.和线程是一对一的,每个线程私有,不会内存泄露

1.4.4 Method Area 方法区

a. java7 永久区 PermGen space (旧)

特点:JVM规范中,方法区的实现,只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有(不叫)“PermGen space”。 在 JDK 1.8 中,HotSpot 将 “PermGen space”移除,取而代之是Metaspace(元空间)

存储内容:一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,即运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。默认64M大小 ,运行时常量池在JDK1.6及之前版本的JVM中是方法区的一部分,所以也在永久代中,JDK1.7及之后,JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。

异常:java.lang.OutOfMemoryError: PermGen space,表示Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。

b. java8 元空间 Metaspace(新) 

元空间的本质和永久代类似,都是对JVM规范中方法区的实现

特点:元空间并不在虚拟机的内存中,而是使用本地内存

c. 特点:被所有线程共享,保存被ClassLoader类加载之后的运行时数据结构

1.4.5  Heap 堆

介绍:一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把对象、方法、常变量放到堆内存中,以方便执行器执行,

特点:堆被所有线程共享

结构:

a.新生区

介绍:新生区是类的诞生、成长、消亡的区域,一个对象在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分,即伊甸区(Eden space)/幸存者区(Survivor pace)。

伊甸区(Eden space):所有的对象都是在伊甸区被new出来的

幸存者区(Survivor pace):分为 0区(Survivor 0 space)/1区(Survivor 1 space)

当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区

b.老年代

介绍:幸存者1 区也满了之后,java对象移动到养老区,养老区 养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。

异常: 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二: (1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。 (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

2.GC垃圾回收机制

2.1 什么是垃圾

在jvm中,当一个对象被垃圾判断算法判断为垃圾时,会被回收。

所以在了解垃圾回收机制前,需要先明白一个问题,什么是垃圾?

判断算法(是否垃圾)有两种

2.1.1引用计数法

定义:通过判断对象的引用数量决定对象是否可以被回收

每个对象都有一个引用计数器,被引用+1,完成引用-1

特点:

优点:执行效率高,程序执行受影响小

缺点:无法检测循环引用,导致内存泄露

由于无法处理循环引用的短板,该方法未被主流垃圾回收器采用

2.1.2 可达性分析算法

定义:通过判断对象的引用链是否可达来决定对象是否可以回收(与根节点不相连),引用链的根节点被称为GC Root

GC Root

1)虚拟机栈中引用的对象(栈帧中的本地变量表)

2)方法区中的常量引用的对象

3)方法区中的类静态属性引用的对象

4)本地方法栈中JNI(Native方法)引用的对象

5)活跃线程引用的对象

2.2 垃圾回收算法

2.2.1 Mark and Sweep(标记清除)

a.标记

从根集合进行扫描,对存活的对象进行标记

b.清除

对堆内存从头到尾进行线性遍历,回收不可达对象的内存

c.特点:

碎片化,导致空间不连续

2.2.2 复制算法(Copying)

将内存空间划分为对象面与空闲面,每次将存活的对象复制到空闲面,然后将对象面的内存清除

特点:

解决了碎片化,顺序分配内存,简单高效

适用于对象存活率低的场景(需要复制的对象少),比如年轻代

2.2.3 标记-整理算法

a.标记

从根集合进行扫描,对存活的对象进行标记

b.整理-清除

移动所有存活的对象,且按照内存地址次序排列,然后将末端地址之后的内存回收

c.特点

解决碎片化,不用设置两块内存交换,适用于对象存活率高的场景

2.2.4 分代收集算法(Generation Collector)

按照对象生命周期划分不同区域采取不同的算法

a.Minor GC(年轻代 yangGC)

特点:尽可能快速收集生命周期短的对象

空间分配:1/3的堆空间,其中Eden 8/10,from与to 1/10

(相关参数 -XX:SuvivorRatio:Eden和Survivor的比值,默认8:1

相关参数 -XX:NewRatio:老年代与年轻代内存大小的比例。)

详情:当Eden区满,会触发一次Minor GC,将存活的对象存放入from,当Eden再次满,会再触发一次Minor GC,将to与from内存地址进行一次交换,并将原from与Eden区的存活的对象拷贝到原to,同时年龄加1,Eden与原from清空(原from会被当成to),当Eden区再次满了之后,会第三次触发Minor GC,会将对象都复制到to,清空from,对象的年龄+1,周而复始,直到对象达到一定年龄会进入老年代

对象进入老年代的情况:

1)经历一定Minor次数依然存活

(相关参数 -XX:MaxTenuringThreshold:对象从年轻晋升到老年代的GC次数阈值,默认15)

2)Survivor区中存放不下的对象

3)新生的大对象(相关参数-XX:+pretenuerSizeThreshold)

b. Full GC/Major GC (老年代,一般老年代的GC会包含年轻代的垃圾收集)

老年代:存放生命周期较长的对象

触发条件:

1) 老年代空间不足

2) 永久代空间不足 (jdk6,7)

3) CMS GC时出现promotion failed,concurrent mode failture 

这两种情况都是有对象要进入老年代,但是老年代空间不足会出现

4) Minor GC晋升到老年代的平均大小大于老年代剩余空间

5) system.gc()调用

6) 使用RMI进行RPC或管理的JDK应用,默认一小时一次full GC

2.3 垃圾收集器

2.3.1 年轻代

a.Serial收集器

单线程收集,复制算法,进行垃圾收集时,必须暂停所有工作线程,不过简单高效,停顿时间毫秒级(200M)

参数-XX:+UseSerialGC

b.ParNew收集器

多线程收集,其余行为,特点与Serial收集器一样

单线程下没优势,多核会比Serial性能高

参数 -XX:+UseParNewGC

c.Parallel Scavenge收集器

多线程,复制算法,尽可能缩短用户线程停顿时间,更关注系统吞吐量

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集回收)

参数 -XX:+UseParallelGC

2.3.2 老年代

a. Serial Old收集器

单线程,标记-整理算法

-XX:+UseSerialOldGC

b.Parallel Old收集器

多线程,标记-整理,吞吐量优先

可搭配Parallel Scavenge 使用,1.6才有

-XX:+UseParallelOldGC

c.CMS收集器

标记-清除算法, 内存空间碎片化

-XX:+UseConMarkSweepGC

步骤:

1) 初始标记:stop-the-world 扫描并标记老年代中与GC root 或年轻代存活对象直接关联的第一个对象

2) 并发标记:并发追溯关联链并标记,不会停顿

3) 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,或者新加入老年代的对象并标记,从而减少下一个阶段的停顿时间。

4) 重新标记:并发预清理也是并行的,因此也可能引起老年代引用的变化,所以需要重新扫描,stop the world 扫描标记堆中的所有剩余存活对象。

5) 并发清理:清理垃圾对象,不会停顿

6) 并发重置:重置CMS的数据结构

2.3.3 G1(Gerbage First)

a.特点

1) 并发与并行

2)分带收集

3) 空间整合,解决碎片化的问题

4) 可预测停顿

b.变化

1) 将整个java堆内存划分成多个大小相等的Region

2) 保留年轻代与老年代的概念,但二者不在物理隔离,只是概念存在

3) 新生代与老年代都是一部分region的集合,且region可以不连续。分配空间时,也不需要指定哪个region是年轻代,哪个是老年代,因为年轻代region空间被清除后会变为可用状态,后续可以分配为老年代的存储空间

4) 老年代回收不需要整个老年代region被回收,可以回收一部分。

c. 参数

-XX:UseG1GC,复制+标记-整理算法

d.概念
1)跨代引用

G1中也叫跨Region引用,在Minor GC时,可能会碰到一些年轻代对象被老年代region中引用,一般这种情况,我们不能扫描整个老年代,会严重影响Minor GC效率。

2)记忆集

记忆集,Remembered Set,实际上就是记录了那些Region有老年代指向年轻代的跨代引用(记录从非收集区域指向收集区域的指针集合的抽象数据结构),从而避免Minor GC时需要扫描全部的老年代,RSet本质上是一种哈希表,Key是Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号

3)卡表

每个Region在内存中会被分成一个个小块内存Card,称为卡页,而卡表存储的是卡页的数组,本身也是记忆集的实现,具体实现方式是,若某个卡页中的对象有跨代引用,那该卡页的标识就会变为1,意味着变脏。GC时,会将本收集区卡表中变脏的元素也加入GC Roots扫描

2.4 java引用

2.4.1 强引用

普通引用

2.4.2 软引用

1)对象处在有用但非必须的状态,内存空间不足时会被回收

2)实现高速本地缓存,但是不用担心OOM

3)String str = new String();SoftReference sr = new SoftReference(str)

2.4.3 弱引用

非必须对象,GC时会被回收

String str = new String();WeakReference sr = new WeakReference(str)

2.4.4 虚引用

不会决定对象的生命周期。任何时候都可能被回收,可能昙花一现,非常短,常用于标记,哨兵。

必须和引用队列ReferenceQueue联合使用

String str = new String();ReferenceQueue queue = new ReferenceQueue();PlantomReference sr = new PlantomReference(str,queue)

3. JVM调优

3.1 -xss 

每个线程虚拟机栈的大小,一般256k 

影响并发线程数的大小

3.2 -xms

初始堆的大小

3.3 -xmx

堆能达到的最大值

你可能感兴趣的:(jvm,java,开发语言,1024程序员节)