JVM总结

本文详述了JVM的垃圾回收机制,包括GC的过程、算法以及几种主要的收集器(CMS、SERIAL、G1)。同时,文章讨论了运行时内存分区、JMM内存模型、类加载机制,并介绍了性能调优工具如jps、jstat等的使用。重点讲解了GC的分代收集、可达性分析以及各种引用类型。此外,还提及了finalize方法的作用和使用注意事项,以及各种垃圾回收算法的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近一直在持续看JVM的相关知识(每天睡不着,所以看着看着就看困了)

本文将主要涉及几个方面:1.GC的过程   及算法

                                         2.GC主要的几个收集器:CMS  SERIAL   G1    

                                         3.运行时内存分区及JMM(内存模型)

                                         4.类的加载机制

                                         5.性能调优的工具:jps   jstat   jmap  jinfo   jstack

  一. 首先来说一下GC                 

        java 是一个面向对象的高级语言, 相比与同样是面向对象的C++  有着一定的优势.  他可以免除开发人员自己对无效对象的清理,而实现这一优势最主要的原因就是 JVM来将这个事情给做了.  下面来简述一下GC的工作过程

       首先我们要了解到的是 GC是一个后台的守护线程. 也就是说,会随着我们运行的主类的结束而死亡.

       GC的分代收集分为:年轻代、老年代、永久代。(方法区是被当做永久代的,不过JDK1.6后将被取消掉了)年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)

       GC触发条件:Eden区满了触发Minor GC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor GC之后,就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。

     GC的核心是对对象进行一个可达性分析,通过逆向寻找某个对象的引用来判断一个对象是否有用.  值得一提的是Java中的引用(reference) 分为  强引用(Strong)  软引用 (soft) 弱引用 (weak) 虚引用()  强引用就是我们经常使用的  new obj(); 这种形式.  软引用会在内存不足的时候被回收     虚引用如果不被回收 ,那么引用他的对象永远不会被回收,需要主动调用clear 方法来释放.    弱引用在GC时一定会被GC回收

     finalize 是位于Object类的一个方法,该方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易访问到这个方法。由于,finalize函数没有自动实现链式调用,我们必须手动的实现,因此finalize函数的最后一个语句通常是 super.finalize()。通过这种方式,我们可以实现从下到上实现finalize的调用,即先释放自己的资源,然后再释放父类的资源。 
根据Java语言规范,JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。另外,规范还保证finalize函数最多运行一次。 
很多Java初学者会认为这个方法类似与C++中的析构函数,将很多对象、资源的释放都放在这一函数里面。其实,这不是一种很好的方式。原因有三,其一,GC为了能够支持finalize函数,要对覆盖这个函数的对象作很多附加的工作。其二,在finalize运行完成之后,该对象可能变成可达的,GC还要再检查一次该对象是否是可达的。因此,使用 finalize会降低GC的运行性能。其三,由于GC调用finalize的时间是不确定的,因此通过这种方式释放资源也是不确定的。 
通常,finalize用于一些不容易控制、并且非常重要资源的释放,例如一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。在这种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源方式为辅,形成一种双保险的管理机制,而不应该仅仅依靠finalize来释放资源。 

   最后简单说一下几个gc  minor gc , full gc 等. 当对象创建时候,会被优先分配在eden区域(新生代),经过了几轮的gc后会进入到老年代      当然这只是一般对象,当对象足够大的时候会直接进入survivor(老年代,这个足够大需要结合JVM参数配置)       默认的新生代与老年代在内存中的内存配比是8:1:1.(两个老年代的分区)    当新生代满了之后 会触发minor gc.   当老年代满了会抛出 outOfMemeryError .

一(2) .  垃圾回收的算法

   目前我了解到的几个算法 标记清除算法     标记整理算法     复制算法   可达性分析     

  标记清除: 顾名思义   会对垃圾对象进行标记  然后对其进行清除  , 缺点是会造成空间碎片.比如清除后的内存区域是不连续的15M, 但是最大的连续区域是5M   那么当一个6M的对象被创建的时候,则无法创建在该区域.

  标记整理: 是标记清除算法的一个优化, 会定期将清理后的内存区域进行整合,使现有的对象集中放置.会避免空间碎片的产生.适用与存活对象较多的区域  如老年代survivor

   复制算法:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收.  不适用与老年代. 复制算法是一种比较高效的方法,但缺点是会浪费内存空间(以空间换时间)

 

二.GC的几种垃圾收集器

    年轻代收集器包括Serial收集器、ParNew收集器以及Parallel Scavenge收集器。老年代  Serial Old收集器、Parallel Old收集器以及CMS收集器。   还有一个最新的收集器   在JDK7后开始广泛使用  G1

   serial收集器是一个串行的收集器,也是最为古老的垃圾收集器.   最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法老年代标记-压缩;垃圾收集的过程中会Stop The World

   ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

   Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩

  Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

  CMS 是B/S架构中使用最为广泛的收集器              采用标记清除算法  可能会造成空间碎片  但是因为他会并行清除  所以效率还是很高的   适用于  老年代   新生代一般是parnew

   G1  收集器 是最新的研究成果   最大的优势是 空间整合和可预测停顿     详细可参考

    https://www.cnblogs.com/ityouknow/p/5614961.html

 

三. 运行时内存分区和JMM(内存模型)

  一般的来说,大家都会把内存分为堆内存和栈内存.  其实这样只是粗略的划分.  在我们java程序运行的时候 , 其实会有更详细的划分.   首先我们会把一个程序的运行分为 共享区和独占区.  在共享区中 就包含了我们常说的堆内存 和 方法区( 其实方法区也是存储在堆内存上的,只不过因为其存储的内容不同 将其单独划分) ,通常意义上来讲的堆  就是我们存放对象的地方, 而方法区存放的是静态变量信息,常量池,类信息等.   在另外一方面  独占区 会分为: 栈内存  寄存器(程序计数器) 和本地方法栈(native stack) .

 所谓独占区的意思就是当我们每执行一个线程的时候,都会未其分配独有的独占区,是不和其他线程共享的.  简单解释一下 寄存器这个东西, 我们在线程调用的时候 难免会有一些线程出现等待的情况,这个程序计数器就是用来记录当前线程执行的情况,可以理解为当前字节码文件执行的行数.   而栈内存中存储的就是一些栈信息 (采用了stack 的结构  first in last out ) ,本地方法栈现在还不是太明白   有看到的大佬可以留言告知   感谢~

 

JMM: 在我们多线程编程中,每一个线程都不会对堆内存进行直接操作.  而是采用了线程副本的机制,每次操作会从堆内存中进行复制,然后对线程副本进行操作   在复制到堆内存中. 这样的话  线程一和线程二是不会互相感知到彼此的存在.而线程间的通信也是靠堆内存来完成的. 在之前的开发过程中曾经踩过这个坑   当时在线程类中new 了一个 concurrent hashmap  希望以此来存储一个KV的值供另一个方法调用,由于没有考虑到添加的时候是多线程  而获取的时候是单线程(其实相当于new 了 三个 juc hashmap) 所以会有一部分值取不到.  最后选择持久化到了redis 来完成这个设定.   

四. 类的加载机制

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。 参考

http://www.importnew.com/25295.html  

五.性能调优的工具:jps   jstat   jmap  jinfo   jstack

  jps: 类似于linux 的ps 指令   返回当前执行的进程号   可以使用jsp -l 来查看进程号及方法名称   

        其他几个命令都需要进程号的参数  所以  jps是基础

  jmap:   jmap -histo pid      pid 为进程号   

             持久代 jmap -permstat pid

   jstack: 用来输出 某个进程的堆栈信息

              jstack [option] pid

              jstack [option] executable core

              jstack [option] [server-id@]remote-hostname-or-ip

   jstat: JVM统计工具      

jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

 另外还可以通过  jconsole 这个可视化工具  查看到内存的动态变化  会有个线性的图动态展示

 

   

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值