要求
结合一段 java 代码的执行理解内存划分
方法区:存放类的相关信息(类的名称、继承关系、引用的其他类的符号、成员变量、方法的字节码、类和方法和成员变量上加的注解等等)
堆: 存放new出来的对象
JVM 虚拟机栈:存放方法内的局部变量和方法参数 java实现的普通方法变量都存在这里,以前需要和os交互的特殊方法需要到本地方法栈去执行,但是现在Oracle公司的 Hotspot 虚拟机实现已经不再使用本地方法栈,或者说两个栈合二为一了,所有方法需要的变量内存都在JVM 虚拟机栈中
说明
会发生内存溢出的区域
内存溢出: 该区域内存耗尽了,报错了
内存泄漏:垃圾回收器无法回收某部分内存,这种现象就叫做内存泄漏;
上图中5块内存区域,除了程序计数器,都会产生内存溢出
方法区、永久代、元空间
方法区只是 JVM 规范中的一种定义 (你得有,怎么实现我不管)
永久代和元空间才是对规范的物理实现
从这张图学到三点
类元数据: 描述类的数据 (哪些成员,什么类型,长度多少…) 存储在元空间(方法区的物理实现)
类名.class 字节码对象,既然是对象,自然就存储在堆中了
类的原始信息(类元数据)存储在元空间中,无法直接访问,得通过java对象访问,这个对象就是字节码对象
从这张图可以学到
一般系统类加载器不会被释放,我们自定义的类加载器不再使用时会被释放( 释放啥? 元空间内存啊 )
要求
堆内存,按大小设置
解释:
从年代角度,JVM将内存划分为新生代和老年代
-Xmn的n就是new 新生代
堆内存,按比例设置
下图的 new 就是新生代,新生代内存可以进一步划分为eden和Survivor,Survivor又可以细分为:from,to
old 自然就是老年代内存
解释:
元空间内存设置
解释:
注意:
代码缓存内存设置
JIT即时编译器,将热点代码编译成机器码后缓存起来,就存放在CodeCache 代码缓存区
解释:
线程内存设置
也就是JVM虚拟机栈的内存
-Xss 设置每个线程占用的内存
不设置,linux系统默认1MB, 也就是每个线程默认占用1MB内存
官方参考文档
- https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
要求
eg: 堆内存中一些对象,已经没有任何栈内存中引用指向它,GC就可以将它回收了
三种垃圾回收算法
标记清除法
解释:
要点:
标记整理法
解释:
特点:
标记速度与存活对象线性关系
清除与整理速度与内存大小成线性关系
缺点是性能上较慢
标记复制法
解释:
特点:
GC 与分代回收算法
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
新生代:垃圾对象比较多 (方法内经常new的局部对象)
老年代: 存活对象比较多,很难回收,或者说不需要经常回收,整理也不会特别耗时 (eg: 静态对象,框架里面长期使用的对象) (老年代存活对象多,标记复制法也会极其浪费内存)
可达性分析算法:找到GC Root 打上标记 (先找到一定不会被回收的对象,然后沿着其引用链再找,再标记)
三色标记法:见下文
垃圾回收器有很多种,见下文
.
Minor GC:新生代的垃圾回收,小范围垃圾回收,暂停时间短,对系统影响小
Full GC: 新生代和老年代都发生内存不足了,来了一次全面的垃圾回收,暂停时间长,明显感到系统卡顿,一般是不愿意看到Full GC的
Mixed GC: 位于以上二者之间,指的是:新生代发生了垃圾回收,部分的老年代也发生了垃圾回收,一种混合垃圾回收,G1垃圾回收器独有的回收方式
个人再整理一下GC和堆内存相关概念:
GC只是回收堆内存
new出来的对象,都放在堆内存
堆内存划分:
从年代角度,JVM将堆内存划分为新生代和老年代
新生代内存又可以分为: eden和Survivor,Survivor又可以细分为:from,to
先总览一下,有个大致框架: 再慢慢看下面详细过程
图中黄色是空闲,白色是已分配
打标记可以用一句话概括: 寻找有没有被根对象直接或者间接引用到的
分代回收
幸存区不足: to不够复制的,肯定把已经在to的给移到老年代 (to很大的,不足肯定是有之前熬过了回收的对象存在的) 提前竞升也是没有办法的事情
大对象:每次GC都要复制来复制去的,太消耗了,不如提前竞升为老年代
GC 规模
Minor GC 发生在新生代的垃圾回收,暂停时间短
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
Full GC 新生代 + 老年代 完整(全面) 垃圾回收,暂停时间长,应尽力避免
三色标记
即用三种颜色记录对象的标记状态
黑色 – 已标记: 沿着根对象的引用链,已经找到这个对象了,且此对象内部的其他引用也已经处理完成了
灰色 – 标记中:沿着根对象的引用链,已经找到这个对象了,但这个对象内部的其他引用还没有处理完
白色 – 还未标记: 就是标记完最终剩下的对象了
将其直接引用标记为灰色,就认为他的引用处理完成了,就可以直接标记为黑色了
并发漏标问题
前面的GC是非并发的,GC在工作时,用户线程就暂停了,因此用户线程不会对GC线程造成影响
也即GC在打标记时,用户线程暂停了,不会对打标记产生任何影响(不会修改引用链)
非并发GC效率低,并发GC,也即并发标记,肯定是需要的
那么GC在打标记时,用户线程还在工作,万一打标过程中,用户线程修改了引用关系,很容易导致漏标啊
比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:
这个时候回收3其实也是合理的
但是万一他断开后又被别的对象引用了呢(不是我们不用了,而是我给别人用了) 就不能回收了呀(见下)
黑色对象已经处理过了(被标记为黑色的,会认为已经处理过了),已经处理过的对象,不会再去处理他的(不会再重复地找他的直接引用然后标记为灰色)
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
解决漏标,核心就是:记录标记过程中的变化+二次处理
上图红箭头 黑->白 黑色对象就是被赋值对象(把白色对象赋值给黑色对象)
垃圾回收器 - Parallel GC
eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
注重吞吐量 (响应时间、暂停时间慢点没关系,但是总体上暂停时间短一点就ok了)
Parallel GC: 实际上由2个垃圾回收器组成,一个工作在新生代,一个工作在老年代
Minor GC 仅仅新生代垃圾回收器工作
Full GC 时,新生代和老年代垃圾回收器都会工作
标记复制和标记整理(慢)都不会有内存碎片
垃圾回收器 - ConcurrentMarkSweep GC
它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
注重响应时间 (也就这一个好处 响应时间很快 不需要等很久)
ConcurrentMarkSweep GC 这是一个老年代垃圾回收器,
ConcurrentMarkSweep GC 简称 CMS垃圾回收器
Concurrent:并发
Mark:标记
Sweep: 扫描,打扫
并发就意味着GC时用户线程暂停时间很短,可以并发执行嘛
标记指的是标记为黑、灰、白三色,清除指的是清除回收白色垃圾对象
然而正因为人家采用的是 标记清除法,有内存碎片问题,因此最新的JDK已经将其标记为废弃了
垃圾回收器 - G1 GC
G1 GC 读作:G one 垃圾回收器
humongous: 巨大无比的
G1也有保底策略:回收速度<新对象创建速度 也就是并发失败 : FailBack Full GC 整体进行一次回收,暂停时间会比较长
G1 回收阶段 - 新生代回收
(eden区和幸存from区中的对象全部复制到新的幸存区(类似to))
G1 回收阶段 - 并发标记与混合收集
前提,老年代内存快不足了,才需要开始回收老年代,老年代标记策略是:并发标记
也不是直接回收所有的老年代区域,而是挑选几个回收价值高的老年代区域(存活对象很少)先进行回收
混合收集,不仅收集挑选出来的回收价值高的老年代(上图红色),还收集新生代(eden+survivor)
内存溢出: 该区域内存耗尽了,报错了
要求
典型情况
上图代码不断创建新的现场并提交,由于每个线程都要阻塞30ms,阻塞队列越来越大,无限制增长,就会导致内存爆
数据库条目太多了,你findAll, 一次查可能就100w条,就是100w个很普通的Product商品POJO集合,也要占用363MB的内存, 服务器内存再大,也经不起这么造啊, 10个用户就得占用3G内存呀
所以后端开发千万不要findAll( 自己不要写,也不要调用)
以后写代码,sql查询一定要加limit (光有条件都不行,条件可能失效啊)
这些错误在测试环境下是测不出来的,生产环境下才有百万级别的数据,才会暴露出来的问题
所以项目做完后,做一下压力测试也是很有必要的,面试会问到
要求
类加载过程的三个阶段
加载
类.class对象
里面有一系列反射方法,可以获知类的所有信息:有哪些成员,有哪些方法
类.class对象
存放在堆里面
链接
初始化
方法,在初始化时被调用验证手段
- 使用 jps 查看进程号
- 使用 jhsdb 调试,执行命令
jhsdb.exe hsdb
打开它的图形界面
- Class Browser 可以查看当前 jvm 中加载了哪些类
- 控制台的 universe 命令查看堆内存范围
- 控制台的 g1regiondetails 命令查看 region 详情
scanoops 起始地址 结束地址 对象类型
可以根据类型查找某个区间内的对象地址- 控制台的
inspect 地址
指令能够查看这个地址对应的对象详情- 使用 javap 命令可以查看 class 字节码
代码说明
- day03.loader.TestLazy - 验证类的加载是懒惰的,用到时才触发类加载
- day03.loader.TestFinal - 验证使用 final 修饰的变量不会触发类加载
字节码对象确实在堆空间(eden区域),不在方法区
会将静态成员和静态代码快里的语句,整合在一起,变成一个方法(cinit方法),在类初始化时调用这个方法
注意:final static 非引用类型 的变量在类加载时(创建字节码对象时)就会初始化好的,这里不需要整合了
前两个打印语句,看起来使用了类,Student.c和Student.m 实际上并没有真正使用到类,因此此时类并没有被加载,内存中并没有类,充分证明了类的加载是懒加载
此时类加载完成了,可以看到类的字节码信息了(类的结构:哪些成员、哪些方法)
当一个类A使用另一个类B的final static 普通类型 变量,实际上是常量,这个时候类A直接将该类B常量复制一份到自己类中,根本不会真的用到另一个类B
如果常量数值比较小,那么直接就写死在方法里
如果数值比较大,超过了short的最大范围(>32767) 就会放到常量池子中,需要用到时到常量池中拿就好了
也即是: 数值较大,会复制到类A自己的常量池中,每个类都有自己的常量池(一个常量列表,且1,2,3,… 地给每个常量编好了号,给出编号,直接到常量池中取那个常量的值)
解析:符号引用-》直接引用 随着代码的执行不断进行的过程,不是一次性就能完成的
类的static成员变量的引用,都是放在常量池的
没有给静态成员赋值时,常量池中就没有直接引用,只有符号引用(空指针 只知道要指向什么类型,但是并没有真的内存)
jdk 8 的类加载器
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
像String.class, Application和Extension类加载器中都没有,无法加载,这个时候必须向上询问Bootstrap启动类加载器,让他加载,然后下级都可见 (String类型是jdk的,上层都需要用到,所有都可见也是合理的)
像自己写的类Student.class, 也会遵循规则先逐级向上询问,上层加载器都没有这个类,Application类加载器才有了加载Student.class的资格,进行加载(上层类加载器不可见,也不需要可见,这种屏蔽很合理)
双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
双亲委派的目的有两点
让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类 (反之不行:jdk肯定不需要依赖你自己写的类)
让类的加载有优先次序,保证核心类优先加载
上级类加载器中的类对下级可见
但是下级类加载器中的类对上级不可见
对双亲委派的误解
下面面试题的回答是错误的
错在哪了?
自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。
假设你自己的类加载器用了双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
以上也仅仅是假设。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了 (实际操作,直接抛安全异常,或者编译不过,到不了假设那一步,jdk已经做了安全措施,防止你这么做了, 直接就不允许你重复写java.lang这重包名了)
代码说明
- day03.loader.TestJdk9ClassLoader - 演示类加载器与模块的绑定关系 =》 结论:不准自己重复写jdk已经有的包名.类名
要求
强引用
普通变量赋值即为强引用,如 A a = new A();
通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
软引用(SoftReference)
例如:SoftReference a = new SoftReference(new A()); (中间有一个SoftReference对象做中转,a间接关联到对象new A())
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象 (内存不足时会触发GC,第一次饶过你,第二次内存不足又触发了GC, 是会将软引用对象回收的(有强引用指向的对象GC无法回收))
软引用自身需要配合引用队列来释放(如下图,a对象是软引用,但是SoftReference自身还是强引用,GC无法回收软引用自身)
典型例子是反射数据(通过反射获取的数据都是软引用数据,如:类名.class=》获取的成员变量,方法等数据信息都是软引用)
弱引用(WeakReference)
例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放 (同上)
典型例子是 ThreadLocalMap 中的 Entry 对象
虚引用(PhantomReference)
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存
引用队列详解:如图,虚引用关联的对象a,b被释放内存后,虚引用本身会被放到引用队列里,由Reference Handler 线程专门负责回收他们,因为他们可能还关联了其他一些资源(不仅仅只是a对象和b对象)
代码说明
- day03.reference.TestPhantomReference - 演示虚引用的基本用法
- day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 entry 内存
String str = new String("hello"); // "hello"在堆内存中 (new出来的都在堆中)
String str = "hello"; // "hello" 在常量池中
ThreadLocalMap 中的 Entry 对象,key是弱引用,value是强引用
上图就是一种典型的内存泄露
解决:使用引用队列,将Entry和某个引用队列关联上,当Entry的key被回收时,整个Entry对象会被放到引用队列里面去,然后直接将已经在引用队列中的Entry对象的Map引用去掉就行了(或者说看看当前Entry在不在Map中,在就将Map里面记录Entry的数组对应引用设置为null),没有引用指向它,下次回收时就会被回收了
jdk不是这么实现的,成本会比较高
★★★key就是ThreadLocal对象本身,线程运行时一定还被其他对象强引用,所以不怕他被设置为弱引用,线程没有结束前,key(有其他强引用)不会被释放。但是value一旦设置为弱引用,真的就只有这一个弱引用了,很可能线程还没结束,就被GC回收了。★★★
要求
finalize
追问:为什么非常不好,非常影响性能?
见下面原理:
补:守护线程,在主线程已经结束时,守护线程就不会再执行了(即使有代码没执行完毕)
finalize 原理
finalize 缺点
代码说明
- day03.reference.TestFinalize - finalize 的测试代码