JVM快速入门篇

一、JVM的位置

JVM位于操作系统之上,可以理解为一个软件。JRE中包含了JVM。
在这里插入图片描述

二、JVM的体系结构

以下是JVM的简要图,其中我们JVM调优绝大多数就是调堆和方法区中的东西。在这里插入图片描述
这是JVM架构的详细版本:在这里插入图片描述

三、类加载器

作用: 加载Class文件
在这里插入图片描述
类是抽象的,是一个模板,对象是具体的。也就是new一个类出来都是不同的对象,在堆中都有具体的地址。

public class Car {

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


        System.out.println(car1.hashCode());//2003749087
        System.out.println(car2.hashCode());//1324119927
        System.out.println(car3.hashCode());//990368553

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

        System.out.println(aClass1.hashCode());//295530567
        System.out.println(aClass2.hashCode());//295530567
        System.out.println(aClass3.hashCode());//295530567
    }
}

类加载器的种类:

  • AppClassLoader (应用程序加载器)
  • ExtClassLoader (拓展类加载器)
  • BootstrapLoader(启动类(根)加载器)
  • 虚拟机自带的加载器
public class Car {

    public int age;

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


        System.out.println(car1.hashCode());//2003749087
        System.out.println(car2.hashCode());//1324119927
        System.out.println(car3.hashCode());//990368553

        Class<? extends Car> aClass1 = car1.getClass();

        ClassLoader classLoader = aClass1.getClassLoader();
        System.out.println(classLoader);//AppClassLoader
        System.out.println(classLoader.getParent());//PlatformClassLoader  ExtClassLoader
        System.out.println(classLoader.getParent().getParent());//null java程序获取不到(因为java底层是用C写的)
    }
}

其中如果JDK是11及以上就会显示PlatformClassLoader,这里使用的是1.8。
在这里插入图片描述
双亲委派机制:主要是为了安全。
查找顺序:AppClassLoader ----> ExtClassLoader ----> BootClassLoader(如果有的话最终执行),如果都没有再返回来查找执行AppClassLoader中的类。

  1. 类加载器收到类加载的请求
  2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类(根)加载器
  3. 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
  4. 重复步骤3

作用:
1.沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改
2.避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.
3.全盘委托机制。 比如Math类,里面有定义了private User user;那么user也会由AppClassLoader来加载。除非手动指定使用其他类加载器加载。也就是说,类里面调用的其他的类都会委托当前的类加载器加载。

四、Native

先了解一下JNI(Java Native Interface),它是java调用本地接口的接口,使得java可以调用操作系统的一些函数,或者一些C语言写的函数(java出现的年代C/C++比较流行,java想存活下去就得能支持C/C++写的一些程序),而且java底层是用C++/C编写的。Native就是用声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。简单地讲,一个native Method就是一个 Java 调用非 Java 代码的接口。

五、方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量(static)、常量(final)、类信息(Class模板)(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

六、PC寄存器

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

七、栈

栈的特点: 先进后出,后进先出,不存在垃圾回收的问题
队列: 先进先出
栈中存放的东西: 8大基本数据类型+对象应用+实例的方法
在这里插入图片描述

八、堆

堆(Heap),一个JVM只有一个堆内存,堆内存的大小是可以调节的。
三种JVM:

  • sun公司:Hotspot
  • BEA:JRockit
  • IBM:J9VM

在这里插入图片描述
堆中放着对象的实例,堆中细分为三个区域:

  • 新生区(伊甸园区)
  • 老年区
  • 永久区
    在这里插入图片描述
    GC垃圾回收主要在伊甸园区和养老区。
    假设内存满了,会出现OOM的报错(就是堆内存满了):java.lang.OutOfMemoryError:Java heap space
    (在JDK8以后,永久存储区就改名为元空间)
    新生区: 类诞生,成长和死亡的地方,所有的对象都是在伊甸园区new出来的。在伊甸园区满了之后会触发一次轻GC,把还在使用的对象放入幸存区。在伊甸园区和幸存区都满了(也就是新生区都满了)就会触发重GC,把还在使用的对象放入养老区。
    永久区:
    这个区域常驻内存。用来存放JDK自身携带的Class对象,Interface元数据, 存储的是Java运行时的一些环境或者类信息,这个区域不存在垃圾回收(关闭VM虚拟就会释放这个区域的内存)
    奔溃条件: 一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;
  • JDK1.6之前:永久代,常量池是在方法区
  • JDK1.7:永久代,但是慢慢地退化了,提出去永久代,常量池在堆中
  • JDK1.8之后:无永久代,常量池在元空间
    在这里插入图片描述

以下程序可以看出在自己电脑上堆区的大小

public class Main {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        //返回JVM的初始化总内存
        long total = Runtime.getRuntime().totalMemory();
        System.out.println("max:"+max+"字节\t"+(max/(double)1024/1024)+"MB");
        System.out.println("total:"+total+"字节\t"+(total/(double)1024/1024)+"MB");

    }
}

在这里插入图片描述
现在通过调整虚拟家内存大小来查看堆内部关系,我们设置初始(默认是最大分配内存的1/64)和最大分配内存大小(默认电脑内存的1/4)都为1024M,打印虚拟机内存情况:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
在这里插入图片描述
在这里插入图片描述
我们发现新生代和老年代加起来的内存刚好等于虚拟机大小,所以也有人说永久代(元空间)在逻辑上存在,物理上不存在(所以又叫做非堆)

如果在一个项目中突然出现OOM故障,如何排查?
1.能够看到第几行代码出错:内存快照分析工具,MAT/Jprofiler
2.Dubug,一行行代码分析代码
MAT/Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象

九、GC

JVM在进行GC时,并不是对新生代,幸存区(from ,to),老年代统一回收,回收的大部分是新生代。
GC的两种类型: 轻GC(普通GC),重GC(全局GC)
GC的算法: 标记清除法,标记整理,复制算法,引用计数器
引用计数器: 给每个对象分配一个计数器,统计使用过多少次,将所有计数器为0的内存对象销毁并回收其占用的内存
在这里插入图片描述
复制算法:
在这里插入图片描述
好处: 没有内存的碎片
坏处: 浪费了内存空间,多了一半空间是空的(to)
复制算法最佳使用场景:对象存货度较低的时候,也就是新生区的

标记清除法:
在这里插入图片描述
优点: 不需要额外的空间
缺点: 两次扫描,严重浪费时间,会产生垃圾碎片
标记(压缩)清除算法:
在这里插入图片描述
总结:
内存算法效率(时间复杂度):复制算法>标记清楚算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法

没有最优算法,只有最合适算法---->GC分代收集算法: 新生代存活率低使用复制算法,老年代存活率高,使用标记清除+标记压缩算法混合实现

十、JMM(Java Memory Model,java内存模型)

以下来自(渣娃-小晴晴)
它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范,是和多线程相关的一组规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM 的实现都要遵守这样的规范,有了JMM规范的保障,并发程序运行在不同的虚拟机上时,得到的程序结果才是安全可靠可信赖的。如果没有JMM 内存模型来规范,就可能会出现,经过不同 JVM 翻译之后,运行的结果不相同也不正确的情况。
计算机在执行程序时,每条指令都是在CPU中执行的。而执行指令的过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程,跟CPU执行指令的速度比起来要慢的多(硬盘 < 内存 <缓存cache < CPU)。因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时,就可以直接从它的高速缓存中读取数据或向其写入数据了。当运算结束之后,再将高速缓存中的数据刷新到主存当中。
JMM 抽象出主存储器(Main Memory)和工作存储器(Working Memory)两种。

  • 主存储器是实例对象所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的。
  • 工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)。
    在这里插入图片描述
    线程无法直接对主内存进行操作,此外,线程A想要和线程B通信,只能通过主存进行。
    JMM的三大特性:原子性、可见性、有序性。
    原子性:一个或多个操作,要么全部执行,要么全部不执行
    可见性:只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。
    有序性:有序性可以总结为:在本线程内观察,所有的操作都是有序的;而在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义:线程内似表现为串行,后半句是指:“指令重排序现象”和“工作内存与主内存同步延迟现象”。处理器为了提高程序的运行效率,提高并行效率,可能会对代码进行优化。编译器认为,重排序后的代码执行效率更优。这样一来,代码的执行顺序就未必是编写代码时候的顺序了,在多线程的情况下就可能会出错。
    在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序,优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(在单线程情况下)。
    ​ 有序性问题 指的是在多线程的环境下,由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致计算结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。
    Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行进入。
    为了支持 JMM,Java 定义了8种原子操作,用来控制主存与工作内存之间的交互:
  • read 读取:作用于主内存,将共享变量从主内存传送到线程的工作内存中。
  • load 载入:作用于工作内存,把 read 读取的值放到工作内存中的副本变量中。
  • store 存储:作用于工作内存,把工作内存中的变量传送到主内存中。
  • write 写入:作用于主内存,把从工作内存中 store 传送过来的值写到主内存的变量中。
  • use 使用:作用于工作内存,把工作内存的值传递给执行引擎,当虚拟机遇到一个需要使用这个变量的指令时,就会执行这个动作。
  • assign 赋值:作用于工作内存,把执行引擎获取到的值赋值给工作内存中的变量,当虚拟机栈遇到给变量赋值的指令时,就执行此操作。
  • lock锁定: 作用于主内存,把变量标记为线程独占状态。
    ·unlock解锁: 作用于主内存,它将释放独占状态。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值