随便说说JVM

去看JVM?没病吧,又不是去面试。哇,又要去面试了,又得重新去背JVM了。这就是我两年前的状态。最近又翻了翻JVM,发现有点理解了。

java是一门高级语言,是由C编写的。现在想象如果你会C语言,要你设计java语言,你会如何设计?

这个话题可能太长了啊,那就如何运行java类吧。给你一个经过校验,并且成功编译了的类,你该怎么用C语言设计这个类的运行功能?

首先,我们是不是要有个地方存储这个类的信息?类名啊,类的方法啊等一些元数据,还有类级别的静态数据,我们就把存在方法区吧。

然后呢,我运行类的方法,这时候我们去方法区找到这个类的方法,去运行它。别想的很高级,无非就是运行一些自定义可识别的字符码,从上至下,一行行的运行。

如果单线程就简单了,从上往下运行就够了,但如果是多线程,我这个线程运行这个方法到这里,我那个线程运行方法到那里,这就会出现混乱。为了解决这种问题,我们是不是要引入一个计数器,记录每个线程当前运行的位置,这个就是程序计数器,你是不是还理解它为什么是线程私有的了,而且存放的就是个整数,所以占用内存也不是很大,我们很少关心它的优化。

现在呢,我们可以往下运行了吧,运行的过程就是对数据处理的过程。方法中会有变量等一些数据,我们就是对这些数据进行处理得到想要的数据。先举个例子,java写个程序计算1+2+3*(4+5),这个需要用到数据结构栈。先把1推入栈,+是符号先保留在一边,然后把2推入栈,这时候还不能计算1+2=3,因为你不知道2后面是什么符号,万一是个*是不是就错了。所以2入栈后不急着计算,再读下一个符号,发现是+,和前一个符号平级,那1和2可以先相加得到3,然后继续往下执行。这个场景没写过的自己写写把,经典的栈应用场景。我们这个运行过程和这个计算过程一样,也是这样计算的,运行过程就放在虚拟机栈(虚拟机栈和程序计数器一样也是线程私有的)。这些1啊,2啊这些数据是存在虚拟机栈里面吗?虚拟机栈会有一张局部变量表,用于存放本次运行的基本类型数据和 引用类型数据的引用地址 。而引用地址指向的实际数据是存放在java堆里面的(标签1)。当方法执行完,局部变量表也就删除了。当你方法一不小心写了一个死循环,那么可能就会抛出StackOverflowError或OutOfMemoryError。如果设置的虚拟机栈内存少的话,就会抛出OutOfMemoryError;设置大的话,就能多运行几次循环,直到超过栈的深度就会抛出StackOverflowError。

对于一些特殊功能,我们直接用C语言实现,然后提供方法入口给java直接调用。这些由C语言直接写的方法就存放在本地方法栈。实现机制和虚拟机栈一样。

接上标签1,虚拟机栈是私有的,成千上万的线程前前后后的调用方法势必会产生很多引用类型数据(对象,数组)。所以内存消耗比较大,这一块就叫做java堆。如此多的内存开销如果只增不减,地球上就没有服务器可以支撑了。所以我们引入垃圾回收器

我们该如何设计垃圾回收算法呢,怎么标明是垃圾呢?

1.垃圾的定义是未被其他对象引用,那么我们给对象计数怎么样。对象被引用一次就加1,引用消失了就减1。很简单的实现原理对吧,这就是引用计数法。但是会有个问题。比如三角恋,A引用了B,B引用了C,C引用了A。这三个的计数永远不可能为0,永远不能回收。

2.我们从根开始进行标记,标记可到达的对象,那么这些对象就还存活着,不是垃圾,过程就像遍历数据结构树一样。那么未被标记的就能被清除了。这就是标记-清除算法。但这个有个问题,垃圾对象可能在内存任何的位置,它不是连续一整块。我们清除了垃圾对象,得到的空闲内存不是连续性的,如果一个大数组进来,它要求分配一块大的连续内存,你就会发现我有这么多的空闲内存,但我没合适的内存分配给它。

3.基于2,我们继续改进,我把内存分成两部分,一部分使用,一部分空闲。对使用的内存进行垃圾回收时,把存活的对象复制到另一块空闲内存上。而且给它们分配的内存是连续的。那么剩下的内存是不是就连续了,那么这一部分内存就变成了使用中,原先使用中部分的内存回收完垃圾就变成了一完整的空闲内存。这样2中的问题就得到了解决。这就是复制算法。但是这个有个明显的缺点,而且缺点挺大的,一个2G的内存被你玩成了1G,一个只要2000块钱的服务器你要去买4000块钱的。

4.基于2,换种改进思路,如果我清除后垃圾对象后,我再花点性能把存活对象给搬到一块连续的内存上,那么剩下的内存不也是连续可用的了吗,这就是标记-压缩算法。这种算法是不是就完美了呢?我认为精益求精的程序就没有完美的,一般都是空间换时间,时间换空间。复制算法我们是用空间换时间,它回收时间快,它只要把存活对象给复制到另一块空闲内存上,程序就可以使用了,不必等垃圾对象回收完,所以存活对象越少这种算法的速度就越快。那么标记-压缩算法,它就要做两件事,一个是要回收垃圾对象,一个是将存活对象重新整理到连续内存,这个肯定更耗时间,但它却节省了空间。

5.没有完美的算法,只有合适的算法,对于不同的情况采用不同的算法,这就是分代算法的基本思想,也是虚拟机采用的回收算法。

5.1我们知道,java项目运行,对外暴露的都是接口,成千上万的用户使用接口,就是使用接口里的方法。那么方法里面的对象,数组所需的内存是不是很庞大。但是它又是短暂的,一个方法执行的时间能有多长。所以产生垃圾对象很快,垃圾对象很多,面对这种情况,我们上面的复制算法是不是就可以适用这种场景。虽说它耗内存,但程序里就没有完美的算法,只有趋向完美。

5.2上面说的复制算法是2G变1G,那么一块内存是五五分。现在我们把它八二分,八用于使用,二用于存放存活对象,但它们不交替了。首先程序生成的对象都到八里面去,垃圾回收时把还存活的对象复制到二里面去,再八清空对象,java项目继续执行。那么二怎么办呢,它的对象就不管了吗,这不是有增无减了吗?

5.3其实二也是采用复制算法,五五开,分为from区和to区。5.2改进,八虚拟机中的名称就是eden区,二就是survivor区,survivor区五五开分成from区和to区,eden和survivor整体叫新生代。先eden进行垃圾回收,存活对象先复制到to区,eden区再清除垃圾对象。接着survivor区进行垃圾回收,from区进行标记存活对象,并计算它们的存活年龄(GC次数还存活),存活年龄达到阀值还存活,此对象被复制到老年代,没达到的存活对象复制到to区,from区再清除垃圾对象,from区内存全部空闲了,可用对象都在to区了,最后把to区的存活对象全部复制到from区,to区为空了。理解整个过程,你就会发现新生代的回收过程完美循环了。这个过程叫Minor GC。上面说的from区存活对象除了年龄达到阀值可以进入老年代,还有当from区满了里面所有的存活对象都会进入老年代。这里说的满了不是说100M内存要100M才满,它会有一个比例,默认50%,也就是说满了50M就会进入到老年代,这个可以设置高点,但也就意味着存活对象多了,复制算法就变慢了。空间和时间自己选择。

5.4老年代就一直增加吗,当然不是。当老年代满了,就会触发FULL GC。老年代回收算法采用的是标记-压缩算法,这种算法上面介绍了,空间和时间综合性价比强。FULL GC所需的时间更长。

系统GC会把其他线程统统挂起来,整个项目停止运行,所以GC的时间越短越好,所以FULL GC次数应该尽量最少。也就是老年代对象尽可能少而且稳定,这就要求Minor GC尽量大点,然后我们写的程序应该运行时间短点,时间短经历的GC次数就少,就会在进入年纪阀值前被回收。

此次先这样把,个人网站: 天才战士93网

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

3wtczs93点抗母

钱癌晚期

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值