黑马-JVM

学习路线
黑马-JVM_第1张图片

内存结构

1、程序计数器(线程私有)

1.1定义

作用:记住下一条jvm指令的执行地址

特点

  • 线程私有的
  • 唯一不会存在内存溢出的区域

1.2作用

Java源代码->二进制字节码(jvm指令)->【解释器解释】机器码->CPU执行

2、虚拟机栈(线程私有)

2.1定义

栈:线程运行时需要的内存空间
栈帧:每个方法运行时需要的内存,栈由多个栈帧组成

每个线程只能有一个活动的栈帧,对应着当前正在执行的那个方法

栈的演示
public class test {

    public static void main(String[] args) {
        m1();
    }

    public static void m1(){
        m2(1,2);
    }
    public static int m2(int a,int b){
        int c = a + b;
        return c;
    }
}

黑马-JVM_第2张图片

问题辨析

1、垃圾回收是否涉及栈内存?
不需要!栈帧出栈的时候就已经释放掉了

2、栈内存分配越大越好吗?
不是!栈内存太大反而影响到线程数目,采用系统默认的大小即可

3、方法内的局部变量是否线程安全?
如果方法内部局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.2栈内存溢出(StackOverFlowError)

什么情况下会发生栈内存溢出
  • 栈帧过多(递归调用)
  • 栈帧过大
设置栈内存大小

-Xss256k

2.3线程运行诊断

案例1:CPU占用过多

  • 用top定位哪个进程对CPU的占用高

    top
    
  • 用ps命令进一步定位是哪个线程引起的CPU占用过高(显示的线程是10进制)

    ps -H -eo pid,tid,%cpu | grep 进程id
    
  • 用jstack工具将进程中的所有线程列出来(显示的线程id是16进制,因此查找问题的时候需要先将上一步查到的线程id换算成16进制),可以根据线程id找到有问题的线程,进一步定位到有问题的代码行数

    jstack 进程id
    

案例2:程序运行很长时间没有结果

jstack 进程id

3、本地方法栈(线程私有)

Java虚拟机调用本地方法(Native Method)时提供的内存空间

4、堆(线程共享)

4.1定义

Heap堆:通过new关键字创建的对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

4.2堆内存溢出(java.lang.OutOfMemoryEeror:Java heap space)

4.3 堆内存诊断

1、jps工具

查看当前系统中有哪些Java进程

2、jmap工具

查看堆内存占用情况(只能查看某一时刻)

jmap -heap 进程id

3、jconsole工具

图形界面的,多功能的检测工具,可以连续监测

案例

垃圾回收之后,内存占用依然很高

解决方法:使用jvisualvm可视化工具dump下堆内存快照进行分析
黑马-JVM_第3张图片

5、方法区(线程共享)

5.1定义

存储了跟类的结构相关的一些信息,包括类的成员变量、方法数据、成员方法和构造器方法的代码等。

5.2组成

黑马-JVM_第4张图片

黑马-JVM_第5张图片
重要!:

1、方法区在虚拟机启动时被创建,逻辑上是堆的组成部分
2、方法区是规范,永久代(jdk1.6,占用的是堆内存)和元空间(jdk1.8,占用的是系统的内存)只是实现。
3、注意看元空间里面的包含:类、类加载器、常量池
4、StringTable1.6是放在方法区,1.8则放到了堆空间

5.3方法区内存溢出(java.lang.OutOfMemoryEeror:Metaspace)

  • jdk1.8以前会导致永久代内存溢出:java.lang.OutOfMemoryEeror:PermGen space
    需要先设置元空间内存大小,方便演示:-XX:MaxMetaspaceSize=8m

  • jdk1.8之后会导致元空间内存溢出:java.lang.OutOfMemoryEeror:Metaspace

5.4运行时常量池

一文搞懂各种常量池

运行时常量池:常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的地址。

public class Test2 {
    //StringTable是一个hashtable结构,不能扩容
    public static void main(String[] args) throws InterruptedException {
        // 常量池中的信息,都会被加载到运行时常量池中,这时a b ab都是常量池中的符号,还没有变为Java字符串对象
        String s1 = "a";// 只有执行到这一段代码时,a才会在StringTable中创建
        String s2 = "b";
        String s3 = "ab";

        String s4 = s1 + s2;// new StringBuilder().append(s1).append(s2).toString();  StringBuilder().toString()-->new String("ab");

        String s5 = "a" + "b";// 创建一个字符串String ab,先去常量池中查找,已经有了,就不创建了。底层为编译期的优化,已经有一个了就不另外创建了

        // s3是在常量池中;s4是一个对象,存放在堆中 答案就是false
        System.out.println(s3 == s4);
        // 他俩其实就是同一个
        System.out.println(s3 == s5);

        // 总结:所有的字符串的对象都是懒惰的,只有在用到时才会加载到StringTable中,加载的时候会先查找,没有才会放进去,有了就直接使用
    }
}

5.5StringTable特性

1、常量池中的字符串仅仅是符号,第一次用到时才变为对象
2、利用串池的机制,来避免重复创建字符串
3、字符串变量的拼接原理时StringBuilder
4、字符串常量拼接的原理是编译期优化
5、可以使用intern方法,主动将串池中还没有的字符串对象放入串池

public class Test3 {
    public static void main(String[] args) {
        // 此时的"a","b"都在常量池中:["a","b"]
        String s = new String("a") + new String("b");// 相当于new String("ab"),存放于堆中

        // 将这个字符创对象尝试放入串池,如果有则不放入,如果没有则放入串池,会把串池中的对象返回
        String s2 = s.intern();

        System.out.println(s2 == "ab");
    }
}

5.6StringTable位置(堆中)

jdk1.6在永久代中,不易被回收
jdk1.8之后在堆中,majorGC就可以回收掉,节约空间

5.7StringTable垃圾回收

StringTable中存在垃圾回收

5.8StringTable性能调优

StringTable底层是一个HashTable,因此调优就是调整HashTable桶的个数:
-XX:StringTableSize=10000
总结:当项目中字符串很多时,可以考虑调整StringTable,增加桶的个数,减少Hash碰撞

6、直接内存(系统内存)

直接内存
1、常见于NIO操作时,用于数据缓冲区
2、分配成本较高,但读写性能高
3、不受JVM内存回收管理

黑马-JVM_第6张图片
操作系统专门划出来一块内存,供Java直接使用,当然,操作系统也可以使用。

public class Demo4 {
    
    static int _100Mb = 1024 * 1024 * 100;
    
    public static void main(String[] args) {
        ByteBuffer allocate = ByteBuffer.allocate(_100Mb);
    }
}

7、垃圾回收

7.1如何判断对象可以回收

1、引用计数法

黑马-JVM_第7张图片

引用计数法的弊端:循环引用

2、可达性分析算法
  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为GC Root?
    • System Class:系统核心类,如Object、HashMap、String
    • Native Stack:本地方法
    • Thread:活动线程和使用的对象
    • Busy Monitor:被加锁的对象

首先确定一系列根对象,何为根对象?即不可能被回收的对象

3、四种引用

黑马-JVM_第8张图片

  • 强引用
    描述:直接和GC Root相连的对象,如上图的A1对象,
    回收时机:即使垃圾回收完内存不够也不会被回收掉

  • 软引用
    描述:不直接和GC Root相连
    回收时机:垃圾回收结束内存还是不够时,会回收掉软引用
    应用:当读取一些大文件时,可以将读取的文件设为软引用,重读读取时可以避免内存溢出

    public class Test4 {
    
    private static final int _4MB = 4 * 1024 * 1024;
    
    public static void main(String[] args) {
        soft();
    }
    
    public static void soft(){
        ArrayList<SoftReference<byte[]>> softReferences = new ArrayList<>();
    
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    
        for (int i = 0; i < 5; i++) {
            // 关联引用队列,当软引用引用的byte数组被回收时,软应用自己就会加入引用队列中去
            SoftReference<byte[]> softReference = new SoftReference<>(new byte[_4MB],queue);
            System.out.println(softReference.get());
    
            softReferences.add(softReference);
            System.out.println(softReferences.size());
        }
    
        System.out.println("循环结束");
    
        // 清除无用的软引用本身
        Reference<? extends byte[]> poll = queue.poll();
        if (poll != null){
            softReferences.remove(poll);
            poll = queue.poll();
        }
    
        System.out.println("==============================");
        
        for (SoftReference<byte[]> softReference : softReferences) {
            System.out.println(softReference.get());
        }
    }
    

}
```

  • 弱引用
    描述:不直接和GC Root相连
    回收时机:只要发生垃圾回收,就回收掉弱引用
  • 虚引用(必须配合引用队列使用,主要配合ByteBuffer使用)
    描述:当虚引用创建的时候,必须关联一个引用队列
    回收时机:虚引用的对象被垃圾回收时,虚引用对象自己就会进入引用队列,从而间接地调用一个线程调用虚引用的方法,然后调用Unsafe.freeMemory区释放直接内存
    描述:当虚引用创建的时候,必须关联一个引用队列
  • 终结器应用(必须配合引用队列使用)

7.2垃圾回收算法

1、标记清除

黑马-JVM_第9张图片

优点:速度快
缺点:容易产生内存碎片,导致内存不连续

2、标记整理

标记过程和“标记-清除算法”一样,但后续步骤是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
优点:不会产生内存碎片
缺点:由于整理要移动对象,导致效率较低

3、复制

黑马-JVM_第10张图片
黑马-JVM_第11张图片
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。
优点:不会产生内存碎片
缺点:要占用双倍的内存空间

7.3分代垃圾回收

1、垃圾回收过程

黑马-JVM_第12张图片
黑马-JVM_第13张图片
黑马-JVM_第14张图片

新创建的对象,默认会存放在伊甸园区,当伊甸园区满了之后,就会触发一次Minor GC,Minor GC采用复制算法,将存活的对象放入幸存区TO中,同时给存活的对象的寿命+1(此时伊甸园区就回收掉释放出空间),Minor GC完成之后,会交换幸存区FROM和幸存区TO。

当伊甸园区再次满了之后,开始第二次垃圾回收,同样会将幸存的对象放入幸存区FROM,同时会对幸存区TO中进行一次垃圾回收,没有被回收掉的对象同样会被放进幸存区FROM,而且寿命+1,最后,交换幸存区FROM和幸存区TO

当幸存区FROM中的对象的寿命达到15时,会被放倒老年代中。老年代满了之后,会触发一次Minor GC,如果回收之后空间仍然不足,那么触发Full GC

重要:Minor GC算法:复制,Full GC算法:标记清除/标记清理

2、相关VM参数

黑马-JVM_第15张图片

7.4垃圾回收器

JDK8默认的垃圾收集器为: Parallel Scavenge(新生代,标记复制算法)+ Serial Old(老年代,标记整理算法)

1、串行(Serial【串行】)

特点:单线程、堆内存较小,CPU核心数比较少

开启串行垃圾回收器:
-XX:+UseSerialGC = Serial + SerialOld
说明:Serial工作在新生代,采用复制算法,SerialOld工作在老年代,采用标记整理算法
黑马-JVM_第16张图片
相关的垃圾收集器:
1、ParNew,实质上是Serial收集器的多线程并行版本,常用搭配:ParNew(新生代)+CMS(老年代)
2、Serial Old:Serial收集器的老年代版本,采用标记整理算法,常用搭配:Parallel Scavenge(新生代)+ Serial Old(老年代)

2、吞吐量优先(Parallel【并行】)

特点:多线程、堆内存较大,多核CPU、让单位时间内,STW的时间最短,多个垃圾回收线程同时运行,但用户线程会暂停

开启并行的垃圾回收器:
-XX:+UseParallelGC(新生代、复制算法) ~ -XX:+UseParallelOldGC(老年代、标记整理算法)
黑马-JVM_第17张图片
相关的垃圾收集器:
1、Parallel Scavenge:新生代收集器,基于标记复制算法,常用搭配:Parallel Scavenge(新生代)+ Serial Old(老年代)
2、Parallel Old:是Parallel Scavenge的老年代版本,基于标记整理算法

二者搭配就是JDK8默认的垃圾收集器

3、响应时间优先(CMS【并发】)

特点:多线程、堆内存较大,多核CPU、垃圾回收时尽可能让单次STW的时间最短、工作的同时用户线程也能并发运行

开启:
-XX:+UseConcMarkSweepGc(老年代) ~ -XX:+UseParNewGC(新生代) ~ SerialOld(并发失败时的补救措施)
-XX:+CMSScavengeBeforeRemark:由于重新标记比较耗时,因此在重新标记前就清理一下新生代,降低标记时长

常用搭配:ParNew(新生代)+CMS(老年代)

面试必答:
1、基于标记清除算法实现
2、工作流程是:初始标记、并发标记、重新标记、并发清除
3、缺点:
1、会产生内存碎片(因为是基于标记清除算法,解决:增大Full GC的频率)
2、重新标记阶段比较耗时(解决:在执行重新标记之前先做一次Young GC)
3、Promotion Failed问题(导致的原因是在进行Minor GC的时候,老年代空间不足,无法放下大对象,解决:对内存占用率达到60%的时候就开始GC)

黑马-JVM_第18张图片
重点:初始标记和重新标记存在STW

重新理解CMS

4、G1

特点:
1、同时注重吞吐量和低延迟,默认暂停时间是200ms
2、超大堆内存,会将堆划分为多个大小相等的Region
3、整体上是标记+整理算法,两个区域之间是复制算法

相关参数:
-XX:UseG1GC
-XX:G1HeapRegionSize=size
-XXMaxGCPauseMillis=time

面试必答:
1、特点:同时追求高吞吐量和低延时、能够预测GC停顿时间。
2、内存布局:把堆内存划分为多个大小相等的独立区域即Region。每一个Region都可以根据需要去扮演Eden区,Survivor区,Old区,Humongous区。(注:Humongous区是一类特殊区域专门存放大对象)。
3、回收过程:在回收时以Region作为最小的回收单位,G1会预测出每个Region的回收时间,回收后得到的内存大小以此计算出的该Region回收的"价值",根据用户设置的期望GC停顿时间,每次回收优先处理回收价值最大的Region,这也是G1(Garabage First)名字的由来原因。
4、好处:一次回收不用针对全部内存,只需要先回收垃圾最多的region,提高了垃圾收集的效率、变相实现了只有新生代才有的复制算法,极大减少了空间碎片的产生(后续会提到)
5、缺点:跨region引用(解决:引入了RSet(Remembered Set),用于记录其他Region指向该Region的引用)

重新理解G1

5、Full GC概念辨析
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集
      • 在并发收集的阶段,回收速度高于垃圾产生的速度:并发标记
      • 在并发收集的阶段,回收速度低于垃圾产生的速度:full gc
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足(老年代占用堆空间大于45%)发生的垃圾收集
      • 在并发收集的阶段,回收速度高于垃圾产生的速度:并发标记
      • 在并发收集的阶段,回收速度低于垃圾产生的速度:full gc
6、三色标记

三色标记详解

7、G1回收器的优化

1、字符串去重
创建两个相同的字符串:

String s1 = new String("abc");// char[]{'a','b','c'}
String s2 = new String("abc");// char[]{'a','b','c'}

将所有新分配的字符串放入一个队列,当新生代回收时,G1并发检查时否有字符串的重复,如果他们值一样,让他们引用同一个char[]

2、并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用时,则卸载它所加载的所有类
3、回收巨型对象
一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时被有限考虑。
4、并发标记起始时间

7.5垃圾回收调优

8、类加载与字节码技术

8.1类文件结构

8.2字节码指令

8.3语法糖

8.4类加载阶段

类加载的阶段:加载-链接-初始化

1、加载
  • 将类的字节码载入方法区中,内部采用C++的instanceKlass描述Java类,他的重要field有:
    • _java_mirror即Java的类镜像
    • _super即父类
    • _fields即成员变量
    • _methods即方法
    • _constansts即常量池
  • 如果这个类还有父类没有加载,就先加载父类
  • 加载和链接可能是交替运行的
    黑马-JVM_第19张图片
    用关键字new创建一个Person的实例对象,实例对象都有自己的对象头(16字节),其中8个字节对应着class对象地址,如果想访问实例对象的属性,首先会通过class地址找到java mirror地址(上图中绿色部分),再通过instanceKlass地址找到元空间中instanceKlass,就可以调用这个类的属性信息了。
2、链接

链接阶段又分为三个小的阶段

  • 验证:验证类是否符合JVM规范,安全性检查
  • 准备:为static变量分配空间,设置默认值
    • static分配空间和赋值是两个步骤,分配空间在准备阶段完成,复制在初始化阶段(下一步)完成
    • 如果static变量时final的基本类型,那么编译阶段值就确定了,复制在准备阶段完成
    • 如果static变量的是是final的,但是属于引用类型,那么复制也会在初始化阶段(下一步)完成
  • 解析:将常量池中的符号引用解析为直接引用
3、初始化

初始化即调用类的构造方法,虚拟机保证这个类的构造方法的线程安全

概括得说,类初始化是【懒惰的】,以下几种情况会导致类的初始化:

  • main方法所在类,总是被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类的初始化,如果父类还没初始化,会引发父类的初始化
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new关键字会导致初始化

不会导致类初始化的情况:

  • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化
  • Class.forName的参数2为false时(人为指定)

8.5、类加载器

1、以JDK8为例:

黑马-JVM_第20张图片

2、双亲委派模式

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成加载。

你可能感兴趣的:(面试题集锦,java)