学习随笔简介
跟随着黑马满老师的《Java八股文面试题视频教程,Java面试八股文宝典》学习,视频教程地址: https://www.bilibili.com/video/BV15b4y117RJ?p=104&share_source=copy_web&vd_source=12ab4e2292b9162c982607a3ca0075ba
共分为四个部分,分别是 基础篇、并发篇、虚拟机、框架篇
本篇更新虚拟机篇的内容
目录
一、JVM内存结构
1.内存结构划分
1.类加载子系统:在运行程序时首次运行类时进行 加载——连接——初始化
2.可运行数据区:
【注意】主要可以分为两个部分:共享区和独占区。共享区主要包括方法区和堆区,独占区包括程序计数器、虚拟机栈和本地方法栈
- 方法区: 存放已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
- 堆:存放对象、数组、非静态变量
- 程序计数器:可以正确控制Java程序中的流程控制,正确轮换多线程
- 虚拟机栈:每个方法对应一个栈帧,栈帧包含局部变量表、操作数栈、动态链接、方法返回值等
- 本地方法栈:不是使用Java实现的函数,用来支持本地方法的调用逻辑的
3.执行引擎:
- 解释器:作用就是读取字节码,并且逐一执行
- 即时编译器:由于一段代码被多次调用时,都需要解释执行,即时编译器可以将这样的字节码编译成本地代码,用于多次的重复调用来提高效率
- 垃圾回收:收集并删除未引用的对象
4.本地方法接口:
- 与本地方法库进行交互,提供执行引擎所需要的本地库
5.本地方法库:
- 执行引擎所需要的本地库的集合
2.内存溢出的区域
- 不会出现内存溢出的区域——程序计数器
-
出现 OutOfMemoryError 的情况:
-
堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
-
方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
-
虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
-
-
出现 StackOverflowError 的区域:
-
JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
-
3.方法区、永久代、元空间
- 方法区:JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
- 永久代:HotSpot虚拟机对JVM规范的实现(JDK1.8之前)
- 元空间:HotSpot虚拟机对JVM规范的另一种实现(JDK1.8之后),使用本地内存作为这些信息的存储空间
- 当第一次用到某个类的时候,由类加载器将class文件的类元信息读入,并存储于元空间
- 类元信息是存储于元空间中,无法直接访问
- 可以用 .class文件间接访问类元信息,它们两属于Java对象,我们在代码中可以使用
从这个图中看出来:
- 堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC就会对它们占用的内存进行释放
- 元空间中:内存释放以类加载器为单位, 当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放
二、JVM内存参数
1.堆内存设置
按大小设置:
具体参数:
- -Xms:最小堆内存( 包括新生代和老年代)
- -Xmx:最大堆内存(包括新生代和老年代)
- 通常建议将最小堆内存和最大堆内存设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
- -XX:NewSize 与 -XX:MaxNewSize设置新生代的最小与最大值,但一般不建议设置,由JVM自己控制
- -Xmn:设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
- 图中的保留是指一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存
按比例设置:
具体参数:
- -XX:NewRatio=2:1 表示老年代占两份,新生代占一份
- -XX:SurvivorRatio=4:1 表示新生代分为6份,伊甸园占4份,from和to区各占一份
2.元空间内存设置
具体参数:
- class space:存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
- non-class space:存储除类的基本信息以外的其他信息(如方法字节码、注解等)
- class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制
3.代码缓存内存设置
具体参数:
- 如果 -XX:ReservedCodeCacheSize < 240m ,所有优化机器代码不加区分存在一起
-
否则,即 -XX:ReservedCodeCacheSize >= 240m,则分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
-
non-nmethods - JVM 自己用的代码
-
profiled nmethods - 部分优化的机器码
-
non-profiled nmethods - 完全优化的机器码
-
三、JVM垃圾回收
1.三种垃圾回收算法
1.标记清除算法:
算法解释:
- 找到 GC Root 对象(那些一定不会被回收的对象,如正在执行方法内局部变量引用的对象、静态变量引用的对象)
- 标记阶段:沿着 GC Root对象的引用链找,直接或间接引用到的对象加上标记
- 清除阶段:释放未加标记的对象占用的内存
要点:
- 标记速度与存活对象线性关系
- 清除速度与内存大小线性关系
- 缺点:会产生内存碎片,无法找到足够的连续内存
2.标记整理算法:
算法解释:
- 前面的标记阶段、清理阶段与标记清除法类似
- 相较之前,多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片的产生
特点:
- 标记速度与存活对象成线性关系
- 清除、整理速度与内存大小成线性关系
- 缺点:移动对象极为负重,必须全程暂停用户应用程序才能进行;性能上较慢
3.标记复制算法:
算法解释:
- 将整个内存分为两个大小相等的区域:from区 和 to区,其中 to区总是处于空闲,from存储新创建的对象
- 标记阶段与前面的算法类似
- 在找出存活对象后,会将它们从 from区复制到 to区,复制的过程中自然完成了碎片整理
- 复制完成后,交换 from区 和 to区 的位置即可
特点:
- 标记、复制速度与存活对象成线性关系
- 缺点就是会占用成倍的空间,造成空间的浪费
2.GC与分代回收算法
GC的目的:实现无用的对象内存自动释放,减少内存碎片、加快分配速度
GC的要点:
- 回收区域是堆内存,不包括虚拟机栈
- 判断无用的对象的方法有可达性分析算法、三色标记法,标记存活的对象,回收未标记的对象
- GC的具体实现称为垃圾回收器
-
GC 大都采用了分代回收思想(建立在弱分代假说、强分代假说和跨代引用假说之上)
-
理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
-
根据这两类对象的特性将回收区域分为新生代和老年代,新生代采用标记复制法、老年代一般采用标记整理法
-
-
根据 GC的规模可以分成 Minor GC、Mixed GC、Full GC
对于上述每个点在下面都有对应的解释:
1.判断无用的对象的方法
- 可达性分析算法:从GC Roots为起点开始遍历整个对象图,和GC Roots直接或间接相连的对象才是存活对象,反之就是死亡对象。从GC Roots搜索过的路径叫做引用链。
- 三色标记法:使用三种颜色表示对象的标记状态,分别是 黑色——已标记,灰色——标记中,白色——未被标记
2.垃圾回收器的介绍放在下面具体说
3.分代回收
-
伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代
-
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
-
将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
-
将 from 和 to 交换位置
-
经过一段时间后伊甸园的内存又出现不足
-
标记伊甸园与 from(现阶段没有)的存活对象
-
将存活对象采用复制算法复制到 to 中
-
复制完毕后,伊甸园和 from 内存都得到释放
-
将 from 和 to 交换位置
-
老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
4.GC 规模
- Minor GC 发生在新生代的垃圾回收,暂停时间短
- Mixed GC 对新生代和老年代的部分区域进行垃圾回收,G1垃圾收集器特有
- Full GC 新生代和老年代完整垃圾回收,暂停时间长,应全力避免
3.并发漏标问题
目前比较先进的垃圾回收器都支持并发标记 ,即在标记过程中,用户线程仍然可以工作。如果用户线程修改了对象的引用,那么就会出现漏标问题。
解决办法:
-
Incremental Update 增量更新法,CMS 垃圾回收器采用
-
思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
-
-
Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
-
思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
-
新加对象会被记录
-
被删除引用关系的对象也被记录
-
4.垃圾回收器
1.Paraller GC(并行垃圾回收器)
- eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
- old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
- 注重吞吐量的时候使用这种垃圾回收器
2.ConncurrentMarkSweep GC(CMS垃圾回收器)
工作在 old 老年代,支持并发标记的一款回收器,采用标记清除算法
- 并发标记时不需暂停用户线程
- 重新标记仍需暂停用户线程
如果并发失败(回收速度赶不上创建新对象的速度),会触发 Full GC
注重响应时间的时候使用这种垃圾回收器
3.G1 GC
将整个堆内存划分为多个大小相等区域,每个区域都可以充当 eden,survivor,old,humongous(专为大对象准备)
分为三个阶段:新生代回收、并发标记、混合收集
如果并发失败,会触发 Full GC
响应时间和吞吐量兼顾
四、内存溢出
这里举几种典型的导致内存溢出的情况:
- 误用线程池导致的内存溢出
- 查询数据量太大导致的内存溢出
- 动态生成类导致的内存溢出
1.误用线程池导致的内存溢出
示例1:通过Executors自动创建 FixedThreadPool 线程池,代码如下:
private static void case1() {
ExecutorService executor = Executors.newFixedThreadPool(2);
LoggerUtils.get().debug("begin...");
while (true){
executor.submit(() -> {
try {
LoggerUtils.get().debug("send sms...");
TimeUnit.SECONDS.sleep(30);
}catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
运行结果:
17:31:44.144 [main] DEBUG G - begin...
17:31:44.148 [pool-1-thread-1] DEBUG A - send sms...
17:31:44.148 [pool-1-thread-2] DEBUG B - send sms...
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
造成原因:
我们查看源码,可以发现其中使用的工作队列为 LinkedBlockingQueue,它是一个无界的工作队列,任务数量将队列塞满:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
解决办法:
我们在使用Executors自动创建线程的时候尽量不要使用 newFixedThreadPool 这种方式
示例2:通过 Executors 自动创建带缓存的线程池 (CachedThreadPool),代码如下:
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();
}
});
}
}
这里测试可以学习视频中,在linux系统中修改进程和线程的最大数来进行测试,这里建议最好不要在Windows下直接测试
造成原因:
我们查看源码,可以发现其中使用的工作队列为 SynchronousQueue ,它创建的线程是没有数量限制的,创建的线程数量过多,耗尽系统的线程资源 :
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
解决办法:
我们在使用Executors自动创建线程的时候尽量不要使用 newCachedThreadPool 这种方式
2.查询数据量太大导致的内存溢出
演示代码:
public class TestOomTooManyObject {
public static void main(String[] args) {
//对象本身内存
long a = ClassLayout.parseInstance(new Product()).instanceSize();
System.out.println(a);
// 一个字符串占用内存
String name = "联想小新Air14轻薄本 英特尔酷睿i5 14英寸全面屏学生笔记本电脑(i5-1135G7 16G 512G MX450独显 高色域)银";
long b = ClassLayout.parseInstance(name).instanceSize();
System.out.println(b);
String desc = "【全金属全面屏】学生商务办公,全新11代处理器,MX450独显,100%sRGB高色域,指纹识别,快充(更多好货)";
long c = ClassLayout.parseInstance(desc).instanceSize();
System.out.println(c);
System.out.println(16 + name.getBytes(StandardCharsets.UTF_8).length);
System.out.println(16 + desc.getBytes(StandardCharsets.UTF_8).length);
// 一个对象估算的内存
long avg = a + b + c + 16 + name.getBytes(StandardCharsets.UTF_8).length + 16 + desc.getBytes(StandardCharsets.UTF_8).length;
System.out.println(avg);
// ArrayList 24, Object[] 16 共 40
System.out.println((1_000_000 * avg + 40) / 1024 / 1024 + "Mb");
}
static public class Product {
private int id;
private String name;
private int price;
private String desc;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
}
演示结果:
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.memory.Universe.getNarrowOopBase()
32
24
24
144
157
381
363Mb
可以看出,占用的内存很大,在高并发的情况下,就有可能出现内存溢出
3.动态生成类导致的内存溢出
演示代码:
public class TestOomTooManyClass {
static GroovyShell shell = new GroovyShell();
public static void main(String[] args) {
AtomicInteger c = new AtomicInteger();
while (true) {
try (FileReader reader = new FileReader("script")) {
shell.evaluate(reader);
System.out.println(c.incrementAndGet());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里就不放演示结果了(结果太长)
造成原因:
GroovyShell对象无法被回收,导致对象中的 GroovyClassLoader 类加载器无法被回收,导致元空间的内存无法被释放,直至溢出
解决办法:
将静态变量的GroovyShell改为方法中的局部变量,循环完一次对象不再使用就可以回收其内存,然后就可以回收对象中的类加载器,这样就可以回收其所占的内存