JVM篇


JVM内存结构

线程私有

  1. 程序计数器:用来记录线程执行到那段代码
  2. 虚拟机栈:存储是局部变量和方法参数信息

    线程共享

  3. 堆:存的对象信息
  4. 方法区: 存储类的信息和方法的代码信息

哪些部分会出现内存溢出

不会出现内存溢出的区域-程序计数器

出现OutOfMemoryError的情况

  1. 堆内存耗尽-对象越来越多,又一直使用,不能被垃圾回收
  2. 方法区内存耗尽-加载的类越来越多,很多框架都会在运行期间动态产生新的类
  3. 虚拟机栈累积-每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁.

出现StackOverflowError

  1. 虚拟机栈内部-方法调用次数过多

方法区与永久代,元空间之间的关系

  1. 方法区是jvm规范中定义的一块内存区域,用来存储类元数据,方法字节码,即时编译器需要的信息等
  2. 永久代是Hostpot 虚拟机对jvm规范的实现(1.8之前)
  3. 元空间是Hotspot虚拟机对jvm规范的实现(1.8以后),使用本地内存作为这些信息的存储空间

JVM垃圾回收算法

标记清除:

首先找到GC Root这些不能被回收对象,然后清除没有GC Root 对象(cms在用这个方法,现在基本弃用了)
缺点:太碎片化了

标记整理:

把那些GC Root 这些对象整合在一起,这样有连续可用空间
缺点:效率低,需要移动地址

标记复制:在GC Root复制到新的内存区域中

缺点:浪费空间

说说GC和分代回收算法

GC的目的在于实现无用对象内存自动释放,减少内存碎片,加快分配速度

GC要点

  1. 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
  2. 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
  3. GC具体的实现称为垃圾回收器
  4. GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
  5. 根据GC的规模可以分成Minor GC,Mixed GC,Full GC

分代回收与GC规模

分代回收

  1. 伊甸园eden,最初对象都分配到这里,与幸存者合称新生代
  2. 幸存区survivo内存不足,回收后的幸存对象到这里,分成from-和to,采用标记复制算法
  3. 老年代old,当幸存区对象熬过几次,当伊甸园内次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

GC规模

  1. Minor GC 发生在新生代的垃圾回收,暂停时间短
  2. Mixed GC 新生代+老年代部分区域的垃圾回收,G1收集器特有
  3. Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

三色标记与并发漏标问题

用三种颜色记录对象的标记状态

  1. 黑色-已标记
  2. 灰色-标记中
  3. 白色-还未标记

漏标问题-记录标记过程中变化

  1. Incremental Update: 只要赋值发生,被赋值的对象就会被记录
  2. Snapshot At The Beginning, SATB:

    1.新加对象会被记录
    2.被删除引用关系的对象也被记录

垃圾回收器

####Paraller GC

  1. eden 内存不足发生Minor GC,标记复制STW(stop the world)
  2. old内存不足发生Full GC,标记整理STW
  3. 注重吞吐量

ConcurrentMarkSweep GC

  1. old并发标记,重新标记时需要STW,并发清除
  2. Failback Full GC
  3. 注重响应时间

G1 GC

  1. 响应时间与吞吐量兼顾
  2. 划分成多个区域,每个区域都可以充当eden,survivor,old,humngous
  3. 新生代回收:eden内存不足,标记复制STW(5%-60%)
  4. 并发标记:old并发标记,重新标记需要STW(占用堆内存的45%以上才会触发)
  5. 混合收集:并发标记完成,开始混合收集,参与复制的eden,survivor,old,其中old会根据暂停时间目标,选择部分回收价值高的区域,复制时STW
  6. Failback Full GC

项目中什么情况下会内存溢出,怎么解决的

  1. 误用线程池导致的内存溢出,解决方法,不要用自带的线程池
  2. 查询数据量太大导致的内存溢出,解决方法,采用分页
  3. 动态生成类导致的内存溢出,解决方法,最好用局部变量

类加载过程

类加载过程分为三个阶段

加载

  1. 将类的字节码载入方法区,并创建类.class对象
  2. 如果此类的父类没有加载,先加载父类
  3. 加载是懒惰执行

链接

  1. 验证-验证类是否符合Class规范,合法性,安全性检查
  2. 准备-为static变量分配空间,设置默认值
  3. 解析-将常量池的符号引用解析为直接引用

初始化

  1. 执行静态代码块与非final静态变量的赋值
  2. 初始化是懒惰执行

final修饰的基本类型变量

  1. 如果final修饰的基本类型,他不会类加载,而是直接复制拿来用,如果值过大,他会放到线程池中
  2. 如果final修饰的引用类型,则会类初始化。

何为双亲委派

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  1. 能找到这个类,由上级加载该类也对下级加载器可见
  2. 找不到这个类,则下级类加载器才有资格执行加载

对象引用类型分为那几类

强引用

  1. 普通变量赋值即为强引用,如A a = new A();
  2. 通过GC Root 的引用链,如果强引用找不到该对象,该对象才能被回收

软引用

  1. 例如:SoftReference a = new SoftReference(new A());
  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存还不足 再次回收时才会释放对象
  3. 软引用自身需要配合引用队列来释放
  4. 典型例子是反射数据

弱引用

  1. 例如:WeakReference a = new WeakReference(new A());
  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
  3. 弱引用自身需要配合引用队列来释放
  4. 典型 例子是ThreadLocalMap中的Entry对象

虚引用

  1. 例如:PhantomReference a = new PhantomReference(new A());
  2. 必须配合引用队列一起使用,当虚引用的对象被回收时,会将虚引用对象入队,由Reference Handler 线程释放其关联的外部资源
  3. 典型例子是Cleaner释放DirectByteBuffer 占用的直接内存

    finalize的理解

  4. 一般回答:它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作
  5. 较为优秀的回答是:将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM,从java9开始就被标注为@Deprecated,不建议被使用了

为什么finalize方法非常不好,非常影响性能

非常不好

  1. FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
  2. 异常被吞掉这个就太糟了,你甚至不能判断有没有在释放资源时发生错误

影响性能

  1. 重写了finalize方法的对象在第一次被gc时,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次gc时才能真正释放内存
  2. 可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易full GC ,full Gc 后释放速度如果依然跟不上创建新对象的速度,就会OOM

质疑

  1. 有的文章提到【Finalizer线程会和我们的主线程进行竞争,不过由于它的的优先级较低,获取到的cpu时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread的优先级较普通线程更高,赶不上步伐的原因应该是finalize执行慢等原因综合导致

文章作者: 小猩
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小猩 !
  目录