JVM系列整体栏目
| 内容 | 链接地址 |
|---|---|
| 【一】初识虚拟机与java虚拟机 | https://blog.csdn.net/zhenghuishengq/article/details/129544460 |
| 【二】jvm的类加载子系统以及jclasslib的基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/129610963 |
| 【三】运行时私有区域之虚拟机栈、程序计数器、本地方法栈 | https://blog.csdn.net/zhenghuishengq/article/details/129684076 |
| 【四】运行时数据区共享区域之堆、逃逸分析 | https://blog.csdn.net/zhenghuishengq/article/details/129796509 |
| 【五】运行时数据区共享区域之方法区、常量池 | https://blog.csdn.net/zhenghuishengq/article/details/129958466 |
| 【六】对象实例化、内存布局和访问定位 | https://blog.csdn.net/zhenghuishengq/article/details/130057210 |
| 【七】执行引擎,解释器、JIT即时编译器 | https://blog.csdn.net/zhenghuishengq/article/details/130088553 |
| 【八】精通String字符串底层机制 | https://blog.csdn.net/zhenghuishengq/article/details/130154453 |
| 【九】垃圾回收底层原理和算法以及JProfiler的基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/130261481 |
垃圾回收篇底层原理及相关算法
垃圾收集,并不是Java语言的产物,早在1960年,第一门使用内存动态分配和垃圾收集技术的Lisp语言诞生。垃圾回收机制也是Java的招牌能力,极大地提高了开发效率。因此在面对垃圾回收时,需要解决三个主要的问题:哪些内存需要回收、什么时候回收、如何回收?

垃圾:指的是在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如创建了某个对象,引用该对象的变量是存储在虚拟机栈的栈帧中,随着入栈出栈该栈帧被销毁,那么栈帧中引用该对象的局部变量变量也被销毁,此时没有任何变量引用着刚刚创建的对象,那么该对象就会变成垃圾,等待回收。
首先如果垃圾不回收,内存很容易被消耗完,在面对一个大系统的时候,没有GC就很难保证应用程序正常运行。
如果不及时的对内存中的垃圾进行回收, 那么这些对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至会出现内存溢出的情况。
java内部通过自动内存管理的方式,无需开发人员手动参与内存的分配和回收,这样可以降低内存泄漏和内存溢出的问题,从而省去繁重的内存管理,可以更加专注的进行业务开发。
然而自动内存管理就如同一个黑匣子,如果过度依赖自动化,那么就会弱化开发人员在程序出现内存溢出时定位问题和解决问题的能力。因此当需要排查各种内存溢出、内存泄漏等问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对这个垃圾回收进行必要的监控和调节。
在进行垃圾回收时,主要针对的是Java堆的这个区域。从次数上来说,频繁的收集Young区,较少的收集Old区域,基本不动方法区(永久代或者元空间)
在这垃圾回收的算法中,主要是做了两件事情:一件是找出垃圾,另一件是清除垃圾。
垃圾标记阶段主要就是找出垃圾,用于判断对象是否存活。堆中几乎所有的对象都存储在堆中,在GC执行垃圾回收之前,需要先区分哪些是存活的对象,哪些是已经死亡的对象。只有被标记已经死亡的对象,GC才会在执行垃圾回收时,才会释放掉其占用的内存,这个阶段就被称为垃圾标记阶段
在JVM内部,判断对象是否存活主要是通过两种方式:引用计数算法和可达性分析算法。
这种算法就是会为每一个对象保存一个引用计数器属性,如对于一个对象A,只要有任何一个对象引用了A,那么这个A对应的计数器就会加1,当引用失效计数器就会减1。只有引用计数器的值为0时,表示不再被任何变量引用,那么该对象就会被标记,后续会根据是否标记进行回收。
优点是实现比较简单,垃圾对象便于识别,并且其效率比较高;缺点是需要单独的字段存储计数器,需要一定的空间开销,并且计数器需要加减运算操作,增加了时间开销,最主要的是无法处理循环引用的问题,因此Java并没有选择这种算法。
public class A{
public A a = null;
List<A> list = new ArrayList();
public static void main(String[] args) {
A objectA = new A();
A objectB = new A();
//这两个对象相互引用,俩个计数器都+1,导致无法回收
objectA.a = objectB;
objectB.a = objectA;
}
}
相对于引用计数器而言,可达性分析算法不仅具有简单和执行高效等特点,更重要的是可以有效地解决这个循环引用的问题,从而防止出现内存泄漏。

通过上图可知,可达性分析算法是以根对象集合为起点,按照从上到下的方式搜索根对象集合所连接的目标是否可达,内存中的存活对象都会被根对象集合直接或者间接连接着,如GC roots和obj1是直接连接着的,和2,3,4是间接连接着的,这整个连接路径被称为引用链。如果目标对象没有任何引用链相连,如6,7,8,则是不可达的,就意味着该对象以及死亡,可以标记为垃圾对象。
在java中,常被用作GC对象的有以下几种:方法中的参数、局部变量、静态属性、字符串常量池引用、同步锁持有的对象、基本数据类型对应的Class对象、异常对象、本地缓存等等。
在使用这个可达性分析算法来判断内存是否可以进行回收,在分析工作时必须在一个能保障一致性的快照中进行,不允许在分析时出现动态的垃圾的增加等,这样才能保证分析结果的准确性。但是也正是因为这个快照的问题,让stw的出现不可避免。
查看GC Root有好几种方式,如使用MAT等,但是MAT要涉及到eclipse这些,因此废弃MAT。这里主要是通过这个 JProfiler 这个工具,可以直接在idea中搜索这个插件安装,然后重启idea即可。

除了这里安装之外,最好再安装一个 .exe 的可执行文件,可以参考黄莹这位大佬写的,这里面有免费JProfiler下载: https://blog.csdn.net/weixin_42311968/article/details/120726106 下载完之后一直点下去,安装,我这里是先使用免费10天的。
安装完成之后,再回到idea,点击右上角的JProfile的图标

然后在提示框中,输入刚刚 .exe安装路径下面的bin目录下面的 .exe,然后保存即可,再次点击就可以直接使用了
//如我这边安装目录是在D盘下,因此找到这个路径下面的 JProfiler.exe 文件即可
D:\environment\jprofiler\jprofiler11\bin
然后下一步也是那个大佬里面写的,将JVM exit action的参数改成如以下图所示即可。

再点击ok之后,那么这个画面就有了,工具基本就可以使用了

一段时间之后,就会出现如下画面,其画面是动态的

在Live memory下的All Objects中监视着所有的内存情况,和之前谈到的JVisualVM的功能都是类似的,如之前谈到查看这个字符串常量池到底存在哪就是通过这个方式举的例子。

如果出现OOM,可以直接通过查看这个Heap Walker目录下的对象信息,来确定是哪个对象发生的OOM,以及是否出现大对象等问题。
上面讲述了两种方式找出垃圾,接下来就需要将找出的垃圾给清除掉,释放无用对象的内存空间,以便有足够的可用内存为新对象的分配。在jvm中,比较常见的三种垃圾回收算法有:标记清除算法、复制算法、标记压缩算法
当堆中有效的内存空间被耗尽的时候,会停止整个程序,简称stw(stop the world),这样可以防止在标记回收的和清除的时候又有新的垃圾出现。
在这里面主要进行两箱工作,第一项是标记,第二项是清除。标记是从根节点开始,标记所有的引用对象,一般是可达对象;清除是从头到尾线性遍历,发现某个对象没有标记为可达对象,则将其回收。

如上图,绿色部分表示存活对象,黑色表示垃圾对象,白色表示空闲对象,然后从根节点出发,判断对象是否可达,从而确定对象是否需要被回收,最后将黑色对应的对象回收即可,从而实现这个标记清除算法。
该算法的优点是简单易理解。但是缺点也很明显,效率较低;并且在GC的时候,需要停止整个应用程序(stw),用户体验差;最主要的是会产生大量的内存碎片,因此在内部需要维护一个空闲列表。这里的清除并不是直接将对象清除,而是将要清除对象的地址加入到空闲列表里面,然后记录指向要被清除对象的指针,后面来新的对象之后,则将这个空闲列表记录的指针指向新来的这个对象。
针对标记清除的算法的缺陷,如会产生内存碎片,因此这种复制算法诞生。
其核心思想就是将内存空间分成两块,每次只使用其中的一块,在垃圾回收的时候将正在使用的内存中的存活的对象复制到未被使用的内存中,之后再将正在使用的内存中的对象清除,再交换两个内存的角色,最后完成垃圾回收

这里的复制是将完整的对象复制,在新生代中存活区的s0区和s1区就是实现了这种算法,经典以空间换时间。并且在后期分配对象时,可以直接使用指针碰撞算法。
复制算法的优点是:没有标记和清除的过程,简单实现,运行高效,并且解决了碎片化的问题。
复制算法的缺点是:需要两倍的内存空间,如果垃圾多,那么需要移动的次数也多,影响效率。
复制算法效率虽然高,但是更加的适合新生代中使用,因为那里的对象朝生夕死,那么需要复制移动的对象就不会太多,也就不会太影响效率。但是复制算法不适合在老年代使用,因为里面的对象基本是存活的,那么真要复制移动起来,那么就会严重影响效率,那么这种算法的代价就会比较高。
而标记清除算法会产生垃圾碎片,显然也不能在这个老年代使用,因为如果出现一个大对象这就可能出现放不下的情况,因此就出现了一种新的算法,标记整理法。这种算法就是在标记清除这种算法之上进行了优化的一种算法,从而解决这种内存碎片的问题。

这种核心思想就是通过根节点开始标记所有能被引用的对象,然后将所有存活的对象按顺序排放,再清理掉没被引用的垃圾。标记整理算法的最终效果就是等同于标记清除之后,再进行一次碎片管理,一次也可以称为 标记-清除-压缩 算法,该算法也不需要使用空闲列表来记录碎片。
标记整理算法的优点是:解决标记清除碎片化问题,复制算法空间问题
标记整理算法的优点是:效率低于复制算法,移动对象的同时需要移动对象引用,移动过程需要stw
从效率上来说,复制算法的效率最高,但是会浪费大量的内存。因此兼顾速度,空间开销,是否需要移动对象三个指标来说,标记整理算法相对来说更为稳定,但是效率上不尽人意,因为他比复制算法多了一个标记阶段,比标记清除算法多了一个整理内存的阶段。
默认情况下,通过这个System.gc()的调用,就会显式的触发这个Full GC,同时对这个老年代和新生代进行回收,尝试释放被丢弃的对象所占用的内存。
但是这个System.gc()会调用一个附带的免责声明,就是说这个确实可以触发这个Full GC,但是垃圾回收器会不会做出具体的响应,已经响应的时间是否即时很难保证,有可能并不会响应这次请求,也可能在很长时间后才触发,导致想清除的对象复活或者迟迟不能回收导致OOM的情况。
垃圾回收一般是可以自动进行的,无需手动触发,否则就太过于麻烦了。但是在测试性能的基准的时候,可以在运行期间调用这个System.gc()。
/**
* @author zhenghuisheng
* @date : 2023/4/19
*/
public class Test {
public static void main(String[] args) {
User user = new User();
//提醒Jvm进行垃圾回收
System.gc();
//强制执行
//System.runFinalization();
}
//触发了垃圾回收就会调用这个finalize()
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("触发了垃圾回收");
}
}
通过上述代码可以得知,在System.gc()之后,有时会触发有时不会触发这个full GC,但是使用这个System.runFinalization()方法时可以保证一定会触发这个 FULL GC的,因为他是强制执行的。
由于GC的技术一直不断地完善和发展,因此在一般的情况下是不会出现OOM的,除非应用程序所占的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,这样才可能出现OOM。GC会进行各种年龄段的垃圾回收,在出现OOM之前也会触发一次FULL GC的操作,这时候会回收大量的内存,如果在回收大量的内存之后还不够用,那么就会出现OOM的问题。在java文档中对OutOfMemoryError的解释是这样的:没有空闲内存,并且垃圾收集器也无法提供更多的内存。
而内存不够的原因,有可能是Java虚拟机堆内存设置的不够;也可能是创建了大量的大对象,并且对象被引用着,不能被回收,或者对象的大小直接超过堆内存的对大值。
指的是对象不再被程序使用,但是GC又回收不了这些对象,就被称为内存泄漏。如一些静态对象,其生命周期比较长,但是这些对象关联了一些只用一次的对象或者一些资源对象,而资源对象没关,如mysql连接等,总而导致出现这个内存泄漏。
简称STW,指的是在触发这个GC时间之后,会产生程序的停顿,就是会让全部的用户线程暂停,没有任何响应,有点像卡死的感觉。在触发这个STW时,需要保证其工作在一个快照中进行,从而保证数据的一致性,如果在分析过程中出现对象还在不断的动态变化着,则最后的分析结果的准确性很难保证。
在被STW中断的应用程序会在GC之后恢复,然而频繁的中断会让用户感觉到卡顿的情况,让用户的体验不友好,因此在后续的优化中,STW就是重点要关注的对象。并且STW是在后台自动的发起和自动的完成的,会在用户不可见的情况下,强行的把用户正常的线程给全部停掉。因此在开发中也要少用System.gc(),否则也容易触发这个STW。
当内存空间还足够时,可以保留在内存中,如果内存空间在垃圾收集之后还是很紧张,则可以抛弃这些对象,这些对象就被称为引用。
强引用(StrongReference):指在代码中普遍存在的引用赋值,如通过new一个对象,无论在任何情况下,只要强引用的关系还在,垃圾收集器就永远不会回收掉引用的对象。强引用的对象基本是可触及的,即rootGc是可达的,同时强引用也是内存泄漏的主要原因之一。
StringBuffer sbu = new StringBuffer("zhenghuisheng");
软引用(SoftReference):在系统将要发生内存溢出之前,会将这些回想列入回收范围之中进行第二次回收,如果第一次回收之后(回收GC Root不可达的对象)还没有足够的空间,那么第二次回收就会回收这些对象,第二次回收之后还是内存空间不足,那么就会抛出内存溢出的异常。如一些缓存,就是典型的使用了软引用
//声明一个强引用
Object obj = new Object();
//实例化一个软引用
SoftReference<Object> sf = new SoftReference(obj);
obj = null;
弱引用(WeakReference):只被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾回收器工作时,无论空间是否足够,都会被回收。弱引用也可以用来作为缓存使用
//声明一个强引用
Object obj = new Object();
//实例化一个弱引用
WeakReference<Object> sf = new WeakReference(obj);
obj = null;
虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚拟引用来获取一个对象的实例。设置一个虚引用关联的唯一目的是在这个对象被收集器回收时收到一个系统通知
//声明一个强引用
Object obj = new Object();
//引用队列
ReferenceQueue ReferenceQueue = new ReferenceQueue();
//实例化一个虚引用
PhantomReference<Object> sf = new PhantomReference(obj,ReferenceQueue);
obj = null;
总结来说:强引用默认是不回收,软引用是内存不足则回收,弱引用是发现即回收,虚引用是用于对象回收追踪
如若转载,请附上转载链接地址:https://blog.csdn.net/zhenghuishengq/article/details/130261481
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐
基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于
我的ruby脚本从命令行参数获取某些输入。它检查是否缺少任何命令行参数,然后提示用户输入。但是我无法使用gets从用户那里获得输入。示例代码:test.rbname=""ARGV.eachdo|a|ifa.include?('-n')name=aputs"Argument:#{a}"endendifname==""puts"entername:"name=getsputsnameend运行脚本:rubytest.rbraghav-k错误结果:test.rb:6:in`gets':Nosuchfileordirectory-raghav-k(Errno::ENOENT)fromtes
我使用irb。下面是我写的代码。“斧头”..“bc”我期待"ax""ay""az""ba"bb""bc"但结果只是“斧头”..“bc”我该如何纠正?谢谢。 最佳答案 >puts("ax".."bc").to_aaxayazbabbbc 关于ruby-从结束值创建一系列字符串,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7617092/
使用RubyonRails,我使用给定的增量(例如每30分钟)用时间填充“选择”。目前我正在YAML文件中写出所有的可能性,但我觉得有一种更巧妙的方法。我想我想提供一个开始时间、一个结束时间、一个增量,并且目前只提供一个名为“关闭”的选项(想想“business_hours”)。所以,我的选择可能会显示:'Closed'5:00am5:30am6:00am...[allthewayto]...11:30pm谁能想出更好的方法,或者只是将它们全部“拼写”出来的最佳方法? 最佳答案 此答案基于@emh的答案。defcreate_hour
使用facebook登录后,我被重定向到/#_=_,其中显示主页。这种垃圾也出现在其他URL中,例如当注册失败并被重定向到/users/sign_in#_=_为什么会发生这种情况,我该如何解决? 最佳答案 如果你真的不想要它,一些简单的javascript就可以了:if(window.location.hash=="#_=_"){window.location.hash="";} 关于ruby-on-rails-为什么Devise/Omniauth会向URL添加垃圾?,我们在StackO