披荆斩棘,呕心沥血,一举拿下JVM


前言

谈谈你对JVM理解?Java8虚拟机和之前的变化更新
什么是OOM,什么是栈溢出?怎么分析
JVM的常用调优参数有哪些?
内存快照如何抓取,怎么分析Dump文件?知道吗?
谈谈JVM中,类加载器你的认识?rt-jar等


一、JVM探究

JVM的位置- JVM的位置
在这里插入图片描述

JVM的体系结构
在这里插入图片描述
注:99%的调优都存在于Heap中,Stack存在出栈弹栈 first-in last-out (FILO),故此Stcak中不会出现GC
在这里插入图片描述
· Stack内存结构
在这里插入图片描述

二、类加载器

1.类加载器的作用

作用:加载Class文件

· 顺便回顾一波面向对象在这里插入图片描述
在这里插入图片描述

主要有以下四种类加载器

(1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库,用来加载java核心类库。无法被java程序直接引用。

(2)扩展类加载器(extensions class loader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。Java虚拟机的实现会提供一个扩展库目录。

(3)应用程序类加载器(system class loader):这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。一般来说,Java应用的类都是由它来完成加载的。

(4)自定义加载器:用户自己定义的类加载器,通过继承 java.lang.ClassLoader类的方式实现。

来个简单示例代码,如下:在这里插入图片描述

第一次输出,获取类加载器可得知当前使用的是应用程序类加载器。
第二次输出,获取父类的类加载器时得知为扩展类加载器。
第三次输出,获取父类的父类的类加载器,此时输出结果null。此时有两种情况,1表面此时父类的父类没有结果。2其实是有结果,但java代码获取不到父类的父类的类加载器。

2.双亲委派机制

例如,我们编写了一个java.long.String类。如下图:
大家不妨先猜一猜运行结果~

在这里插入图片描述

此时运行结果:

在这里插入图片描述

报错信息非常离谱,明明我的方法中存在main方法,为什么程序会说它找不到呢?
哈哈此时就是双亲委派机制在搞事情了

双亲委派机制:安全。
注:它在运行一个程序之前,会层层向上找,最终执行启动类加载器(rt.jar)里的string方法
应用程序类加载器(AppClassLoader)–>扩展类加载器(ExtClassLoader)–>启动类加载器(Bootstrap ClassLoader) 其实最终运行的是Bootstrap ClassLoader。
------------------------------------------------------------------------------分隔符------------------------------------------------------------------------
得出结论:
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动类加载器会检查是否能加载当前类,能加载就结束(使用当前的类加载器),否则,抛出异常,通知子加载器进行加载
4.重复步骤3
5.如果所有类加载器都找不到呢? 那么会抛出java.lang.ClassNotFoundException

看源码:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查是否已经被类加载器加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 存在父加载器,递归的交由父加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 直到最上面的Bootstrap类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 如果仍然没有找到,那么按顺序调用findClass  
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

【什么?还不理解双亲委派机制,话不多说上图看】
在这里插入图片描述

三、沙箱安全机制- 沙箱安全机制

在这里插入图片描述
在这里插入图片描述

Java安全模式额核心就是Java沙箱【sandbox】,如下图:

在这里插入图片描述

四、Native

有没有小伙伴好奇过线程是如何启用的? 看源码,如下:

在这里插入图片描述

这时,我们发现当代码走到 private native void start0();时,就进不去了,这时为什么呢?

    public static void main(String[] args) {
        new Thread(() -> {
        }, "Thread one").start();
        
        // private native void start0();
        //native:凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层语言的库【这里是C++】
        //凡是有native的,则会进入本地方法栈
        //调用本地方法本地接口 JNI 【Java_Native_Interface】,本地方法接口会去本地方法库中查找(找的其实就是ext)
        //JNI作用:扩展Java的作用,融合其他语言为Java所用   最初是想融合:C、C++
        //Java诞生的时候C、C++横行,想立足就必须要有调用C、C++的程序
        //于是Java就专本开辟了一个标记native的标记区域:Native_Method_Stack 也就是本地方法栈,用于登记native方法
        //在最终执行的时候,会通过JNI加载本地方法库中的方法
        
        //一般常用于Java驱动硬件,管理系统  企业级应用不常用
       
        //调用其他接口:  http,rpc,Socket,WebService
    }

在这里插入图片描述

如下图:

在这里插入图片描述

总结:凡是带了Native关键字的方法就会进入本地方法栈,其他的就是java栈

在这里插入图片描述

方法区【此区域属于共享区间】

在这里插入图片描述

根据代码看下图:

public class Car {
  private int price;
  private String name = "别克";
  public static void main(String[] args) {
      Car car1 = new Car();
//        car1.name = "洗脚城ct6";
      System.out.println(car1.name);
  }
}

在这里插入图片描述

五、栈(Stack)

栈:先进后出,后进先出【想象成一个桶】
队列:先进先出【FIFO(Firist Input First Output)想象成管道】

模拟栈溢出,StackOverflowError

public class TestStack {
  public static void main(String[] args) {
      //main()先加载到栈中
      //main调a方法,a也加载到栈
      new TestStack().a();
  }
  public void a(){
      b();//加载b到栈
  }
  public void b(){
      a();//加载a到栈
  }
}

在这里插入图片描述

栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放了。
对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over!
栈中存在的数据:8大基本类型+对象的引用地址+实例的方法。

在这里插入图片描述

六、堆(Heap)

Heap,一个JVM只有一个堆内存,并且堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把类、方法、常量、变量保存我们所引用类型的真实对象。
堆中还分三个区域:
新生区(Young):类,新生或成长,甚至死亡的地方。新生代又被划分为三个区域Eden、From Survivor, To Survivor。所有的对象都在Eden区new出来的!
养老区 (old)
永久区(元空间)

在这里插入图片描述
举个栗子:Eden满了之后会触发 Minor GC,因为大部分对象在短时间内都是可收回掉的,因此Minor GC后只有极少数的对象能存活下来,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,而被移动到S1区(采用的是复制算法)。
当触发下一次Minor GC 时,会将Eden区和S1区的存活对象移动到S2区,同时清空Eden区和S1区。当再次触发Minor GC 时,这时候处理的区域就变成了Eden区和S2区(即S1和S2进行角色交换)。每经过一次Minor GC ,存活对象的年龄就会加1。经过多次Minor GC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FullGC。

堆溢出,如下图:

在这里插入图片描述

经过研究表明:99%的对象都是临时对象,能进入养老区的并不多。

永久区:此区域常驻内存,用来存放JDK自带的class对象。interface元数据,存储的是java运行时的一些环境。
这个区域不存在GC,关闭VM虚拟机就会释放这个区域的内存。
JDK1.6::永久代,常量池在方法区中
JDK1.7::永久代,相比于JDK1.6,慢慢退化了,也叫“去永久代”,常量池在堆中
JDK1.8: 元空间,无永久代,常量池在元空间 (元空间逻辑上存在,物理上不存在)
在这里插入图片描述
新生区、永久区、堆内存调优

        long max = Runtime.getRuntime().maxMemory();//获取运行时想用的最大内存
        long total = Runtime.getRuntime().totalMemory();//获取使用的总内存
        int availableProcessors = Runtime.getRuntime().availableProcessors();//可用处理器的个数
        System.out.println(max + "字节==" + (double) (max / 1024 / 1024) + "M");
        System.out.println(total + "字节==" + (double) (total / 1024 / 1024) + "M");
        System.out.println(availableProcessors);
         // 默认情况下:分配的总内存是电脑内存的1/4, 初始化的内存是电脑的1/64

我们可以手动配置堆内存大小 -Xms1024m -Xmx1024m -XX:+PrintGCDetails

-Xms:设置初始化内存分配大小 默认1/64。表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
-Xmx:配置当前程序能够使用的最大内存大小 默认1/4,不能超过本机内存。在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
-Xss:规定了每个线程虚拟机栈及堆栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。
在这里插入图片描述

堆内存溢出,看下图GC走势

在这里插入图片描述
由此我们得出,在新生代,老年代,都已经满了,并且Full GC此时已经清理不动了,产生了OOM。

提问:OOM怎么解决?思考一下

可以这么做:
1.尝试扩大堆内存看结果
2.分析内存,用专业工具看一下那个地方出现了问题
可使用内存快照工具:网上一堆,自行百度
作用:
分析dump内存文件,快速定位内存泄漏问题
获取堆中的数据
定位大的对象
-Xms20m -Xmx30m -XX:+HeapDumpOnOutOfMemoryError
在这里插入图片描述
在这里插入图片描述

七、GC:垃圾回收

在这里插入图片描述

JVM在进行GC时,并不是对这三个区域进行统一回收,大部分时候都是在新生区 ;
新生区
幸存区:【from和to】
老年区
GC两种类:Minor GC(普通垃圾回收)、Full GC(全局垃圾回收)

JVM的内存模型和分区【详细到每个区放什么】
堆里面的分区有哪些?新生区【Eden,from,to】、老年区,说说他们的特点
GC算法有哪些?标记清除法、标记整理/标记压缩、复制算法、引用计数器,怎么用?
Minor GC和Full GC分别在什么时候发生?

1.引用计数法
在这里插入图片描述
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次,则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

目前虚拟机基本都是采用可达性算法,从GC Roots 作为起点开始搜索,那么整个连通图中的对象边都是活对象,对于GC Roots 无法到达的对象变成了垃圾回收对象,随时可被GC回收。

2.GC复制算法
在这里插入图片描述
在这里插入图片描述

可以简单的说GC复制算法就是作用于新生区的 from和to区
好处:没有内存碎片
坏处:
1、也就是浪费了一个幸存者区也就是to区的空间,复制算法上面也说了就是to区永远都是空的
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视,所以从以上描述不难看出。复制算法要想使用,最起码对象的存活率要非常低才行,而且 最重要的是,我们必须要克服50%的内存浪费。
最佳使用场景:新生区,毕竟对象存活率较低

3.标记清除(Mark-Sweep)
在这里插入图片描述

标记清除算法:
好处:不需要额外的空间
坏处:这个算法需要暂停整个应用,会产生内存碎片。两次扫描,严重浪费时间。消耗时间多,标记一次、清除一次,有内存碎片。

4.标记清除压缩

标记整理说明:老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

在这里插入图片描述

标记压缩算法:清除了内存碎片,但是又加长了时间,应该说是用时间换空间

小总结
内存效率:复制算法>标记清除法>标记压缩法
内存整齐度:复制算法=标记压缩法>标记清除法
内存利用率:标记压缩法=标记清除法>复制算法
所以说算法没有最优的,只有最合适的_GC:又叫分代收集算法

年轻代:存活率低,复制算法很合适
老年代:区域大,标记清除(内存碎片不是很多)+标记压缩混合使用


感谢阅读,希望此篇文章对您有所帮助~
加油,打工人!!!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值