【Java】Java虚拟机内存模型及回收机制
Java的理解
java是一门纯粹的面向对象的语言。
面向对象这种程序设计模式它将现实世界中的一切事物都看作是对象,例如,一个人是一个对象,汽车、飞机、小鸟等等,都是对象;它强调从对象出发,以对象为中心用人类的思维方式来认识和思考问题。每个对象都具有各自的状态特征(也可以称为属性)及行为特征(方法),java就是通过对象之间行为的交互来解决问题的。
1.平台无关性
(1)为什么要先编译成java字节码再解析成机器码
a.准备工作:每次执行都需要各种检查。
因为编译的过程本来就是在进行编译以及检查的过程,如果直接对java代码解析成机器码,岂不是每次解析的时候都需要进行检查,何不提前检查好了,再进行解析
b.兼容性:也可以将别的语言解析成字节码
比如Clojure,Groovy, JRuby, Jython , Scala等他们最终会 通过不同的编译器(java是javac编译器)编译成 .class字节码在虚拟机进行运行
(2)JVM如何加载.class文件
a.JVM内存结构模型
JVM是内存中的虚拟机,可以理解为,JVM的存储就是内存,我们所有写的常量,变量,方法都在内存中
JVM架构:
ClassLoader (类加载器) : 依据特定的格式,加载class文件到内存
Runtime Data Area (运行数据区域) :JVM内存空间结构模型
Execution Engine (执行引擎) :对命令进行解析(解释器,对命令进行解析,能不能运行需要他来负责)
Native Interface (本机接口) :融合不同开发语言的原生库为java所用,使用Class.forName()方法中的forName0()方法进行其他语言的类库的调用,返回值是native
类从编译到执行的过程(java从源码到运行共有三个阶段)
编译器将.java源文件编译为.class字节码文件 Source源码阶段
ClassLoader将字节码转换为JVM中的Class<对象名>对象(实质是byte数组) Class类对象阶段
JVM利用Class<对象名>对象实例化 * 对象加载到内存 Runtime运行时阶段
ClassLoader
作用:
ClassLoader负责通过将Class文件的二进制数据流(使用io流进行读取的)装载进系统,然后交给java虚拟机进行连接,初始化操作(从系统外获取Class的二进制数据流)
种类: (Alibaba Dragonwell 是一款免费的 OpenJDK 发行版)
BootStrapClassLoader :C++编写,加载核心库java.* http://hg.openjdk.java.net/jd...;browse==>src==>share==>native==>java.lang
ExtClassLoader :java编写,加载扩展库javax.* System.out.println(System.getProperty(“java.ext.dirs”));
AppClassLoader :java编写,加载程序所在目录 System.out.println(System.getProperty(“java.class.path”));
自定义ClassLoader
双亲委派机制:
1.自下向上_检查_类是否已经加载 自定义ClassLoader==》AppClassLoader==》ExtClassLoader ==》BootStrapClassLoader
2.自上向下尝试_加载_类 BootStrapClassLoader (jre/lib/rt.jar)==》ExtClassLoader(jre/lib/ext/*.jar) 》AppClassLoader(classpath目录)》自定义ClassLoader
小编总结:双亲委派机制:检查类是否加载的时候,顺序是自下而上的。如果是加载类的时候,顺序是自上而下的
有资料或文档需求的可以关注公众号发送暗号“12”即可哦
为什么要使用双亲委派机制:
1.避免多份同样字节码的加载(使用委托机制逐层去父类查找是否加载)
类的加载方式:(了解)
隐式加载:new (隐式调用类加载器将类加载带JVM)
显式加载:loadClass,forName等
loadClass和forName的区别:(了解)
Classloder.loadClass得到的class是没有链接的 JDBCUtils.class.getClassLoader() //getResourceAsStream(“druid.properties”)
Class.forName得到的class是已经初始化完成的 Class.forName(“cn.itcast.utils.JDBCUtils”)
JVM内存模型-JDK8:
1.程序计数器:
作用:
1.当前线程所执行的字节码行号指示器(逻辑)
2.改变计数器的值来选取下一条需要执行的字节码指令
3.和线程是一对一的关系,即线程私有
4.对java方法技术,如果是Native方法,则计数器值为Undefined
5.不会发生内存泄漏
干什么用的:循环,跳转,异常处理,线程恢复 都是【程序计数器】通过【改变计数器的值】来实现的,命令指向的作用。
2.java堆(Heap):
对象实例分配区域
GC管理的主要区域
3.java虚拟机栈:
1.java方法执行的内存模型,每个方法运行的时候都会创建一个栈帧【一个栈帧就相当于一个方法执行的时候,存在栈里面的状态】
2.包含多个栈帧,栈帧用来存储(局部变量表【局部变量存储的地方】,操作栈【进行运算的地方】,动态连接???,返回地址)方法调用结束时,帧才会被销毁
局部变量表:包含方法执行过程中的所有变量
操作数栈:入栈,出栈,复制,交换,产生消费变量
递归为什么会引发java.lang.stackoverflowerror异常?
递归过深,栈帧数超出虚拟栈深度,方法调用层次过多
java.lang.OutOfMemoryError异常?
虚拟机栈开启过多,线程开启太多,导致内存不足以创建新的虚拟机栈
扩展:方法结束,栈帧会自动释放,栈的内存,不需要GC去回收
(jstack命令可以生成虚拟机当前时刻的线程快照,可以定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源长时间的等待)
jmap
本地方法栈:
与虚拟机栈相似,只要作用于标注了native的方法
4.方法区:
方法区里面存储了类信息、静态变量、即时编译器编译后的代码(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)等。
JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,放到堆中
元空间(MetaSpace):【方法区的实现,可以理解为方法区是规定放什么东西,元空间是真正存放方法区规定东西的内存】
永久代:是HotSpot(虚拟机)的一种具体实现,实际指的就是方法区
元空间与永久代(PermGen)的区别
元空间使用本地内存,永久代使用的是JVM内存
元空间相比永久代的优势
1.字符串常量池存在永久代中,容易出现性能问题和内存溢出
2.类和方法的信息大小难易确定,给永久代的大小指定带来困难
3.永久代会为GC带来不必要的复杂性【不懂】
4.方便HotSpot与其他JVM如Jrockit的集成
扩展:
JVM三大性能调优参数 -Xms -Xmx -Xss的含义
命令演示:java -Xms128m -Xmx128m -Xss256k -jar **.jar
-Xss:规定了每个线程虚拟机栈的大小,一般来说256k足够,此配置会影响此进程中并发线程数的大小
-Xms:堆的初始值 (如果堆的大小超出初始值的容量怎么办,会扩容到-Xmx配置的堆的内存最大值)
-Xmx:堆能达到的最大值,一般来说我们的初始值和最大值设置一样,以避免每次垃圾回收完成后JVM重新分配内存
java内存模型中堆和栈—内存分配策略(了解)
静态存储:编译时就能确定每个对象目标在运行时的存储空间需求
栈式存储:(动态存储)数据区需求,在编译时未知,运行时模块入口前确定
堆式存储:编译时或运行时,模块入口都无法确定,动态分配
java内存模型中堆和栈的联系:
引用对象、数组时,栈里定义变量保存堆中目标的首地址
java内存模型中堆和栈的区别【重点】:
管理方式:栈自动释放,堆需要GC进行垃圾回收
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆 (内存不能及时释放产生的碎片,因为GC不是实时的)
分配方式:栈支持静态和动态分配,而堆仅支持动态分配
效率:栈的效率比堆高
2.GC【Garbage Collection】
对象被判定为垃圾的标准,没有被其他对象引用的情况下
怎么判定是否为垃圾的算法
a.引用计数算法(了解)
通过判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
任何引用计数为0的对象实例可以被当做垃圾收集
优点:执行效率高,程序执行受影响小
缺点:无法检测出循环引用的情况,导致内存泄漏
b.可达性分析算法(我们现在使用的是这个算法)
通过判断对象的引用链(GC Root)是否可达,来决定对象是否可以被回收
如果GC Root判断对象是可达对象,就会将此对象标记为蓝色
如果GC Root判断对象是不可达对象,就会将此对象标记为灰色,标记为灰色的对象就会被GC回收
哪些对象可以作为GC Root对象
1.栈中引用的局部变量【方法里面的对象】【方法执行会被引用】
2.方法区中类【静态】属性引用的对象【类中的对象】【类加载会被引用】
3.方法区中常量引用的对象【常量】【类加载会被引用】
4.本地方法栈中JNI(即一般说的Native方法)引用的对象【native方法的引用】
垃圾回收算法
a.标记-清除算法(Mark-Sweep GC)
1.标记阶段:从根集合出发,将所有活动对象及其子对象打上标记(使用刚才讲的可行性分析算法进行标记)
2.清除阶段:遍历堆,将非活动对象(未打上标记)的连接到空闲链表上
缺点:标记清除之后,使内存中出现N个不连续的碎片块,分配速度不理想,每次分配都需要遍历空闲列表找到足够大的分块
b.复制算法(Copying)
1.分为对象面和空闲面
2.对象在对象面创建
3.存活的对象被从对象面复制到空闲面
4.将对象面的所有对象内存清除
优点:
解决碎片化的问题
顺序分配内存,简单高效
适用于对象存活率低的场景
缺点:
浪费50%的内存
对于对象存活率较高的场景时,需要频繁的复制
c.标记-整理算法(Mark-Compact)【标记-清除算法 升级版】
1.标记:从根集合进行扫描,对存活的对象进行标记
2.清除: 移动所有存活的对象,且按照内存地址次序依次排列,然后末端内存地址以后的内存全部回收
优点:
自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。
d.分代收集算法(Generational Collection):
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下
1.Minor GC
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法(Copying)。只需要付出少量存活对象的复制成本就可以完成收集。
新生代:尽可能快速的收集掉,那些生命周期短的对象
新生代分为两个区:
Eden区,两个Survivor区(from区,to区【这俩区域会随时进行互换】):
新创建的对象进入Eden区,如果区域满了,触发Minor GC,进行垃圾回收,将可达性对象复制到Survivor 1 区,然后进行垃圾回收,年龄+1
然后Eden区对象又满了的话,触发Minor GC,使Eden区和Survivor 1 区的可达性对象复制到Survivor 2 区,进行垃圾回收,年龄+1
如果然后Eden区对象又满了的话,又触发Minor GC,使Eden区和Survivor 2 区的可达性对象复制到Survivor 1 区,进行垃圾回收年龄+1
如果然后Eden区对象又满了的话,又触发Minor GC,使Eden区和Survivor 1 区的可达性对象复制到Survivor 2 区,进行垃圾回收年龄+1
年龄大于15,进入到老年代中,此参数可以通过 -XX:MaxTenuringThreshould进行设置
如果遇到特别大的对象,需要分配较大区域来装载对象,可以直接进入老年代中
对象如何晋升到老年代:
经历一定的Minor GC次数依然存活的对象
Survivor区中放不下的对象
Eden区新生成的大对象(-XX:+PretenuerSizeThreshold 可以控制大对象的大小)
常用的性能优化参数:
-XX:SurvivorRatio:Eden和Survivor的比例是8:1
-XX:NewRatio: 新生代内存容量与老生代内存容量的比例 1 :2
-XX:MaxTenuringThreshould:对象从新生代晋升到老年代经过GC次数的最大阀值
2.Full GC
老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除(Mark-Sweep GC)或者标记-整理(Mark-Compact)。
Full GC比Minor GC慢,但是执行频率低,不容易死
触发Full GC的条件:
1.老年代空间不足;
【略】永久代空间不足(JDK8以后没有这个情况出现)
2.CMS GC时出现promotion failed 【新生代放不下,准备放到老年代,老年代也放不下】,concurrent mode failure【同时有对象放入老年代中】
【GC日志出现这两种情况时,会触发Full GC】
3.Minor GC晋升到老年代的平均大小 大于 老年代的剩余空间
4.调用System.gc();只是提醒,具体回不回收,虚拟机来决定
5.使用RMI来进行RPC或管理的JDK应用,默认每小时执行一次Full GC,
可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间
总结
1.老年代空间不足
2.手动GC, 调用System.gc()
3.默认每小时执行一次Full GC
垃圾收集器:
专业名词解释:
Stop-the-World【停止这个世界】
JVM由于要执行GC而停止了应用程序的执行
任何一种算法中都会发生
多数GC优化通过减少Stop-the-World发生的时间来提高程序性能
Safepoint【安全点】
分析过程中对象引用关系不会发生变化的点
产生Safepoint点的地方:方法调用;循环跳转;异常跳转
安全点数量得适中,不能太少,会让GC等待时间太长,太多会增加程序运行的负荷
吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) 例:98 / (98+2)=98%
jvm的运行模式:
Server:启动速度较慢,稳定后运行速度较快,默认的,使用java -version命令查看
Client:启动速度较快
常见的垃圾收集器:
新生代的收集器:
Serial收集器(-XX:+UseSerialGC ,复制算法)
1.单线程收集,进行垃圾收集时,必须暂停所有的工作线程,会停顿10ms-100ms之间
2.简单高效,Client模式下默认的新生代收集器
PerNew收集器(-XX:+UseParNewGC,复制算法)
1.多线程收集,其余的行为,特点和Serial收集器一样
2.单核执行效率不如Serial,在多线程执行才有优势(默认开启的线程数和CPU线程数相同)
3.可以和CMS收集器配合
Parallel Scavenge收集器(-XX:+UseParallelGC ,复制算法)
1.也是复制算法,也是多线程
2.比起关注用户线程停顿时间,更关注系统的吞吐量(高效率利用CPU时间,尽可能快的完成运算任务)
3.在多核下执行才有优势,Server模式下默认的新生代收集器
(可以使用-XX:+UseAdaptiveSizePolicy 配合自适应调节策略,会把内存管理的调优任务交给虚拟机去完成)
老年代的收集器:
Serial Old收集器(-XX:++UseSerialOldGC,标记-整理算法 )
1.单线程收集,进行垃圾收集时,必须暂停所有的工作线程
2.简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParallelOld GC ,标记-整理算法)
多线程,吞吐量优先
CMS收集器(-XX:+UseConcMarkSweepGC ,标记-清除算法)
划时代的收集器,垃圾回收线程【几乎】可以和用户线程同时工作,还是必须Stop-the-World才可以
垃圾回收过程:
1.初始化标记:Stop-the-World
2.并发标记:并发追溯标记,程序不会停顿
3.并发预清理:查找执行并发标记阶段从新生代晋升到老年代的对象
4.重新标记:暂停虚拟机,扫描CMS堆中的剩余对象 Stop-the-World
*5.并发清理:清理垃圾对象,程序不会停顿
6.并发重置:重置CMS收集器的数据结构
可以应用于新生代和老年代的收集器 G1收集器(-XX:+UseG1GC ,复制 + 标记-整理算法)
Garbage First收集器的特点:
1.并行和并发,使用多个CPU来缩短停顿的时间
2.分代收集,独立管理整个堆,采用不同方式处理新创建的对象和熬过多次GC的对象
3.空间整合,基于标记整理算法,解决了内存碎片的问题
4.可预测的停顿,能让使用者设置停顿时间
5.将整个java堆内存划分成多个大小相等的Region
6.新生代和老年代不再物理隔离
JDK11正在研发中的两款GC:Epsilon GC 和 ZGC
java中的强引用,软引用,弱引用,虚引用有什么用:
强引用:即使抛出OutOfMemoryError终止程序,也不会回收具有强引用的对象,可以通过将对象设置为null来弱化引用,使其被回收
String str = new String(“abc”);//强引用
软引用:对象处在有用,但非必须的状态,只有当内存空间不足时,GC才会回收该引用的对象的内存,可以用来实现高速缓存
String str = new String(“abc”);//强引用
SoftReference softStr = new SoftReference<>(str);//软引用
弱引用:非必须的对象,比软引用更弱一些; GC时会被回收 ;被回收的概率不大,因为GC线程优先级比较低;适用于偶尔使用且不影响垃圾收集的对象
String str = new String(“abc”);//强引用
WeakReference weakStr = new WeakReference<>(str);//弱引用
虚引用:不会决定对象的生命周期,任何时候都可能被垃圾收集器回收,跟踪对象被垃圾收集器回收的活动,起哨兵作用【必须和引用队列ReferenceQueue联合使用】
String str = new String(“abc”);//强引用
ReferenceQueue queue = new ReferenceQueue();
//ReferenceQueue无实际的存储结构,存储逻辑依赖于内部节点的关系来表达【类似链表】并且可以监控队列里面是否有对象来判断对象是否被回收
PhantomReference phantomStr = new PhantomReference(str, queue);//虚引用
GC发现一个对象具有虚引用,在回收之前,会将该对象虚引用加入到与之关联的引用队列当中,程序可以通过判断引用队列是否加入虚引用来了解被引用的对象是否被回收
四种引用的级别由高到低:强引用>软引用>弱引用>虚引用
实战:
内存泄漏排查方案:
1.使用jps命令查找当前运行项目 pid
2.jmap -dump:format=b,file=d://demo.hprof [pid]使用该命令导出镜像
3.使用MAT工具https://www.eclipse.org/mat/d...
4.导入镜像
5.三步分析法:
查看报告之一:内存消耗的整体状况
查看报告之二:分析问题的所在
查看报告之三:查看下从 GC 根元素到内存消耗聚集点的最短路径
最常见的OOM情况有以下三种:
java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中 的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的 Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导 致方法区溢出。
java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也 会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
OOM分析–heapdump
要dump堆的内存镜像,可以采用如下两种方式:
设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。
使用JDK自带的jmap命令。"jmap -dump:format=b,file=heap.bin " 其中pid可以通过jps获取。
dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:
mat: eclipse memory analyzer, 基于eclipse RCP的内存分析工具。详细信息参见:http://www.eclipse.org/mat/,推荐使用。
jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问 分析结果。不推荐使用,因为在实际的排查过程中,一般是先在生产环境 dump出文件来,然后拉到自己的开发机器上分析,所以,不如采用高级的分析工具比如前面的mat来的高效。
就讲到这里啦~
希望可以帮到你们
有资料或文档需求的可以直接关注公众号【乐字节】(关注后记得一定要回复小编的暗号12哦!)
小编都会尽全力给你们解答哒~
以上是 【Java】Java虚拟机内存模型及回收机制 的全部内容, 来源链接: utcz.com/a/103464.html