3-虚拟机篇
Posted 11.π.14
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-虚拟机篇相关的知识,希望对你有一定的参考价值。
一.java JVM 的内存结构
内存:按线程类型分两类
线程共享:
- 方法区:存放类的信息
- 堆:存放java对象的信息
线程私有:
- java虚拟机栈:存放java方法、方法参数和局部变量
- 程序计数器:记录程序执行到几行
执行引擎
- 解释器:把class 字节码代码解释成机器码,对同一行代码反复解释,比如调用同一个方法多次,就会被解释多次。
- JIT即时编译器:把热点代码解释成机器代码,并且缓存起来。
2.1热点判定的方式有两种:采样热点探测、计数器探测。判定某段代码是否为热点代码,是否要触发即时编译的这种行为称为“热点探测”
二.哪些部分会出现内存溢出
除了程序计数器外,方法区、堆、栈、本地方法栈都会出现内存溢出。
内存溢出分为两种情况:
-
OutOfMemoryError
- 堆内存耗尽- 对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽- 加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累积- 默认每个线程最多会占用1M内存,线程个数越来越多,而 又长时间运行不销毁时
-
StackOverflowError
- 虚拟机栈内部- 方法调用次数过多,比如递归错误,无限制的运行,消耗掉线程内的1M内存。
方法区与永久代、元空间之间的关系
方法区是规范,永久代和元空间都是对方法区的实现。
-
方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
-
永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)
-
元空间是Hotspot虚拟机对JVM规范的实现(1.8之后),使用本地内存作为这些信息的存储空间
JVM内存参数
对于JVM内存配置参数:
- -Xmx10240m(最大内存数10G)
- -Xms10240m(最小内存数10G)
- -Xmn5120m(新生代5G)
- -XX:SurvivorRatio=3其最小内存值和Survivor区总大小分别是
- -Xss 线程的内存 linux 64位默认是1M
JVM垃圾回收算法
-
标记清除(都不用了)CMS,在最新的JVM虚拟机中已经废弃了
分为标记阶段,和清除阶段。
标记阶段先找到根对象,一定不能回收的对象。例如:局部变量引用的对象,正在被使用或者,静态变量引用的对象。沿着根对象的引用链,找到被引用的对象,标记这些对象。
清除阶段:没有标记的对象直接清除就好了。
缺点:标记清除,会导致内存碎片化,内存不连续 -
标记整理(适用于老年代垃圾回收)
标记阶段:沿着根对象的引用链,找到被引用的对象,标记这些对象。
整理阶段:移动存活对象到一边,解决内存碎片化,不连续的问题
-
标记复制(适用于新生代代垃圾回收)
标记阶段:沿着根对象的引用链,找到被引用的对象,标记这些对象。
复制阶段:把存活对象复制到空闲区域,直接清理旧的区域就好了
缺点:占用内存多,经常用于新生代的内存空间。不适合老年代
说说GC 和 分代回收算法!!!!!
-
1.Gc的目的在于实现无用对象的内存自动释放,减少内存碎片、加快内存分配速度
-
2.GC要点
- 1.回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
- 2.判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
- 3.GC具体的实现称为垃圾回收器常见的垃圾回收器
- 4.GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
- 5.根据GC的规模可以分成Minor Gc(新生代发生了垃圾回收),Mixed GC(老年代发生了垃圾回收),Full GC(时间比较长)
-
3.分代回收
- 1.伊甸园eden,最初对象都分配到这里,与幸存区合称为新生代
- 2.幸存区survivor ,当伊甸园内存不足,回收后的幸存对象到这里,分成from 和 to ,采用标记复制算法
- 3.老年代old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
-
4 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.被删除引用关系的对象也被记录
几个重要的垃圾处理器
- Parallel GC特点有如下
- 1.eden内存不足发生Minor GC,标记复制 STW
- 2.old内存不足发生Full GC,标记整理STW
- 3.注重吞吐量
- 4.虽然会暂停,但是会多个线程并行执行垃圾回收,所以时间比较短
- ConcurrentMarkSweep GC
- 1.old 并发标记,重新标记时需要STW,并发清除
- 2.Failback Full GC 并发失败,清除速度 < 对象产生的速度,就会并发失败,就会触发 Full GC
- 3.注重响应时间
- G1 GC 从JDK9开始作为默认GC
- 1.响应时间与吞吐量兼顾
- 2.划分多个区域,每个区域都可以充当eden,survivor,old,humongous(存放大对象的区域)
- 工作流程可以分为3个阶段
- 1.新生代回收:eden内存不足,标记复制STW,复制到survivor,如果幸存对象到达了晋升阈值,就复制到老年代
- 2.并发标记:old在堆内存中的占比超过45%,触发并发标记,重新标记时需要STW
- 3.混合收集:并发标记完成,开始混合收集,参与复制的有eden、survivor、old,其中old会根据暂停时间目标,选择部分回收价值高(存活对象少)的区域,复制时STW
- 4.Failback Full GC
项目中什么情况下会出现内存溢出,怎么解决的
- 误用线程池导致的内存溢出
// 案例1 主要是由于等待队列成撑爆了内存
public class TestThreadPool
public static void main(String[] args)
ExecutorService executor = Executors.newFixedThreadPool(2);
while (true)
executor.submit(()->
try
LoggerUtils.get().debug("send sms");
TimeUnit.SECONDS.sleep(30);
catch (InterruptedException e)
e.printStackTrace();
);
主要是由于等待队列成撑爆了,不要用工具类创建线程池,自己调用 ThreadPoolExecutor ,控制拒绝策略和线程数上限
// 由于线程数没有上限,导致的内存溢出
public class TestThreadPool
public static void main(String[] args)
case2();
static AtomicInteger c = new AtomicInteger();
private static void case2()
ExecutorService executor = Executors.newCachedThreadPool();
while (true)
System.out.println(c.incrementAndGet());
executor.submit(()->
try
TimeUnit.SECONDS.sleep(30);
catch (InterruptedException e)
e.printStackTrace();
);
-
查询数据量太大导致的内存溢出
-
动态生成类过多导致的内存溢出
类加载过程、双亲委派机制
- 类加载分为三个阶段
- 加载
- 1.将类的字节码载入方法区,并创建类.class对象
- 2.如果此类的父类没有加载,先加载父类,接口也是一样的。spring bean 也是一样的,创建当前bean时,先创建其依赖的bean
- 3.加载是懒惰执行。正在用到类的时候,才会加载。否则不会加载到方法区
- 链接
- 1.验证 - 验证类是否符合Class规范,合法性、安全性检查
- 2.准备 - 为static 变量分配空间
- 3.解析 - 将常量池的符号引用解析为直接引用
- 4.final 静态变量赋值
- 初始化
- 1.执行静态代码块为非final 静态变量赋值
- 2.初始化是懒惰执行的
- 加载
- 何为双亲委派
- 所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
- 1.能找到这个类,由上级加载,加载后该类也对下级加载器可见
- 2.找不到这个类,则下级加载器才有资格执行加载
一道错误的面试题解答
能不能自己写一个类叫 java.lan.System?
- 错误的回答
答:通常是不可以的,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用java系统提供的System,自己写的System类根本就没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委派机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统给的加载器就无法加载,也就是最终还是由我们自己的加载器加载。 - 错在哪里了?
自己编写类加载器就能加载一个假冒的java.lang.System吗?
不行- 1.假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的java.lang.System,自然不会加载假冒的
- 2.假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的java.lang.System时,它需要先加载父类java.lang.Object,而你没有用委派,找不到java.lang.Object所以加载会失败
- 3.以上仅仅是假设。实际操作你就会发现自定义类加载器加载以java.大头的类时,会抛出安全异常,在jdk9以上版本这些特殊包名都与模块进行了绑定,更连编译都过不去
- 双亲委派的目的是什么?
- 1.上级加载器加载的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类
- 2.让类的加载有优先次序,保证核心类优先加载
对象的引用类型分为哪几种
- 1.强引用
- 1.普通变量赋值即为强引用,如 A a = new A();
- 2.通过GC Root 的引用链,如果强引用找不到该对象,该对象才会被回收
- 2.软引用(SoftReference)
- 1.例如:SofReference a = new SoftReference(new A());
- 2.如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
- 3.软引用自身需要配合引用队列来释放
- 4.典型例子是反射数据
- 3.弱引用(WeakReference)
- 1.例如: WeakReference a = new WeakReference(new A());
- 2.如果仅有弱引用引用了该对象时,只要发生垃圾回收,就会释放该对象
- 3.弱引用自身需要配合引用队列来释放
- 4.典型例子是ThreadLocalMap中的Entry对象
- 4.虚引用(PhantomReference)
- 1.例如:PhantomReference a = new PhantomReference(new A())
- 2.必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由Reference Handler 线程释放其关联的外部资源
- 3.典型例子是Cleaner释放DirectByteBuffer占用的直接内存
finalize的理解?
- 一般的回答:它是Object中的一个方法,子类重写它,垃圾回收此方法会被调用,可以在其中进行一些资源释放和清理工作
- 较为优秀的方法:将资源释放和清理放在finalize方法中是非常不好的,非常影响性能,严重时甚至会引起oom,从java9开始就被标注为@Deprecated,不建议被使用
- 但是,为什么?性能不好
- 1.非常不好
- 1.FinalizerThread是守护线程,代码很有可能没有来得及执行完,线程就结束了,造成资源没有正确释放
- 2.异常被吞掉了,这个就太糟了,你甚至不能判断 有没有在释放资源时发生错误
- 2.影响性能
- 1.重写了finalize方法的对象在第一次被GC的时候,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次gc时才能真正释放内存
- 2.可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾累积过多就会容易full gc,full gc 后释放速度如果仍跟不上创建新对象的速度,就会OOM
- 3.质疑
- 1.有的文章提到【Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread的优先级较普通线程更高,赶不上步伐的原因应该是finalize执行慢等原因导致的
- 1.非常不好
以上是关于3-虚拟机篇的主要内容,如果未能解决你的问题,请参考以下文章