JVM内存结构
线程私有
哪些部分会出现内存溢出
不会出现内存溢出的区域-程序计数器
出现OutOfMemoryError的情况
- 堆内存耗尽-对象越来越多,又一直使用,不能被垃圾回收
- 方法区内存耗尽-加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累积-每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁.
出现StackOverflowError
- 虚拟机栈内部-方法调用次数过多
方法区与永久代,元空间之间的关系
- 方法区是jvm规范中定义的一块内存区域,用来存储类元数据,方法字节码,即时编译器需要的信息等
- 永久代是Hostpot 虚拟机对jvm规范的实现(1.8之前)
- 元空间是Hotspot虚拟机对jvm规范的实现(1.8以后),使用本地内存作为这些信息的存储空间
JVM垃圾回收算法
标记清除:
首先找到GC Root这些不能被回收对象,然后清除没有GC Root 对象(cms在用这个方法,现在基本弃用了)
缺点:太碎片化了
标记整理:
把那些GC Root 这些对象整合在一起,这样有连续可用空间
缺点:效率低,需要移动地址
标记复制:在GC Root复制到新的内存区域中
缺点:浪费空间
说说GC和分代回收算法
GC的目的在于实现无用对象内存自动释放,减少内存碎片,加快分配速度
GC要点
- 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
- 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
- GC具体的实现称为垃圾回收器
- GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
- 根据GC的规模可以分成Minor GC,Mixed GC,Full GC
分代回收与GC规模
分代回收
- 伊甸园eden,最初对象都分配到这里,与幸存者合称新生代
- 幸存区survivo内存不足,回收后的幸存对象到这里,分成from-和to,采用标记复制算法
- 老年代old,当幸存区对象熬过几次,当伊甸园内次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
GC规模
- Minor GC 发生在新生代的垃圾回收,暂停时间短
- Mixed GC 新生代+老年代部分区域的垃圾回收,G1收集器特有
- Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
三色标记与并发漏标问题
用三种颜色记录对象的标记状态
- 黑色-已标记
- 灰色-标记中
- 白色-还未标记
漏标问题-记录标记过程中变化
- Incremental Update: 只要赋值发生,被赋值的对象就会被记录
- Snapshot At The Beginning, SATB:
1.新加对象会被记录
2.被删除引用关系的对象也被记录
垃圾回收器
####Paraller GC
- eden 内存不足发生Minor GC,标记复制STW(stop the world)
- old内存不足发生Full GC,标记整理STW
- 注重吞吐量
ConcurrentMarkSweep GC
- old并发标记,重新标记时需要STW,并发清除
- Failback Full GC
- 注重响应时间
G1 GC
- 响应时间与吞吐量兼顾
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humngous
- 新生代回收:eden内存不足,标记复制STW(5%-60%)
- 并发标记:old并发标记,重新标记需要STW(占用堆内存的45%以上才会触发)
- 混合收集:并发标记完成,开始混合收集,参与复制的eden,survivor,old,其中old会根据暂停时间目标,选择部分回收价值高的区域,复制时STW
- Failback Full GC
项目中什么情况下会内存溢出,怎么解决的
- 误用线程池导致的内存溢出,解决方法,不要用自带的线程池
- 查询数据量太大导致的内存溢出,解决方法,采用分页
- 动态生成类导致的内存溢出,解决方法,最好用局部变量
类加载过程
类加载过程分为三个阶段
加载
- 将类的字节码载入方法区,并创建类.class对象
- 如果此类的父类没有加载,先加载父类
- 加载是懒惰执行
链接
- 验证-验证类是否符合Class规范,合法性,安全性检查
- 准备-为static变量分配空间,设置默认值
- 解析-将常量池的符号引用解析为直接引用
初始化
- 执行静态代码块与非final静态变量的赋值
- 初始化是懒惰执行
final修饰的基本类型变量
- 如果final修饰的基本类型,他不会类加载,而是直接复制拿来用,如果值过大,他会放到线程池中
- 如果final修饰的引用类型,则会类初始化。
何为双亲委派
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
- 能找到这个类,由上级加载该类也对下级加载器可见
- 找不到这个类,则下级类加载器才有资格执行加载
对象引用类型分为那几类
强引用
- 普通变量赋值即为强引用,如A a = new A();
- 通过GC Root 的引用链,如果强引用找不到该对象,该对象才能被回收
软引用
- 例如:SoftReference a = new SoftReference(new A());
- 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存还不足 再次回收时才会释放对象
- 软引用自身需要配合引用队列来释放
- 典型例子是反射数据
弱引用
- 例如:WeakReference a = new WeakReference(new A());
- 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
- 弱引用自身需要配合引用队列来释放
- 典型 例子是ThreadLocalMap中的Entry对象
虚引用
- 例如:PhantomReference a = new PhantomReference(new A());
- 必须配合引用队列一起使用,当虚引用的对象被回收时,会将虚引用对象入队,由Reference Handler 线程释放其关联的外部资源
- 典型例子是Cleaner释放DirectByteBuffer 占用的直接内存
finalize的理解
- 一般回答:它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作
- 较为优秀的回答是:将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM,从java9开始就被标注为@Deprecated,不建议被使用了
为什么finalize方法非常不好,非常影响性能
非常不好
- FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
- 异常被吞掉这个就太糟了,你甚至不能判断有没有在释放资源时发生错误
影响性能
- 重写了finalize方法的对象在第一次被gc时,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次gc时才能真正释放内存
- 可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易full GC ,full Gc 后释放速度如果依然跟不上创建新对象的速度,就会OOM
质疑
- 有的文章提到【Finalizer线程会和我们的主线程进行竞争,不过由于它的的优先级较低,获取到的cpu时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread的优先级较普通线程更高,赶不上步伐的原因应该是finalize执行慢等原因综合导致