JVM探究

JVM探究

  • 请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?

  • 什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析?

  • JVM的常用调优参数有哪些?

  • 内存快照如何抓取,怎么分析Dump文件?知道吗?

  • 谈谈JVM中,类加载器你的认识?

1. JVM位置

在这里插入图片描述

2. JVM的体系结构

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

3. 类加载

3.1 类加载过程

类加载的过程分为三个阶段:

1.加载

  • 将类的字节码载入方法区,并创建类.class对象(存在堆中);
  • 如果此类的父类没有加载,先加载父类或先加载接口;
  • 加载是懒惰执行。(用到这个类时才会把类加载到方法区中)

2.链接

  • 验证 -验证类是否符合Class规范,合法性、安全性检查;
  • 准备 -为static变量分配空间,设置默认值;
  • 解析 -将常量池的符号引用解析为直接引用。

3.初始化

  • 执行静态代码块与非final静态变量的赋值;
  • 初始化是懒惰执行。

3.2 类加载器

作用:加载class文件
在这里插入图片描述

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器(用c写的)

  3. 拓展类加载器

  4. 应用程序加载器

public class Car {

    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();

        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();

        ClassLoader classLoader = aClass1.getClassLoader();
        System.out.println(classLoader); //AppClassLoader   应用程序加载器
        System.out.println(classLoader.getParent());//ExtClassLoader  拓展类加载器  在\jre\lib\ext
        System.out.println(classLoader.getParent().getParent()); //null 启动类(根)加载器 在External Libraries下的 rt.jar

    }
}

3.2 双亲委派机制

public class String {

    public String toString(){
        return "1111";
    }

    public static void main(String[] args) {
        String s = new String();
        s.toString();
    }
}

在这里插入图片描述

结论:

  1. 类加载器收到类加载的请求;
  2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类(根)加载器;
  3. 启动加载器检查是否能够加载这个类,能加载就结束(使用当前加载器),否则,通知子加载器进行加载;
  4. 重复步骤3;
  5. 如果都找不到报Class Not Found。

运行一个类之前,会先到启动类(根)加载器中找有没有,有则执行,没有就去拓展类加载器找,再没有才去应用程序加载器。

public class Car {

    public static void main(String[] args) {
        Car car1 = new Car();
        Class<? extends Car> aClass1 = car1.getClass();
        ClassLoader classLoader = aClass1.getClassLoader();
        System.out.println("自己写的Car类的加载器为:"+classLoader);


        String s = new String();
        Class<? extends String> aClass = s.getClass();
        ClassLoader classLoader1 = aClass.getClassLoader();
        System.out.println("String类的加载器为:"+classLoader1);


    }
}

在这里插入图片描述

为null的原因:启动类(根)加载器是用C语言写的,java调用不到。

双亲委派机制存在的意义:

  1. 通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

  2. 通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改

  3. 双亲委派机制是在classLoader里的loadclass方法里实现的,

    /**
    *加载具有指定二进制名称的类。该方法的默认实现按以下顺序搜索类: 
    调用findloaddclass (String)检查类是否已经加载。 
    在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。 
    调用findClass(String)方法来查找类。
    如果使用上述步骤找到该类,并且resolve标志为true,则该方法将在结果class对象上调用resolveClass(class)方法。 
    ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。 除非重写,否则该方法在整个类加载过程中同步getClassLoadingLock方法的结果。 
    参数:
    name -类的二进制名称 
    resolve -如果为真,则解析类 返回: 产生的Class对象 抛出: ClassNotFoundException—如果找不到类
    
    **/
    
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // 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;
            }
        }
    

沙箱安全(了解)

4. Native、本地方法栈

native关键字:

  1. 凡是带了native关键字的方法,说明java的作用范围达不到了,要去调用C语言的库(本地方法库中)。

  2. 会进入本地方法栈,调用本地方法接口,本地方法库

  3. JNI(本地方法接口)的作用: 拓展Java的使用,融合不同编程语言为Java所用

  4. 它在内存区域中专门开辟了一标记区域(本地方法栈),登记native方法,

    在最终执行的时候,加载本地方法库中的方法通过本地方法接口(JNI)

使用其它接口(其它语言写的)的方法:

http,socket,webservice调用其它语言写的接口

5. 程序计数器(PC寄存器)

程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

6. 方法区

static,final,Class文件信息,已被虚拟机加载的信息(即编译器编译后的代码缓存等数据);

Class文件信息:包含的信息有版本、访问标志、常量池、当前类、超级类、接口、字段、方法、属性等。

7. 栈

8大基本类型、对象引用、实例的方法

在这里插入图片描述

值传递:

引用传递

(18条消息) 值传递与引用传递的区别_梦樊哥哥的博客-CSDN博客_值传递和引用传递的区别是什么

8. HotSpot

三种JVM:(了解)

  • Sun公司:HotSpot
  • BEA公司:JRockit
  • IBM公司:J9

9. 堆

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把类,方法,常量,变量放到堆中,还有我们所有引用类型的实例对象,数组

堆内存分为三个区域:

  • 新生区(伊甸园)
  • 养老区
  • 永久区 Perm

OOM(OutOfMemoryError):堆内存不够

在这里插入图片描述

在Jdk1.8后,永久代变为元空间

9.1 新生区

概述:类 诞生 和 成长,甚至死亡的地方

  • 伊甸园:所有对象都是在伊甸园中new出来的
  • 幸存者区(0,1):当伊甸园存满后,会触发轻GC垃圾回收,没有被回收掉的进入幸存区

解释:

假设伊甸园可以存十个,当伊甸园存满后,会触发轻GC垃圾回收,没有被回收掉的进入幸存区,

当伊甸园和幸存区都存满了,尝试进入老年代,老年代放不下会触发重GC清理空间,

清理完后依然放不下会OOM了,但99%的对象都是临时对象。能进老年区的都很少。

9.2 老年代

9.3 永久区(元空间)

这个区域常驻内存,用来存放jdk自身携带的Class对象,Interface元数据,存储的java运行时一些环境,不存在垃圾信息

关闭虚拟机就会释放这个区域的内存

jdk1.6之前: 永久代,常量池在方法区

jdk1.7: 永久代,常量池在堆中,在虚拟机中

jdk1.8: 无永久代,常量池在元空间,元空间在本地内存中

逻辑上存在:

在这里插入图片描述

在这里插入图片描述

元空间的内存释放比较苛刻,先回收实例引用a、b、c,然后等所有类都被回收了,先把类加载器回收了,最后才会把元空间中占用的内存才会释放掉。

结论:类内存释放需要等到类的实例不再引用,它的类加载器被回收。

9.4 堆内存调优

public class demo {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory();
        //返回jvm的初始化总内存
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("max="+maxMemory+"字节\t"+(maxMemory/(double)1024/1024)+"MB");
        System.out.println("total="+totalMemory+"字节\t"+(totalMemory/(double)1024/1024)+"MB");
    }
}

默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存:1/64

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

  • -Xms:设置初始化总内存
  • -Xmx:设置最大内存
  • -XX:打印堆信息
    在这里插入图片描述

其它结论:

新生代大小 + 老年代大小 = 堆内存大小

所以元空间逻辑上存在堆,物理上在本地内存

处理OOM:

  1. 尝试扩大堆内存看结果
  2. 扩大堆内存无效,分析内存(使用专业工具)

10. 使用JPofiler工具分析OOM原因

OOM处理过程:

  1. 扩大堆内存
  2. 内存快照分析工具:JPofiler

JPofiler作用:

  • 分析Dump内存文件,快速定位内存泄露;
  • 获得堆中的数据
  • 获得大的对象
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

11.GC回收

GC: 垃圾回收,只存在堆(包括方法区)中,大部分发生在新生区

  • 伊甸园
  • 幸存区(form、to)
  • 老年区

GC目的:实现无用对象内存自动释放,减少内存碎片、加快分配速度。

GC要点:

  • 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存;
  • 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象;
  • GC具体的实现称为垃圾回收器
  • GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代老年代,不同区域应用不同的回收策略;
  • 根据GC的规模可以分成Minor GCMixedGCFullGC

GC规模分类:

  • 轻GC(普通GC):主要发生在新生区
  • 重GC(全局GC):主要发生在老年区,清理全局

11.1 分代回收与GC规模

11.1.1 分代回收

  • 伊甸园eden,最初对象都分配到这里,与幸存区合称新生代;
  • 幸存区survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成from和to,采用标记复制算法;
  • 老年代old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升);

11.1.2 GC规模

  • MinorGC:发生在新生代的垃圾回收,暂停时间短;
  • MixedGC:新生代+老年代部分区域的垃圾回收,G1收集器特有 ;
  • FullGC:新生代+老年代完整垃圾回收,暂停时间长,应尽力避免。

11.2 GC回收算法

GC算法:

  1. 标记清除法
  2. 标记整理
  3. 标记复制
  4. 引用计数器
  5. 三色标记法

根据如何判定对象是垃圾,垃圾回收算法分为两类:
1、引用计数式垃圾收集(判定垃圾是通过引用计数器)别名:直接垃圾收集

  • 引用计数法

2、追踪式垃圾收集(判定垃圾是通过GC Roots)别名:间接垃圾收集

可达性算法用于追踪,三色标记法用于标记

11.2.1 引用计数法(基本不用)

每个对象都有一个计数器,每引用一次,计数器就加一,没引用就GC掉

在这里插入图片描述

很low,用的少

11.2.2 标记清除(很少用)

“标记清除”算法首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。

标记是通过GC Root根对象的引用链查看该对象是否被引用,从而决定是否被标记,

也可以反过来,标记要存活的对象,统一回收所有未标记的对象。
在这里插入图片描述

缺点:造成内存碎片,虽然清除后释放了很大的内存,但它们都不连续。如果我需要一段连续的内存(数组),将会不够用。

所以很少用这个算法。

11.2.3 标记复制(新生代主要算法)

在这里插入图片描述

注意:to和from区会轮流交换,不是确定的

谁空谁是to

1.每次GC都会把伊甸园中活的对象移到幸存区,一旦伊甸园被GC后就会是空的;

2.假设to区和from区都只有一个对象时,会把其中一个区的对象复制到另一个区,然后变成to区

3.但一个对象经历15次(默认)GC还存活,会进入养老区,可以通过-XX:MaxTenuringThreshold=5设置对象进入老年代经历GC的次数

在这里插入图片描述

好处:没有内存的碎片

坏处: 浪费了内存空间,多了半空间

复制算法最佳使用场景:

对象存活度较低的区域(新生区)

11.2.4 标记整理(老年代主要算法)

把标记后的对象往一端靠拢,防止内存碎片问题的产生。

缺点:效率比较低
在这里插入图片描述

11.2.5 三色标记法

1. 概念

用三种颜色记录对象的标记状态

  • 黑色:已标记,
  • 灰色:标记中
  • 白色:未标记
2. 并发漏标问题

产生原因:有两类线程:垃圾回收线程(守护线程)和用户线程;

以前的垃圾回收器是不可以让两个线程并发运行的,现在某些垃圾回收器可以让两个线程并发运行,并发运行就可能产生漏标问题。

解决方案:记录标记过程变化

1.Incrementtal Update(增量更新)

  • 只要复制发生,被赋值的对象就会被记录。

2.Snapshot At The Beginning,SATB(原始快照)

  • 新加对象就会被记录;
  • 被删除引用关系的对象也被记录。

11.3 垃圾回收器

应用程序可分为两种:

1.注重响应时间

2.注重吞吐量:注重很多计算

JVM基础 -> 什么是STW?_jvm stw_欧皇小德子的博客-CSDN博客

11.3.1 Paraller GC

  • eden内存不足发生Minor GC,标记复制 STW;
  • old内存不足发生Full GC,标记整理 STW;
  • 注重吞吐量。

11.3.2 ConcurrentMarkSweepGC

  • old并发标记,重新标记时需要STW==(防止漏标问题)==,并发清除;----->标记清除算法
  • Failback Full GC;
  • 注重响应时间。

因为用了标记清除法,会有内存碎片,所有会有更好的垃圾回收器代替它。

11.3.3 G1 GC

从jdk9开始已经作为默认垃圾回收器

  1. 响应时间与吞吐量兼顾;
  2. 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous;
  3. 新生代回收:eden内存不足,标记复制STW;
  4. 并发标记:old并发标记,重新标记时需要STW;
  5. 混合收集:并发标记完成,开始混合收集,参与复制的有eden、survivor、old,其中old会根据暂停时间目标,选择部分回收价值高(存活对象少)的区域,复制时STW;
  6. Failback Full GC:当收集的速度小于需要分配新对象的速度,出现并发失败的情况,触发Full GC。

第一阶段:新生代回收
在这里插入图片描述

第二个阶段:并发标记

触发条件:当老年代的内存越来越多,达到堆内存的45%以上,才会触发并发标记;

并发标记:就是在老年代找到那些存活对象,给它们加上标记,这个标记过程是并发执行的,而为了解决漏标问题,采用了原始快照法,重新标记时,需要处理漏标对象,也需要STW。

在这里插入图片描述

第三个阶段:混合收集

会先根据存活时间目标,选择部分回收价值高(存活对象少)的老年代区域以及eden,幸存区做一次垃圾回收。

在这里插入图片描述

完成后释放内存

在这里插入图片描述

混合收集会进行多次,结束又循环回到新生代回收。

12.执行引擎

12.1 Interpreter解释器

1.把java的字节码转换成适用各个平台的机器码;

2.会多次解释同一段代码。

12.2 JIT Compiler 即使编译器

如果某一段代码调用次数非常地频繁,这段代码会被标记成热点代码,热点代码不适合再用解释器进行解释,

所以要用JIT Complier把热点代码解释成机器码(会对代码进行优化),并缓存起来(缓存在代码缓存区),下次再调用这段代码时,会调用缓存起来的代码

(33条消息) JVM系列之:关于即时编译器的其他一些优化手段_hresh的博客-CSDN博客

12.3 GC垃圾回收

GC垃圾回收为执行引擎的一种

13.JVM内存参数

-Xmx10240m:虚拟机最大内存,m为单位(兆);

-Xms10240m:虚拟机最小内存;

-Xmn5120m:新生代内存大小,剩下的内存为老年代;

-Xss:每个线程占用的内存,如果不设置这个参数,与操作系统有关,虚拟机栈的内存与这个参数有关;

-XX:SurvivorRatio=3: eden区:from区=3:1,默认比例为 8:1

-XX:NewRatio=2: 老年代内存:新生代内存=2:1

在这里插入图片描述

除了按比例设置,还可以直接设置大小:

在这里插入图片描述

建议在生产环境把-Xmx和-Xms设置成一样的。

元空间的位置在本地内存,所以它的大小取决于物理内存

在这里插入图片描述

代码缓存区:用于保存即时编译器对热点代码的缓存(机器码)

在这里插入图片描述

14.内存溢出

不会出现内存溢出的区域-程序计数器

出现OutOfMemorvError的情况:

  • 堆内存耗尽-对象越来越多,又一直在使用,不能被垃圾回收
  • 方法区内存耗尽-加载的类越来越多,很多框架都会在运行期间动态产生新的类
  • 虚拟机栈累积-每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时

出现StackOverflowError的区域:

  • 虚拟机栈内部-方法调用次数过多

项目中什么情况下会出现内存溢出,怎么解决?

  1. 误用线程池导致的内存溢出

    例子一:

     public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(2);
         
    
            while (true){
                executorService.submit(()->{
                    try {
                        TimeUnit.SECONDS.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                });
            }
    
        }
    

    使用Executors.newFixedThreadPool(2);创建的线程池的队列无限制,没有拒绝策略,会导致任务过多,造成内存溢出;

    所以推荐使用ThreadPoolExecutor创建自定义创建线程池。

    例子二:

    public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            while (true){
                executorService.submit(()->{
                    try {
                        TimeUnit.SECONDS.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                });
            }
    
        }
    

    用的是有界队列,但它可创建的线程数为Interget.MAX_Value(2147483647),如果任务很多,会创建大量的线程造成内存溢出。

  2. 查询数据量太大导致的内存溢出

    查询出来的记录数据太多导致创建的对象过多导致内存溢出。

  3. 动态生成类导致的内存溢出

    生成的类太多且一直在引用不能被回收

    使用Executors.newFixedThreadPool(2);创建的线程池的队列无限制,没有拒绝策略,会导致任务过多,造成内存溢出;

    所以推荐使用ThreadPoolExecutor创建自定义创建线程池。

    例子二:

    public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            while (true){
                executorService.submit(()->{
                    try {
                        TimeUnit.SECONDS.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                });
            }
    
        }
    

    用的是有界队列,但它可创建的线程数为Interget.MAX_Value(2147483647),如果任务很多,会创建大量的线程造成内存溢出。

  4. 查询数据量太大导致的内存溢出

    查询出来的记录数据太多导致创建的对象过多导致内存溢出。

  5. 动态生成类导致的内存溢出

    生成的类太多且一直在引用不能被回收

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值