博主专注于做Java程序开发相关技术分享,旨在与各路大神做技术交流,觉得不错的朋友,点个关注,有想深度交流,也可参考博主其他文章:java架构师知识技能图谱-CSDN博客
学习一门语言,我们必须得了解这门语言是如何运行的。所以对于java来说,我们首先要了解jvm。
所谓jvm,故名思义,即java虚拟机,提供了java代码执行的环境,jvm有各个版本,本质上来说,其实是一个在内存中的虚拟机,所以java程序所有的代码,也就都是在内存中运行的。
想了解jvm,离不开三个问题的学习:jvm的内存模型,GC垃圾回收机制,jvm调优
一般,一个jvm都会包含以下几个部分
a.仅工作在Class装载的加载阶段,ClassLoader负责将由java文件编译好的class文件,以二进制流(byte数组)的方式加载到runtime data area运行数据区,从而转化成一个个Class对象,最终这些Class对象可以被jvm用来链接,初始化成各种具体对象。
b.注意ClassLoader只负责加载,而class是否可以正常运行,由 Execution Engine判断;
a.加载
通过classLoader加载class字节码文件,将该二进制流代表的静态存储结构,转换为方法区的运行时数据结构,生成对应的Class对象作为这个类各个数据的访问入口.
b.链接
校验-检查加载class的正确性和安全性
准备-为类变量分配存储空间并设置类变量初始值
解析-jvm将常量池内的符号引用转换为直接引用
c.初始化
类变量赋值
静态代码块
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技术都是在建立在这一基础上。
当用到某个类时,先自下而上查看类加载器是否曾经加载过,如果到了最上层都没曾经加载过,再自上而下去类加载器对负责的目录中加载(注意ExtClassLoader的父类加载器是null,是因为BootStrapClassLoader是C++编写的)
好处:可以避免Class字节码对象被重复加载.
隐式加载 :new 对象
显示加载:
a.类加载器调用loadClass方法
loadClass调用时,Class对象还没有进行链接,所以通过loadClass可以做到延时加载,比如SringIoc
b.Class的静态方法forName时
forName得到的Class对象是初始化好的
也叫做解释器(Interpreter),负责解析class文件中的字节码,解析完成后,将解析后的命令提交操作系统执行。
融合不同的编程语言为Java所用,它的初衷是融合C/C++程序
在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
与硬件有关的应用场景,比如通过Java程序驱动打印机,或者Java系统管理生产设备,由于现在的异构领域间的通信很发达,比如Socket通信,Web Service等,目前应用面减少
作用:运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,即由Execution Engine执行引擎负责解释命令,提交操作系统执行。
栈也叫栈内存,是 Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over。一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配
Native Method Stack中登记native方法,在Execution Engine 执行时加载native libraies
a.也叫PC 寄存器,每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。
b.分支,循环,跳转,异常处理,线程恢复都需要该计数器
c.和线程是一对一的,每个线程私有,不会内存泄露
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类加载之后的运行时数据结构
介绍:一个 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)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
在jvm中,当一个对象被垃圾判断算法判断为垃圾时,会被回收。
所以在了解垃圾回收机制前,需要先明白一个问题,什么是垃圾?
判断算法(是否垃圾)有两种
定义:通过判断对象的引用数量决定对象是否可以被回收
每个对象都有一个引用计数器,被引用+1,完成引用-1
特点:
优点:执行效率高,程序执行受影响小
缺点:无法检测循环引用,导致内存泄露
由于无法处理循环引用的短板,该方法未被主流垃圾回收器采用
定义:通过判断对象的引用链是否可达来决定对象是否可以回收(与根节点不相连),引用链的根节点被称为GC Root
GC Root
1)虚拟机栈中引用的对象(栈帧中的本地变量表)
2)方法区中的常量引用的对象
3)方法区中的类静态属性引用的对象
4)本地方法栈中JNI(Native方法)引用的对象
5)活跃线程引用的对象
a.标记
从根集合进行扫描,对存活的对象进行标记
b.清除
对堆内存从头到尾进行线性遍历,回收不可达对象的内存
c.特点:
碎片化,导致空间不连续
将内存空间划分为对象面与空闲面,每次将存活的对象复制到空闲面,然后将对象面的内存清除
特点:
解决了碎片化,顺序分配内存,简单高效
适用于对象存活率低的场景(需要复制的对象少),比如年轻代
a.标记
从根集合进行扫描,对存活的对象进行标记
b.整理-清除
移动所有存活的对象,且按照内存地址次序排列,然后将末端地址之后的内存回收
c.特点
解决碎片化,不用设置两块内存交换,适用于对象存活率高的场景
按照对象生命周期划分不同区域采取不同的算法
特点:尽可能快速收集生命周期短的对象
空间分配: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)
老年代:存放生命周期较长的对象
触发条件:
1) 老年代空间不足
2) 永久代空间不足 (jdk6,7)
3) CMS GC时出现promotion failed,concurrent mode failture
这两种情况都是有对象要进入老年代,但是老年代空间不足会出现
4) Minor GC晋升到老年代的平均大小大于老年代剩余空间
5) system.gc()调用
6) 使用RMI进行RPC或管理的JDK应用,默认一小时一次full GC
单线程收集,复制算法,进行垃圾收集时,必须暂停所有工作线程,不过简单高效,停顿时间毫秒级(200M)
参数-XX:+UseSerialGC
多线程收集,其余行为,特点与Serial收集器一样
单线程下没优势,多核会比Serial性能高
参数 -XX:+UseParNewGC
多线程,复制算法,尽可能缩短用户线程停顿时间,更关注系统吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集回收)
参数 -XX:+UseParallelGC
单线程,标记-整理算法
-XX:+UseSerialOldGC
多线程,标记-整理,吞吐量优先
可搭配Parallel Scavenge 使用,1.6才有
-XX:+UseParallelOldGC
标记-清除算法, 内存空间碎片化
-XX:+UseConMarkSweepGC
步骤:
1) 初始标记:stop-the-world 扫描并标记老年代中与GC root 或年轻代存活对象直接关联的第一个对象
2) 并发标记:并发追溯关联链并标记,不会停顿
3) 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,或者新加入老年代的对象并标记,从而减少下一个阶段的停顿时间。
4) 重新标记:并发预清理也是并行的,因此也可能引起老年代引用的变化,所以需要重新扫描,stop the world 扫描标记堆中的所有剩余存活对象。
5) 并发清理:清理垃圾对象,不会停顿
6) 并发重置:重置CMS的数据结构
1) 并发与并行
2)分带收集
3) 空间整合,解决碎片化的问题
4) 可预测停顿
1) 将整个java堆内存划分成多个大小相等的Region
2) 保留年轻代与老年代的概念,但二者不在物理隔离,只是概念存在
3) 新生代与老年代都是一部分region的集合,且region可以不连续。分配空间时,也不需要指定哪个region是年轻代,哪个是老年代,因为年轻代region空间被清除后会变为可用状态,后续可以分配为老年代的存储空间
4) 老年代回收不需要整个老年代region被回收,可以回收一部分。
-XX:UseG1GC,复制+标记-整理算法
G1中也叫跨Region引用,在Minor GC时,可能会碰到一些年轻代对象被老年代region中引用,一般这种情况,我们不能扫描整个老年代,会严重影响Minor GC效率。
记忆集,Remembered Set,实际上就是记录了那些Region有老年代指向年轻代的跨代引用(记录从非收集区域指向收集区域的指针集合的抽象数据结构),从而避免Minor GC时需要扫描全部的老年代,RSet本质上是一种哈希表,Key是Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号
每个Region在内存中会被分成一个个小块内存Card,称为卡页,而卡表存储的是卡页的数组,本身也是记忆集的实现,具体实现方式是,若某个卡页中的对象有跨代引用,那该卡页的标识就会变为1,意味着变脏。GC时,会将本收集区卡表中变脏的元素也加入GC Roots扫描
普通引用
1)对象处在有用但非必须的状态,内存空间不足时会被回收
2)实现高速本地缓存,但是不用担心OOM
3)String str = new String();SoftReference sr = new SoftReference(str)
非必须对象,GC时会被回收
String str = new String();WeakReference sr = new WeakReference(str)
不会决定对象的生命周期。任何时候都可能被回收,可能昙花一现,非常短,常用于标记,哨兵。
必须和引用队列ReferenceQueue联合使用
String str = new String();ReferenceQueue queue = new ReferenceQueue();PlantomReference sr = new PlantomReference(str,queue)
每个线程虚拟机栈的大小,一般256k
影响并发线程数的大小
初始堆的大小
堆能达到的最大值