黑马面试题JVM篇总结
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();
// ExecutorService executor = Executors.newFixedThreadPool(2);
while (true) {
executor.submit(()->{
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
情况3:查询对象太多
对象本身就占用了一部分的内存,如果查询的对象数量太多,100w条,那么就可能会导致最后内存溢出。
情况4:动态生成类
如果动态生成很多类,而且类加载器是个长期存活的对象,就会导致Meta空间里面类信息无法被回收,最后导致本地内存溢出。
// -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();
}
}
}
}
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); // 关键代码1,会触发类加载
System.out.println("已加载 Student");
TestLazy testLazy = new TestLazy();
testLazy.studentClass = Student.class;
System.in.read();
Student stu = new Student(); // 关键代码2,会触发类初始化
System.out.println("已初始化 Student");
System.in.read();
}
}
static变量和static final的不同?
主要是赋值阶段不同, static final在解析的阶段就已经进行赋值了,但是static还需要在初始化阶段的时候混合static块中集中进行赋值。这里的#x的意思就是在方法区运行时常量池中查找。也就是静态变量实际上存在方法区,开辟空间给静态变量赋值。
- static final 必须是基本类型才能够在解析的时候初始化
- static在初始化阶段把static块和变量结合成新的新cinit方法进行初始化
static final int c;//声明,创建堆内存空间给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 // #18方法区的常量池中的静态变量a赋值为119
5: getstatic #21 // java/lang/System.out:Ljava/io/PrintStream;取出静态类
8: ldc #27 // String Student.class init 取出常量池的字符串。
10: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: sipush 136
16: putstatic #35 // Field b:I
19: new #4 // 创建#4方法区的那个Object对象
22: dup//复制引用,一个用于调用方法,一个用于存入堆内存初始化的n属性
23: invokespecial #3 // Method java/lang/Object."<init>":()V
26: putstatic #38 // Field n:Ljava/lang/Object;
29: return
final static的基本类型和引用类型的区别?
基本类型是直接复制一份到引用它的那个类的常量池上面。引用类型仍然要去访问类对象的属性,所以类需要进行初始化。可以看看下面部分代码
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: sipush 153 //复制一份直接取出赋值
6: invokevirtual #15 // Method java/io/PrintStream.println:(I)V
9: getstatic #21 // Field java/lang/System.in:Ljava/io/InputStream;
12: invokevirtual #25 // Method java/io/InputStream.read:()I
15: pop
16: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #31 // int 32768//复制一份直接取出赋值
21: invokevirtual #15 // Method java/io/PrintStream.println:(I)V
24: getstatic #21 // Field java/lang/System.in:Ljava/io/InputStream;
27: invokevirtual #25 // Method java/io/InputStream.read:()I
30: pop
31: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
34: getstatic #32 // Field 但是这个地方很明显就是直接访问Student的属性day03/loader/Student.n:Ljava/lang/Object;
37: invokevirtual #36 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
40: getstatic #21 // Field java/lang/System.in:Ljava/io/InputStream;
43: invokevirtual #25 // Method java/io/InputStream.read:()I
46: pop
47: return
public class TestFinal {
public static void main(String[] args) throws IOException {
System.out.println(Student.c); // c 是 final static 基本类型
System.in.read();
System.out.println(Student.m); // m 是 final static 基本类型
System.in.read();
System.out.println(Student.n); // n 是 final static 引用类型
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);
}
// 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
static class MyWeakMap {
// static ReferenceQueue<Object> queue = new ReferenceQueue<>();
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);
// push onto unfinalized
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);//开启线程执行finalize
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() {
// in case of recursive call to run()
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (VM.initLevel() == 0) {
// delay until VM completes initialization
try {
VM.awaitInitLevel(1);
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
结构是什么
通过把对象放进Finalizer里面,然后连接成一个双向链表,加入到引用队列。