1.JVM 的内存结构
java 程序是怎么运行的?
首先就是从源文件开始编译成 class 字节码文件,然后 jvm 创建内存区域,开启 main 线程。
- 这个时候会给 main 线程创建一个虚拟机栈,主要就是用来存储线程的参数,变量,和各种返回信息
- 然后调用入口方法前还需要加载类,这个时候就需要加载主类到方法区,存入类的信息,静态变量,常量池等
- 然后就是调用方法里面创建的对象存入堆中
- 接着就是调用类似 hashCode 这样的方法需要用到操作系统的指令,这个时候这样的方法使用的是本地方法栈。
- 而且程序执行需要把字节码全部放进方法区,交给程序计数器来指向下一行要执行的代码
- 接着就是把程序送到各个平台运行就需要解释器把字节码文件翻译成机器码
- 如果有热点代码就需要JIT来缓存机器码,下次不需要再次进行编译(什么是热点代码?)
2. 那些区域会造成内存溢出?
OutOfMemory
- 堆内存,创建对象过多,比如一直 add 对象进 list
- 虚拟栈,创建线程太多
- 方法区,动态创建和加载太多的类对象
StackOverFlow
- 虚拟机栈,方法调用过多,导致栈帧太多
3. 方法区、永久代、元空间的关系?
- 方法区其实就是 jvm 的一个定义,规定有这么一个内存区,永久代 (保存在 java 内存中) 和元空间(保存在本地内存)就是对方法区的一个具体的是实现。
类信息什么时候会加载到元空间?
在类加载器生成,并且创建第一个对象的时候。那么怎么指向元空间中的类信息?通过在堆内存中创建 Class 对象保存内存地址,然后指向元空间中的对象
元空间什么时候移除类信息?
当类加载器下面的对象全部都被 gc 的时候就会清空雷系信息。
4.JVM 内存参数有哪些?分别的作用是什么?
suivivor 是什么?
from+to 区,最小内存值其实就是求 from
- -Xmx 是最大的 jvm 内存,-Xms 是最小的
- -Xmn 新生代的内存(最小和最大相同)
- -XX:NewSize 最小新生代内存,MaxNewSize 最大新生代内存
- -XX:CompressedClassSpaceSize 是方法区中保存类信息的最大内存,MaxMetaspaceSize 是最大的元空间内存,non-classspace 是非类信息空间,class space 是类信息空间内存
- ReserveedCodeCacheSize 如果小于 240 那么全部存入一起,存入的是 JIT 优化的机器码或者是 jvm 自身的一些代码
- 如果大于 240 就要分成 jvm 自身,部分优化,完整优化的缓存空间。
5. 有多少种垃圾回收算法?
- 标记清除
- 标记整理
- 标记复制
过程和作用?
标记清除:标记那些不能被回收的,然后清除掉那些可以回收的。通过不能回收的 root 来找那些间接被引用的对象。root 可以是局部变量引用也可以是静态变量的引用堆内存的对象。会出现什么问题?内存碎片
标记整理:标记不能回收的,清除之后移动那些没被回收的对象到连续内存防止标记清除带来的问题内存碎片
标记复制:准备两个内存块,一个是存入对象的,一个是空的用于复制,标记不能回收,把不能回收的存入空的块,然后交换两个 from 和 to 的指针。
应用场景?
- 标记整理可以应用在老年代因为老年代的存活时间长,如果使用标记复制就需要经常进行复制导致性能下降,而且占据的内存多,因为老年代多,而且存活长。
- 标记清除基本不使用了。
- 标记复制用于新生代,新生代的存活少,复制相对也很少。能够防止内存碎片提高清除的速度。
6. 说说 GC 和垃圾回收器
为什么要 gc?为什么要这么多垃圾回收器,他们之间的区别?
gc 的目的?
gc 的目的是回收无用对象,防止内存碎片,加快分配速度
gc 的重点?
gc 回收谁?堆
gc,how 判断无用?可达性分析和三色标记法
gc 的如何实现?通过各种垃圾回收器
gc 的规模怎么变化?minor(新生代)、mixed(新生代和部分老年代)、full(老年代新生代)
分代回收的作用?
新生代存活短和老年代存活长,不同的存活区那么就要使用不同的垃圾回收策略。假设都是堆在一个区上面导致的问题就是同时使用标记复制,如果区域中存活长的对象多,那么很多没有意义的重新复制。但是对于存活少的时候,那么复制的对象就会少,增加了效率。对于存活长的对象更推荐使用标记整理。
分代回收的区域以及使用的策略是什么?
- 伊甸园:初始的对象区
- 幸存区:from+to,把那些存活相对长的存入幸存区。
- 老年代:如果幸存区存活长或者是大对象(大对象复制需要空间大)那么就存入老年代。
gc 规模的介绍?
- minor gc:标记复制,伊甸园和 from 区域的存活对象复制到 to,并且交换 from 和 to
- mixed gc:新生代 + 部分老年代的垃圾回收,G1 回收器
- full gc:新生代和老年代全部一起回收,暂停时间长。
什么是三色标记?
三色标记就是记录引用的情况,如果黑色说明引用处理完,如果是灰色就是未被处理,白色未处理。在 gc 的作用是什么?
并发漏标问题解决?
什么是并发漏标?
- 用户线程和垃圾回收线程是并发的,假设在垃圾回收的时候,用户线程修改了那些标记的模块,就会导致并发漏标。比如灰色块断开对某个对象的引用,并且垃圾回收开始,但是这个黑色块对象在垃圾回收过程中再次引用白色块,但是由于黑色块已经被标记,不可能再被垃圾回收的可达性分析算法找到白色块导致重新引用的白色被回收
- 第二种情况就是回收过程中新创建对象,黑色引用,但是也是会被回收的
怎么解决?
- 增量更新,监视黑色块,如果发现引用改变,那么先改成灰色再次调用可达性算法分析,等待引用处理完再次进行更新(监视黑色块的重新赋值情况)
- 原始快照,监视那些被删除的引用对象和新加对象,等待回收之后,停止用户线程再次做一遍。(监视新增和垃圾回收中引用的节点)
垃圾回收器了解多少种各自的特点?
- ParallelGc:注重吞吐量,新生代容量不足使用 minorgc,老年代不足时候用 full gc。但是等待时间长(两个 gc 都会 stw 导致等待时间很长)
- CMS:响应时间段,并发标记,重新标记(stw),并发清除。带来什么问题?内存碎片多,如果发生并发失败就会调用 full gc 保底
G1
G1 的特点?
吞吐和响应时间兼顾,主要依靠三个阶段的工作原理。这里没有 from 和 to 了,只有 survivor 和 eden 区
工作原理
- 有 eden、survivor、old、humongous 区,一开始生成固定比例的 eden,如果达到容量,那么就清除并且复制到新的 survivor 区,第二次如果容量不足,旧的 survivor 和 eden 都会复制和迁移那些存活的对象到新的 survivor 区,如果旧 survivor 有对象存活期达到阈值那么就送到老年代。(第一阶段,新生代达到阈值【一般是内存的百分之 5 到 6】,但是老年代还没有)
- 进入第二阶段,老年代如果达到阈值百分之 45 左右,那么就会并发标记不被回收的,并且为了防止并发漏标,这里还会进行一次重新标记(stw,使用的是原始快照,监视那些重新加入的对象)
- 进入第三阶段 mixed 收集之后老年代选择那些回收价值大的区域进行回收。送到另一个老年代。新生代照常。这里老年代也是进行把存活的复制到新的老年代,而不是清除防止内存碎片
- 如果出现并发问题就会恢复到 full gc
7. 什么时候会发生内存溢出?
情况 1:线程池的任务队列溢出
如果一直给线程池添加任务,而且线程池初始化的阻塞队列是无限大小就会导致内存溢出。那么内存溢出导致的问题就是整个程序停止运行。OutOfMemoryError thrown from the UncaughtExceptionHandler in thread “main”. 这个异常的意思其实就是 gc 了,但是没有 gc 到很多对象说明任务对象都在被引用导致的内存溢出问题。
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
while(true){
service.submit(()->{
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
情况 2:线程创建太多导致虚拟栈的内存不足不能继续创建的问题
这里就是因为 cache 无限创建线程导致的线程
OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
while (true) {
executor.submit(()->{
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
情况 3:查询对象太多
对象本身就占用了一部分的内存,如果查询的对象数量太多,100w 条,那么就可能会导致最后内存溢出。
情况 4:动态生成类
如果动态生成很多类,而且类加载器是个长期存活的对象,就会导致 Meta 空间里面类信息无法被回收,最后导致本地内存溢出。
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();
}
}
}
}
8. 类加载的过程、双亲委派机制
类的加载有多少个阶段?
三个阶段
- 加载
- 把类的字节码加载到方法区,并且创建类的对象指向方法区的类信息, 同时把
- 加载类之前先加载父类
- 懒加载
- 链接(验证规范,准备静态空间、符号 -> 直接引用)
- 验证:验证类是否符合 class 规范
- 准备:准备静态变量的空间
- 解析:符号引用变成直接引用
- 初始化
- 静态代码块、静态变量赋值(主要就是对静态变量赋值,合并静态代码块和静态变量赋值为一个块)
- 初始化懒加载。
调用 Student.class 的时候加载类对象成功。并且存在类对象存在于堆内存中,而且 final 的静态变量已经赋值了,在方法区的常量池中赋值。其它静态变量设置默认初始值,等到初始化的时候就会把静态变量赋值
public class TestLazy {
private Class<?> studentClass;
public static void main(String[] args) throws IOException {
System.out.println("未用到 Student");
System.in.read();
System.out.println(Student.class);
System.out.println("已加载 Student");
TestLazy testLazy = new TestLazy();
testLazy.studentClass = Student.class;
System.in.read();
Student stu = new Student();
System.out.println("已初始化 Student");
System.in.read();
}
}
static 变量和 static final 的不同?
主要是赋值阶段不同, static final 在解析的阶段就已经进行赋值了,但是 static 还需要在初始化阶段的时候混合 static 块中集中进行赋值。这里的 #x 的意思就是在方法区运行时常量池中查找。也就是静态变量实际上存在方法区,开辟空间给静态变量赋值。
- static final 必须是基本类型才能够在解析的时候初始化
- static 在初始化阶段把 static 块和变量结合成新的新 cinit 方法进行初始化
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 153
static final int m;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 32768
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 119
2: putstatic #18
5: getstatic #21
8: ldc #27
10: invokevirtual #29
13: sipush 136
16: putstatic #35
19: new #4
22: dup
23: invokespecial #3
26: putstatic #38
29: return
final static 的基本类型和引用类型的区别?
基本类型是直接复制一份到引用它的那个类的常量池上面。引用类型仍然要去访问类对象的属性,所以类需要进行初始化。可以看看下面部分代码
Code:
stack=2, locals=1, args_size=1
0: getstatic #7
3: sipush 153
6: invokevirtual #15
9: getstatic #21
12: invokevirtual #25
15: pop
16: getstatic #7
19: ldc #31
21: invokevirtual #15
24: getstatic #21
27: invokevirtual #25
30: pop
31: getstatic #7
34: getstatic #32
37: invokevirtual #36
40: getstatic #21
43: invokevirtual #25
46: pop
47: return
public class TestFinal {
public static void main(String[] args) throws IOException {
System.out.println(Student.c);
System.in.read();
System.out.println(Student.m);
System.in.read();
System.out.println(Student.n);
System.in.read();
}
}
解析的符号引用变成直接引用?
其实意思就是在链接,因为本文件里面只知道有 A 这个符号,但是没有 A 这个类文件,所以要等待类 A 也被加载的时候才能够链接上,给对应的 class 进行赋值,下面的 B 明显还没有创建,所以是 unresolvedClass(参考程序员的自我修养链接部分)
什么是双亲委派机制?
类加载器加载类时优先问上一级。比如一个 Student 类,在 app 类加载器中准备加载的时候都会优先去问一下 Extension 加载没,能不能加载,然后 Extension 先去问 BootStrap 加载没能不能加载,然后才会查询自己的路径,再返回给 App,如果没有 app 再在自己的类路径下查找。
类加载器的类型?
- Bootstrap(无法直接访问) 访问路径 jre/lib
- Extension 访问路径 jre/lib/ext
- Application 类路径
- 自定义
能不能自己写一个 java.lang.System?
不能,原因是
- 如果你要自己写一个的话,使用的是自定义的类加载器放弃双亲委派,并且加载外面的 java.lang.System 是不行的,原因 System 继承了 Object,那么类加载器应该优先加载父类,但是没有双亲委派机制,导致类加载器不能够加载 Object,如果没有 Object 就更不可能加载这个 System。
- 如果是双亲委派那么肯定就是优先加载核心类 System 先
- 而且 java 开头的包需要 PlatformClassLoader(ExtensionClassLoader)才能够进行加载。而且加载 java.lang 的是 BootstrapClassLoader。
双亲委派的目的?
- 优先加载核心类,并且共享给下级的类加载器使用
- 让类的加载有先后
9. 四种引用的作用
强引用
相当于就是 root,局部变量或者是静态变量指向的对象
软引用
就是在引用中间增加了软引用,第一次回收不会回收,但是第二次内存不够就会回收软引用的
弱引用
与软引用相似,但是第一次回收就会被回收掉.。
在 ThreadLocalMap 里面需要用到引用队列,因为 key 是弱引用但是 value 是强引用导致最后出现内存溢出。但是引用队列,可以取出这些引用并且进行清理操作。 像这种清理,就是删除了对象 A,但是仍然有外部关联资源 value,这个时候就要通过引用队列来进行删除
public class TestWeakReference {
public static void main(String[] args) {
MyWeakMap map = new MyWeakMap();
map.put(0, new String("a"), "1");
map.put(1, new String("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);
}
static class MyWeakMap {
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();
}
}
}
简化处理的类 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 {
}
}
虚引用
一定要配合引用队列,为了在回收虚引用的对象之后,还要释放对象关联的外部资源
但是如果字符串是常量 “b” 那么就会有一个引用直接存入串池中,并且引用指向这个 “b” 的字符串对象,那么就不会清除了
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) {
if (ref instanceof MyResource resource) {
resource.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");
}
}
}
10.finalize 为什么不好?
补充这个是在垃圾回收器确定对象不使用的时候进行调用。而且线程是提前开启的,等待垃圾回收的信息。下面有个 while 循环。
- finalize 会使用 finalizer 来释放资源,清理,但是他是一个守护线程需要等待它完成之后才能让 main 线程关闭。
- 无法处理异常
- 而且每次在执行 gc 的时候都没办法及时回收需要等待 finalize 出队之后才能回收,而且 finalize 由于要上锁所以速度非常慢,而不是因为优先级低的问题。
private Finalizer(Object finalizee) {
super(finalizee, queue);
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, null, "Finalizer", 0, false);
}
public void run() {
if (running)
return;
while (VM.initLevel() == 0) {
try {
VM.awaitInitLevel(1);
} catch (InterruptedException x) {
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
}
}
}
}
结构是什么
通过把对象放进 Finalizer 里面,然后连接成一个双向链表,加入到引用队列。