再来一遍 JVM

首先我们说一下内存管理,因为所有程序都是运行在内存之上的,有的程序需要自己管理运行内存比如C语言,有的程序就是半自动管理运行内存,例如 Java,自动的东西 当然是完成了80~90%的工作,而剩下10~20的管理就没有管理的那么细致了,所以JVM内存的管理,上手简单,说白了,也就是不需要你了解多少东西,就能直接用。那么下面我们就来看看JVM内存自动管理的那部分属性状态吧。

JVM内存结构

          堆,栈,方法区,程序计数器    其中栈呢又包括两个部分{虚拟机栈,本地方法栈},虚拟机栈我更乐意叫它 "线程栈"

          程序计数器:就是为了记录当前线程字节码指令执行到了什么位置,因为在处理器中每一个时间点是只能执行一个线程的,多线程的运行在于线程的来回切换,线程切换回来你得知道线程运行到了什么位置,下面该要执行哪个字节码指令了。程序计数器,做的就是这个工作。

          虚拟机栈:这东西也是线程自己的,声明周期和线程一样,运行每一个方法的时候,都会创建一个栈帧,方法在运行时间所需要的入参,以及方法中变量的计算赋值,这一系列动作都是在栈帧里面进行的,当方法执行完毕,再进行出栈动作,将计算结果返回给调用者,或者直接不返回,方法在编译期间就已经确定了要在栈帧中分配多大空间。

          本地方法栈:其实跟虚拟机栈是基本类似的,虚拟机栈是为了JVM中的方法执行提供记录与存储的,本地方法栈就是为本地方法执行提供方便的呗。

          堆:堆,存放的最多的还是对象信息,在堆里面存放不同的对象,这时候就跟内存的自动管理挂钩了,也就是堆要完成对于堆中存放的对象的自动管理,也就是自动的销毁与创建或者换位置等等,按照 "代" 收集理论,主要分为 新生代、老年代、永久代、 Eden 、Survivor区。

          方法区:对于所有线程也是共享的,方法区不是永久代,只是用永久代的概念来设计方法区内存的回收。现在基本上已经放弃了永久代这么个说法,改用本地内存来实现方法区(元空间),所以以后来说你的本地内存多大,你的方法区就有多大。

          运行时常量池:运行时常量池其实也是方法区的一部分,主要存放编译期生成的各种字面量与符号引用,在类加载之后,将这部分内容放在方法区里头。

          直接内存:直接内存的使用主要是为了提高程序运行效率,减少磁盘数据到内存数据直接的来回复制以及线程上下文的频繁切换,减少程序运行时间,但是开辟直接内存代价还是很高的。

      对象的创建过程:

  • 在new的时候,会去检查执行new指令所带的参数能不能在常量池中找到,如果能在常量池中找到就证明类已经被加载,不用再次加载类。
  • 类加载检查之后,类在内存中需要占用具体多大内存,就已经知道了,就开始着手给新对象分配内存,分配内存就看堆中的内存是不是足够,是不是连续的,内存的连续与否就看内存的回收算法是怎样的了。
  • 也可以通过线程的本地缓冲区进行创建对象的存储。

对象中的内容主要包括两个方面,一个是对象自身存储的运行时数据,另一个是对象的状态数据,比如对象的锁状态标志,偏向线程ID

对象的访问定位

1. 通过句柄访问对象

  如果通过句柄访问对象的话,栈中存储着对于句柄池句柄的引用,句柄池中存储这对于对象实例的引用和对象类型的引用

2.直接通过引用来访问实例数据

  通过引用直接指导堆中的实例数据,实例数据中存储这对于对象类型的引用,

使用直接指针的好处:访问速度快,很少通过句柄访问的,因为多了一层定位过程。

 

栈的深度指的就是栈帧的创建个数,一次循环方法的调用,就能在一个线程的栈中不停地创建栈帧,然后压入到栈中,有点类似于 hashMap的桶。

 

 

垃圾收集器与分配策略

如何判断一个对象该不该在下一次GC的时候就行回收

1.引用计数法

    如果我要回收它,我就给他盖个戳

2.可达性算法

以GC-Root为根节点,如果能在这颗引用树上的都不回收,剩下的都回收

GC-Root的对象

   虚拟机栈中引用的对象

   方法区中类静态属性引用的对象

   方法区中常量引用的对象

   本地方法栈中引用的对象

   被同步锁持有的对象

 

  对象死不死,判断就是要执行两次标记过程,第一次发现在引用链上没有它,就标记它,随后再进行一次筛选,条件就是有没有实现finalize方法,如果没有的话,系统就判断这个对象死了,如果说实现了finalize方法,系统会执行这个方法,对象想要复活  就在finalize方法中跟GC-Root下的对象挂上勾,这样就死不了了。

方法区回收:

    方法区回收的主要对象就方法区里面存放的东西,你说这不是废话么。那你就说说 方法区里面放了什么? 常量和类型嘛

   常量回收就很简单啦,只要我虚拟机里面没地方调用你,并且垃圾回收器判断有必要的话(至于垃圾回收期怎么判断有没有必要 Enn....)就把你回收掉。

    类型的回收就比较麻烦了,

这个类的所有实例都已经被回收了,虚拟机里面再没有了
该类的类加载器也被系统回收了
该类对应的Class对象没有任何地方引用了

 满足上述条件,只能说你这个类型具备被回收的条件了。虚拟机提供了这个参数 -Xnoclassgc 来判断是否要回收

类型的回收主要是在动态生成的类,反射生成的类 JSP文件的编译生成的类,这些类型生命周期短,一般都是要及时回收的,从而保证方法区的压力不是太大

垃圾回收算法

      1.标记清除 大法

               我给要回收的对象盖个戳,在下一次GC的时候就把你回收掉,但是没有回收的对象依旧保持自己的内存位置,这样会导致内存中太多碎片。

       2.标记复制 大法

               标记-复制 大法 主要就是把整个堆内存分为相等的2块,一块用来做备份space1一块用来放新生成的对象space2,一旦space2满了,我就把space2里面的存活的对象全部放到space1里面,然后把space2清空。这样的好处是有大块的连续的内存空间了,但是空间的利用率不高,你想嘛 一半都用来做备份了,能高么。

       3.标记整理 大法

              将所有的存活的对象都移动到内存的一端,在GC的时候直接把存活对象之外的对象直接干掉,这样我空间利用率也高了,并且我还有足够大的连续内存空间,可以用来分配大的对象。但是缺点也是很明显滴,每次内存中存活对象的移动都会导致栈中更新对象的引用,这样做要停下来所有的工作用来进行对象的移动。并且像活跃对象多的地方-老年代也要进行这样的大操作,会导致系统间歇性的卡顿。

 基于以上的方法,我们可以看出,要么 我们不移动对象,但是系统内存碎片化太严重,要么我们移动对象,这样会导致系统卡顿,那么有没有我们既要移动少量对象用以腾出来大的内存空间,并且不会导致系统卡顿的方法呢? 肯定有嘛,不然怎么会这么问。

       4.分代回收

               分代理论主要是将虚拟机中对象的存储分为 年轻代,老年代  年轻代又分为  Eden区 Survivor区  Survivor区分为两个

Survivor1 Survivor2.  E:S:S=8:1:1  ,其中s2永远是空的,用来存放GC之后的剩余的在Eden区中存活的对象(是不是有点怪,既然是空的为什么还要存放对象呢? 因为  他们角色扮演,会互换角色)

               新生成的对象首先默认在Eden区分配,在进行GC的时候Eden区的存活的对象进入到s2, 这时候判断s1里面的对象是否要进入老年代,判断完了之后会把s2的剩余的对象全部复制到s1里面,同时清空s2 保证下次GC的时候,eden区中存活的对象直接扔到s2里面。就这样一遍一遍的GC循环,当s1里面的对象填满的时候,直接把s1的对象全部扔到老年代。

              对象什么时候会直接进入老年代呢。

             1.对象过大的时候,因为如果对象过大,对象在年轻代里面GC的时候会大量的进行复制,占用系统资源,这时候我们直接扔到老年代,避免大对象的复制。

             2.设置对象进入老年代的年龄限制。就像不慢18岁不能进98一样,我修改这个门槛,让你8岁就能进98.

             3.当s1里面相同年龄的对象占s1内存的一半以上,我就直接让这部分以及年龄比这部分大的对象进入老年代,比如,规定18岁不能进98,结果全国一半的人认为这个规定不合理(半数原则嘛),那我没办法,只能降低98年龄限制,放你们进来。

空间分配担保:

       说白了就是为了避免Survivor里面放不下新生对象的情况,因为新生代的内存分配是8:1:1嘛  ,如果我的8 里面经过GC之后发现,我活着的对象还占4成,那我的Survivor1里面肯定是放不下的啊,这时候就要有一个区域来保证我的活着的对象有地方放,那老年代的内存区域就是我的保障,我直接把对象丢到老年代就行了。但是   这样有风险。老年代也放不下了就凉凉,只能进行Full GC,也就是我放不下了,大家都别干活了,先把没用的清出去,让我有地方放。

 

排版不好看,我是个粗人,先将就着看吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章