JVM详解

JVM

  • 1.JVM的好处
  • 2.JVM的内存结构
    • (1)程序计数器
    • (2)虚拟机栈
      • 定义
      • 问题辨析
      • 栈内存溢出
      • 线程运行诊断
    • (3)本地方法栈
    • (4)堆
      • 堆内存溢出
      • 堆内存诊断
      • 垃圾回收之后,内存占用仍然很高
    • (5)方法区
      • 定义
      • 内存溢出
      • 运行时常量池
      • StringTable(字符串常量池)
    • (6)直接内存
      • 直接内存释放原理
      • 禁用显示回收对直接内存的影响
  • 3.垃圾回收
    • (1)如何判断对象是否可以被回收
      • 引用计数法
      • 可达性分析法
  • 4.四种引用
    • (1)强引用
    • (2)软引用
    • (3)弱引用
    • (4)虚引用
  • 5.垃圾回收算法
    • (1)标记清除
    • (2)标记整理
    • (3)复制
    • (4)分代回收
    • (5)GC
      • GC相关参数
    • (6)垃圾回收器
      • 串行垃圾回收器
      • 吞吐量优先垃圾回收器
      • 响应时间优先垃圾回收器
    • (7)G1
      • 新生代回收
      • 新生代回收+并发标记
      • 混合回收
      • Full GC
      • 新生代回收的跨代引用(老年代引用新生代)问题
      • Remark
      • 字符串去重
      • 并发标记时的类卸载
      • 回收巨型对象
      • 并发标记起始时间的调整
    • (8)GC调优
  • 6.类加载
    • (1)类文件结构
      • 魔数
      • 版本
      • 常量池
    • (2)字节码指令
      • 将class文件中常量池信息载入运行时常量池
      • 方法字节码载入方法区
      • main线程开始运行 分配栈帧内存
      • 执行引擎开始执行字节码
    • (3)javap
    • (4)类加载
      • 加载
      • 链接
      • 初始化
    • (5)类加载器
      • 启动类加载器
      • 扩展类加载器
      • 应用程序类加载器
      • 自定义类加载器
      • 双亲委派

1.JVM的好处

(1)一次编译,到处运行

(2)自动内存管理,垃圾回收功能

(3)数组下标越界检查

(4)多态

JVM详解_第1张图片

2.JVM的内存结构

(1)程序计数器

(1)记住下一条JVM指令的执行地址

(2)使用寄存器做程序计数器

(3)线程私有的

(4)不会存在内存溢出

(2)虚拟机栈

JVM详解_第2张图片

定义

(1)每个线程运行时所需要的内存

(2)每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存

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

问题辨析

(1)垃圾回收不涉及栈内存,因为每一个方法对应一个栈帧,每个方法运行结束后,栈帧就会自动出栈

(2)栈内存不是分配越大越好,因为栈内存分配越大,线程数量就会越少

(3)方法内的局部变量是否线程安全?
	如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
	如果局部变量引用了对象,并逃离了方法的作用范围,它就存在线程安全的风险

栈内存溢出

(1)栈帧过多导致栈内存溢出(方法递归调用时)

(2)栈帧过大

线程运行诊断

(1)某个程序CPU占用过多
	第一步:用 top 定位哪个进程对CPU的占用过高
	第二步:用 ps H -eo pid,tid,%cpu | grep 进程id 来定位这个进程中哪个线程对CPU占用过高
	第三步:用 jstack 进程id ,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

(2)程序运行很长时间没有结果
	第一步:jstack 进程id :划到最后可以看出可能是线程死锁

(3)本地方法栈

给本地方法运行提供内存空间

(4)堆

(1)通过new关键字创建的对象都会使用堆内存

(2)是线程共享的,堆中对象都需要考虑线程安全的问题

(3)有垃圾回收机制

堆内存溢出

public class TestOOM {

    public static void main(String[] args) {
        int i=0;
        try {
            List<String> list=new ArrayList<>();
            String a="hi";
            while (true){
                list.add(a);
                a=a+a;
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);
        }
    }

}

JVM详解_第3张图片

堆内存诊断

(1)jps工具
	查看当前系统中有哪些java进程

(2)jmap工具
	jmap -heap 进程id :查看堆内存占用情况

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

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

(1)jps:查看进程id

(2)jmap -heap 进程id :查看堆内存占用

(5)方法区

定义

(1)线程共享的

(2)存储了和类的结构相关的数据,类的成员变量、方法数据、成员方法以及构造器方法的代码

(3)在JVM启动时被创建

(4)JDK1.7时方法区(永久代)在堆内存,JDK1.8(元空间)在直接内存

JVM详解_第4张图片

内存溢出

(1)JDK1.8之后,方法区在直接内存,没有设置上限,不容易出现方法区内存溢出

运行时常量池

(1)常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等

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

StringTable(字符串常量池)

(1)常量池中的字符串仅是符号,第一次用到时才变为对象

(2)利用字符串常量池的机制,来避免重复创建字符串对象

(3)字符串变量拼接的原理是StringBuilder

(4)字符串常量拼接的原理是编译器优化

(5)可以使用 intern 方法,主动将字符串常量池中还没有的字符串对象放入串池
	将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
public class TestStringTable {

    public static void main(String[] args) {
        //StringTable ["a" "b" "ab"]
        /**
         * 常量池中的信息都会被加载到运行时常量池中
         * 这时 a b ab 都是常量池中的符号,还没有变成字符串对象
         * ldc #2 会把 a 符号变为 "a" 字符串对象
         * ldc #3 会把 b 符号变为 "b" 字符串对象
         * ldc #4 会把 ab 符号变为 "ab" 字符串对象
         */
        String s1="a";
        String s2="b";
        String s3="ab";
        String s4=s1+s2;//new StringBuilder().append("a").append("b").toString() 堆中:new String("ab")
        String s5="a"+"b";//javac在编译期间做了优化 结果已经在编译器确定为ab

        System.out.println(s3==s4);//false
        System.out.println(s3==s5);//true
    }

}
public class TestStringTable {

    public static void main(String[] args) {
        String s=new String("a")+new String("b");
        /**
         * 此时:
         * 字符串常量池中:["a","b"]
         * 堆中:new String("a")  new String("b")  new String("ab")
         */
        //将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回
        String s2=s.intern();
        //此时字符串常量池:["a","b","ab"]
        System.out.println(s2==s);//true
        System.out.println(s2=="ab");//true
        System.out.println(s=="ab");//true
    }

}

public class TestStringTable {

    public static void main(String[] args) {
        String x="ab";
        String s=new String("a")+new String("b");
        /**
         * 此时:
         * 字符串常量池中:["a","b","ab"]
         * 堆中:new String("a")  new String("b")  new String("ab")
         */
        //将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回
        String s2=s.intern();

        System.out.println(s2==x);//true
        System.out.println(s==x);//false
        System.out.println(s=="ab");//false
    }

}

垃圾回收

(1)当空间不足时,将没有用的字符串常量垃圾回收

性能调优

(1)调整StringTable的大小

(2)将StringTable的size调大,可以减少哈希冲突,速度会变快

(3)可以使用 inter() 来避免重复的字符串对象入池,从而减少需要的内存

(6)直接内存

(1)常见于NIO操作时,用于数据缓冲区

(2)分配回收成本较高,但读写性能高

(3)不受JVM内存回收管理

JVM详解_第5张图片

JVM详解_第6张图片

直接内存释放原理

(1)使用 Unsafe 类分配和释放内存,并且回收时需要主动调用 freeMemory()

(2)ByteBuffer的实现类内部使用了 Cleaner(虚引用) 来监测ByteBuffer对象,一旦
	ByteBuffer对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的clean
	方法调用freeMemory方法来释放内存

禁用显示回收对直接内存的影响

(1)为了不影响程序执行效率,可能会禁用 System.gc(),这样的话就不会自动进行直接内存回收

(2)需要我们手动调用 unsafe.freeMemory() 来释放直接内存

3.垃圾回收

(1)如何判断对象是否可以被回收

引用计数法

(1)某个对象被其它对象引用,则计数+1;如果该引用释放,则计数-1,当计数为0时,就可被回收

(2)但存在循环引用,也就是两个对象互相引用的情况

可达性分析法

(1)确定根对象,扫描堆内存中所有的对象,如果某个对象被根对象直接或间接引用,则标记它,
	表示该对象不能被回收;没有被标记的对象则可以被垃圾回收

(2)哪些对象可以作为根对象:java虚拟机栈中的引用的对象,方法区中的类静态属性引用对象
				        方法区中的常量引用对象,本地方法栈中的引用对象

4.四种引用

JVM详解_第7张图片

(1)强引用

(1)平常通过 = 创建的对象

(2)只有当没有引用指向该对象时,该对象才可以被垃圾回收

(2)软引用

(1)当发生垃圾回收并且内存不足时,一次垃圾回收后内存依然不足,只有软引用引用的对象会被回收

(2)一些不重要的数据可以用软引用,在内存不足时可以进行回收

(3)如果使用了引用队列,当软引用引用对象被回收时,该软引用会放入引用队列,
	是为了回收软引用
public class Test {

    private static final int _4MB=4*1024*1024;

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

    //软引用
    private static void soft(){
        //list --强引用--> SoftReference --弱引用--> byte[]
        List<SoftReference<byte[]>> list=new ArrayList<>();
        for (int i=0;i<5;i++){
            SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB]);
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println(list.size());//1
    }

    //带队列的软引用
    private static void soft1(){
        //list --强引用--> SoftReference --弱引用--> byte[]
        List<SoftReference<byte[]>> list=new ArrayList<>();
        //引用队列
        ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
        for (int i=0;i<5;i++){
            //关联了引用队列,当软引用所指向的byte[]被回收时,软引用自己会加入到queue中
            SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB],queue);
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println(list.size());//1
        //从队列中获取无用的软引用对象,并移除
        Reference<?extends byte[]> poll= queue.poll();
        while (poll!=null){
            list.remove(poll);
            poll= queue.poll();
        }
    }

}

(3)弱引用

(1)只要发生垃圾回收,不管内存充不充足,只有弱引用引用的对象都会被回收

(2)如果使用了引用队列,当弱引用引用对象被回收时,该弱引用会放入引用队列,
	是为了回收弱引用

(4)虚引用

(1)虚引用必须配合引用队列使用

5.垃圾回收算法

(1)标记清除

(1)将没有被根对象引用的对象标记

(2)将被标记的对象释放(将垃圾对象的起始和结束地址进行记录)

(3)比较快,但容易产生内存碎片

(2)标记整理

(1)将没有被根对象引用的对象标记

(2)在清除垃圾的过程中,将可用的对象移向一端

(3)不会造成内存碎片问题,效率低

(3)复制

(1)将内存区域划分为大小相等的区域

(2)将没有被根对象引用的对象标记

(3)将from区域存活的对象复制到to区域

(4)清除from中的垃圾,交换from和to的位置

(5)不会存在内存碎片,需要双倍内存空间

JVM详解_第8张图片

(4)分代回收

JVM详解_第9张图片

(1)对象首先分配在伊甸园区域

(2)新生代空间不足时,触发 Minor GC,伊甸园和from存活的对象使用复制算法复制到to中,
	存活的对象年龄+1,释放垃圾,交换from和to
	Minor GC 会引发 stop the world,暂停其它用户线程,等垃圾回收结束,用户线程恢复运行

(3)当对象寿命超过阈值(最大15次)时,会晋升到老年代

(4)当老年代空间不足时,会先进行 Minor GC,当 Minor GC 后空间依然不足,会触发 Full GC
	Full GC 也会引发 stop the world,比 Minor GC 引发的时间更长

(5)此时空间仍然不足时,会触发OOM

(5)GC

GC相关参数

JVM详解_第10张图片

(6)垃圾回收器

串行垃圾回收器

(1)单线程

(2)适合堆内存较小,适合个人电脑

(3)-XX:+UseSerialGC = Serial + SerialOld 打开串行垃圾回收器

JVM详解_第11张图片

吞吐量优先垃圾回收器

(1)多线程

(2)适合堆内存较大,必须多核CPU

(3)垃圾回收时,让单位时间内 stop the world 的时间最短

JVM详解_第12张图片

响应时间优先垃圾回收器

(1)多线程

(2)适合堆内存较大,必须多核CPU

(3)垃圾回收时,尽可能的让 stop the world 的单次时间最短

(4)内存碎片可能过多,导致并发失败,

JVM详解_第13张图片

(7)G1

(1)注重吞吐量和低延迟

(2)适合超大堆内存,会将堆划分为多个大小相等的Region

(3)整体上是标记整理算法,两个区域之间用的是复制算法

新生代回收

回收伊甸园区的垃圾对象

新生代回收+并发标记

(1)在 Young GC 时会进行 GC Root 的初始标记

(2)老年代占用堆内存空间比例达到阈值时,进行并发标记

混合回收

(1)最终标记,将可能没有被标记的垃圾对象标记

(2)对伊甸园区、幸存区、old区进行回收

(3)伊甸园区的存活的对象会被复制到幸存区,幸存区达到条件的存活对象会晋升到老年代,其余
	也复制到另一个幸存区。垃圾对象将被回收

(4)老年代的垃圾对象会根据设置的暂停时间被有选择的回收,如果某个区域垃圾对象被回收的时间会
	超过暂停时间,那么就不会回收;如果不会超过暂停时间的,则会回收

Full GC

(1)当垃圾回收的速度跟不上垃圾产生的速度时,并发收集失败,开始 Full GC

新生代回收的跨代引用(老年代引用新生代)问题

将引用新生代对象的老年代对象标记(将卡表中的卡标记为脏卡)

Remark

(1)根据对象的黑白状态,来确定是否被回收

(2)可能存在被强引用的对象被标记为白色,而被垃圾回收

(3)采用写屏障技术,在某个对象的引用改变前,会将该对象加入队列,在remark阶段会重新扫描队列

字符串去重

字符串在底层是由char[]数组表示

(1)将所有新分配的字符串放入一个队列

(2)在新生代回收时,G1并发检查是否有字符串重复

(3)如果它们值一样,让他们引用同一个 char[]

(4)优点:节省大量内存  缺点:略微多占用了CPU时间,新生代回收时间略微增加

并发标记时的类卸载

(1)所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,
	则卸载它们所加载的所有类

回收巨型对象

(1)一个对象大于region的一半时,称之为巨型对象

(2)G1不会对巨型对象进行拷贝

(3)回收时被优先考虑

(4)G1会追踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代
	垃圾回收时被处理掉

并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为 Full GC

(8)GC调优

6.类加载

(1)类文件结构

魔数

0~3字节,表示它是否是class类型的文件

版本

4~7字节,表示类的版本

常量池

记录java类中的各种信息

8~9字节,表示常量池长度,00 23 23的十进制就是35 表示常量池有#1~#34项,#0不计入,也没有值

第#1项0a表示一个method信息,00 06 和 00 15 表示它引用了常量池中#6和#21项来获得这个方法的
所属类和方法名

(2)字节码指令

将class文件中常量池信息载入运行时常量池

JVM详解_第14张图片

方法字节码载入方法区

JVM详解_第15张图片

main线程开始运行 分配栈帧内存

JVM详解_第16张图片

执行引擎开始执行字节码

(3)javap

字节分析类文件结构太麻烦了,可以使用javap工具来反编译class文件

javap -v xxx.class

-v:显示详细信息

(4)类加载

加载

JVM详解_第17张图片

链接

验证

验证类是否符合JVM规范,安全性检查

准备

(1)为静态变量分配空间,设置默认值,需要在初始化阶段完成赋值

(2)静态变量存储在堆中

(3)如果静态变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

(4)如果静态变量是final的,但数于引用类型,那么赋值也会在初始化阶段完成

解析

将常量池中的符号引用解析为直接引用

初始化

(1)即调用 ()

会导致初始化的情况:
(2)main方法所在的类,总会被首先初始化

(3)首次访问这个类的静态变量或静态方法时会导致初始化

(4)子类初始化时,如果父类还没初始化,那么会引发父类初始化

(5)子类访问父类的静态变量,只会触发父类的初始化

(6)调用Class.forName会触发初始化

(7)new 会导致初始化

不会导致类初始化的情况:
(1)访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化

(2)类对象.class不会触发初始化

(3)创建该类的数组不会触发初始化

(4)类加载器的 loadClass 方法,不会导致初始化

(5)Class.forName 的参数2为false时不会导致初始化

(5)类加载器

启动类加载器

(1)加载 JAVA_HOME/jre/lib

扩展类加载器

(1)加载 JAVA_HOME/jre/lib/ext

应用程序类加载器

(1)加载 classpath

自定义类加载器

(1)加载 自定义

什么时候需要自定义类加载器:
(2)想加载非 classpath 随意路径中的类文件
(3)都是通过接口来使用实现,希望解耦时,常用在框架设计
(4)这些类希望予以隔离,不同应用的同类名都可以加载,不冲突,常见于tomcat

步骤:
(1)继承 ClassLoader 父类
(2)要遵从双亲委派机制,重写 findClass 方法
(3)读取类文件的字节码
(4)调用父类的 defineClass 方法来加载类
(5)使用者调用该类加载器的 loadClass 方法

双亲委派

(1)沙箱安全机制,防止核心API被篡改

(2)代码复用,如果一个类已经被加载,那么不会被重复加载

(3)稳定性,可以确保java类库的稳定性

你可能感兴趣的:(jvm,java,开发语言)