要求
结合一段 java 代码的执行理解内存划分
说明
会发生内存溢出的区域
方法区、永久代、元空间
从这张图学到三点
从这张图可以学到
*
要求
堆内存,按大小设置
解释:
堆内存,按比例设置
解释:
元空间内存设置
解释:
注意:
代码缓存内存设置
解释:
线程内存设置
官方参考文档
- https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
要求
三种垃圾回收算法
标记清除法
解释:
要点:
标记整理法
解释:
特点:
标记速度与存活对象线性关系
标记复制法
解释:
特点:
GC 与分代回收算法
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
分代回收
GC 规模
Minor GC 发生在新生代的垃圾回收,暂停时间短
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
三色标记
即用三种颜色记录对象的标记状态
并发漏标问题
比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
垃圾回收器 - Parallel GC
old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
垃圾回收器 - ConcurrentMarkSweep GC
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
垃圾回收器 - G1 GC
G1 回收阶段 - 新生代回收
G1 回收阶段 - 并发标记与混合收集
要求
典型情况
// -Xmx64m
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
// new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
// new LinkedBlockingQueue<Runnable>()); // 队列上限是整数最大值
LoggerUtils.get().debug("begin...");
while (true) {
executor.submit(()->{
try {
LoggerUtils.get().debug("send sms...");
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
public class Executors {
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
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");
}
}
import groovy.lang.GroovyShell;
// -XX:MaxMetaspaceSize=24m
// 模拟不断生成类, 但类无法卸载的情况
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")) {
GroovyShell shell = new GroovyShell();
shell.evaluate(reader);
System.out.println(c.incrementAndGet());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
要求
类加载过程的三个阶段
加载
将类的字节码载入方法区,并创建类.class 对象
static final Object = new Object
、静态引用类型和 *.class类型Student.class
会触发类加载和初始化,静态常量static final int
不会。
* 每一个类都有常量池,其他类未加载时是符号引用,类加载后变成直接引用(拥有具体的内存地址,常量池和类的其他信息都存放到方法区中)。<cinit>
方法,在初始化时被调用验证手段
- 使用 jps 查看进程号
- 使用 jhsdb 调试,执行命令
jhsdb.exe hsdb
打开它的图形界面
- Class Browser 可以查看当前 jvm 中加载了哪些类
- 控制台的 universe 命令查看堆内存范围
- 控制台的 g1regiondetails 命令查看 region 详情
scanoops 起始地址 结束地址 对象类型
可以根据类型查找某个区间内的对象地址- 控制台的
inspect 地址
指令能够查看这个地址对应的对象详情- 使用 javap -c -v -p 命令可以查看 class 字节码
代码说明
- day03.loader.TestLazy - 验证类的加载是懒惰的,用到时才触发类加载
- day03.loader.TestFinal - 验证使用 final 修饰的变量不会触发类加载
jdk 8 的类加载器
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
双亲委派机制
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
双亲委派的目的有两点
让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
让类的加载有优先次序,保证核心类优先加载
对双亲委派的误解
下面面试题的回答是错误的
错在哪了?
自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。
假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
以上也仅仅是假设。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了
代码说明
- day03.loader.TestJdk9ClassLoader - 演示类加载器与模块的绑定关系
要求
强引用
普通变量赋值即为强引用,如 A a = new A();
通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
软引用(SoftReference)
例如:SoftReference a = new SoftReference(new A());
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
软引用自身需要配合引用队列来释放
典型例子是反射数据
弱引用(WeakReference)
例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放
典型例子是 ThreadLocalMap 中的 Entry 对象
// 弱引用队列可解决ThreadLocal的ThreadLocalMap内存泄露,但是成本高不建议使用。
// (一般手动使用remove移除map的value对象,而不是让gc回收)
// ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
// ThreadLocal 同时实现了线程内的资源共享
// ThreadLocal 不同线程间隔离,同一个线程共享变量。
package day03.reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class TestWeakReference {
public static void main(String[] args) {
MyWeakMap map = new MyWeakMap();
map.put(0, new String("a"), "1");
// 强引用不会被当前方法gc回收
map.put(1, "b", "2");
map.put(2, new String("c"), "3");
map.put(3, new String("d"), "4");
System.out.println(map);
System.gc();
System.out.println(map.get("a"));
System.out.println(map.get("b"));
System.out.println(map.get("c"));
System.out.println(map.get("d"));
System.out.println(map);
map.clean();
System.out.println(map);
}
// 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
static class MyWeakMap {
// 给ThreadLocalMap增加引用队列,清除value的强引用
static ReferenceQueue<Object> queue = new ReferenceQueue<>();
static class Entry extends WeakReference<String> {
String value;
public Entry(String key, String value) {
super(key, queue);
this.value = value;
}
}
// 回收资源
public void clean() {
Object ref;
while ((ref = queue.poll()) != null) {
System.out.println(ref);
for (int i = 0; i < table.length; i++) {
if(table[i] == ref) {
table[i] = null;
}
}
}
}
Entry[] table = new Entry[4];
public void put(int index, String key, String value) {
table[index] = new Entry(key, value);
}
public String get(String key) {
for (Entry entry : table) {
if (entry != null) {
String k = entry.get();
if (k != null && k.equals(key)) {
return entry.value;
}
}
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Entry entry : table) {
if (entry != null) {
String k = entry.get();
sb.append(k).append(":").append(entry.value).append(",");
}
}
if (sb.length() > 1) {
sb.deleteCharAt(sb.length() - 1);
}
sb.append("]");
return sb.toString();
}
}
}
执行结果:
[a:1,b:2,c:3,d:4] null 2 null null [null:1,b:2,null:3,null:4] day03.reference.TestWeakReference$MyWeakMap$Entry@b81eda8 day03.reference.TestWeakReference$MyWeakMap$Entry@68de145 day03.reference.TestWeakReference$MyWeakMap$Entry@27fa135a [b:2]
进程已结束,退出代码0
总结一下: 在MyWeakMap类中,Entry类继承了WeakReference
,其中使用了弱引用来持有键(key)。通过将键包装在Entry对象中并使用弱引用,可以实现在没有强引用持有键时,键所对应的Entry对象可以被垃圾回收器回收。 在调用gc,由于软引用的键new string acd 和字符串b都是强引用,所以都不会被回收,可以获取到数据。 转换一下就是:当对象只被弱引用引用时,如果垃圾回收器扫描到这个对象时发现只有弱引用指向它,那么该对象就会被标记为可回收,并在适当的时机被垃圾回收器回收。 由于都拥有强引用,所以不能被回收。
在给定的代码中,字符串
"b"
是一个强引用对象,而不是一个弱引用对象或可引用对象。通过
map.put(1, "b", "2")
将字符串"b"
直接作为键传递给了put
方法,它不会被包装在弱引用或其他特殊引用对象中。因此,字符串"b"
是一个强引用对象,其生命周期与持有它的变量或数据结构相关联。弱引用对象是指在没有其他强引用持有它时可以被垃圾回收器回收的对象。在给定的代码中,并没有使用弱引用来包装字符串
"b"
,因此它不属于弱引用对象。可引用对象是一个更广义的概念,它包括强引用对象、软引用对象、弱引用对象和虚引用对象。在给定的代码中,并没有使用任何引用类型来包装字符串
"b"
,所以它也不属于可引用对象。综上所述,在给定的代码中,字符串
"b"
是一个强引用对象,它的生命周期取决于具体的引用和使用方式。它不是一个弱引用对象或可引用对象。- 弱引用的作用是确保在没有其他强引用持有对象时,对象可以被垃圾回收器回收。对于字符串常量来说,它们是被 JVM 管理的共享资源,存在于常量池中,并且可以被多个引用共享。因此,字符串常量不需要使用弱引用来确保垃圾回收。
- 所以该Entry对象不会进入引用队列,可以看到最终打印结果:[b:2]
- 如果把
map.put(0, new String("a"), "1");
改成String a = new String("a");map.put(0, a, "1");
该Entry也不会被回收。最终:弱引用自身需要配合引用队列来释放,像软引用一样。
static ReferenceQueue<Object> queue = new ReferenceQueue<>();// 引用队列 static class Entry extends WeakReference<String> { String value; public Entry(String key, String value) { super(key, queue);// 入队 this.value = value; } } public void clean() { Object ref; while ((ref = queue.poll()) != null) {// 检查引用队列中是否有可用的引用对象,并将其从队列中移除并返回。 System.out.println(ref); for (int i = 0; i < table.length; i++) { if(table[i] == ref) { table[i] = null;// 将整个Entry设为null,等待垃圾回收 } } } } Entry[] table = new Entry[4];// 缓存
虚引用(PhantomReference)
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
典型例子是 jdk.internal.ref.Cleaner
释放 java.nio.DirectByteBuffer
关联的直接内存
public class TestPhantomReference {
public static void main(String[] args) throws IOException, InterruptedException {
ReferenceQueue<String> queue = new ReferenceQueue<>();// 引用队列
List<MyResource> list = new ArrayList<>();
list.add(new MyResource(new String("a"), queue));
list.add(new MyResource("b", queue));
list.add(new MyResource(new String("c"), queue));
System.gc(); // 垃圾回收
Thread.sleep(100);
Object ref;
// 循环出队
while ((ref = queue.poll()) != null) {
// 语言级别 '8' 不支持 'instanceof' 中的模式 升级到16
// if (ref instanceof MyResource resource) {
// resource.clean();
// }
if (ref instanceof MyResource) {
((MyResource)ref).clean();
}
}
}
static class MyResource extends PhantomReference<String> {
public MyResource(String referent, ReferenceQueue<? super String> q) {
super(referent, q);
}
// 释放外部资源的方法
public void clean() {
LoggerUtils.get().debug("clean");
}
}
}
代码说明
- day03.reference.TestPhantomReference - 演示虚引用的基本用法
- day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 entry 内存
//import java.lang.ref.Cleaner;
// 前面讲的弱、虚引用配合引用队列,目的都是为了找到哪些 java 对象被回收,从而进行对它们关联的资源进行进一步清理
// 为了简化 api 难度,从 java 9 开始引入了 Cleaner 对象
public class TestCleaner1 {
public static void main(String[] args) throws IOException {
Cleaner cleaner = Cleaner.create();
cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));
cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
MyResource obj = new MyResource();
cleaner.register(obj, ()-> LoggerUtils.get().debug("clean 4"));
cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 5"));
cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 6"));
System.gc();
System.in.read();
}
static class MyResource {
}
}
//import jdk.internal.ref.Cleaner;
public class TestCleaner2 {
public static void main(String[] args) throws IOException {
Cleaner cleaner1 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));
Cleaner cleaner2 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
Cleaner cleaner3 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
Cleaner cleaner4 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 4"));
System.gc();
System.in.read();
}
static class MyResource {
}
}
要求
finalize
finalize 原理
finalize 缺点
代码说明
- day03.reference.TestFinalize - finalize 的测试代码