JVM重点知识整理(JDK、JVM组成、运行时数据区、类加载器与双亲委派机制、沙箱安全机制、垃圾回收机制、Native、OOM的调优、JMM)

一、JDK体系结构

  • JRE:Java Runtime Environment ,即Java运行环境,包括Java虚拟机和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
  • JDK:Java Development Kit ,即Java开发工具包,JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。
  • JVM:Java Virtual Machine,即Java虚拟机,因为有了JVM,所以同一个Java程序在多个不同的操作系统中都可以执行。
    在这里插入图片描述

二、JVM整体架构

例如在我们最初学习Java时都写过一个Helloword.java 源代码程序的,首先是通过Javac命令编译成字节码文件,再通过java命令执行。但实际上通过java命令就将字节码文件放到了JVM,最终放到操作系统交给我们的CPU中运行。

计算机只能识别0和1组成的机器码,而一个Java程序可以再多种操作系统上运行,但是我们也知道,同样的一个java程序在不同的操作系统上的机器码是不同的,但我们写的Helloword程序都一样的,为什么在不同操作系统上就能生成不同的机器码呢?这实际是由JVM实现的,通过java命令执行字节码文件,就是把字节码文件放到JVM生成针对于不同操作系统的机器码。

JVM从软件层面屏蔽不同操作系统在底层硬件与指令上的区别

在这里插入图片描述

三、JVM的组成

通过javac命令编译的字节码文件,会通过类装载子系统将其加载到运行时数据区,最后通过执行引擎执行。

一个完整的Java虚拟机,包括类装载子系统,运行时数据区(包括堆栈等),执行引擎三大部分组成

在这里插入图片描述

四、运行时数据区

1. 栈

我们都知道在Java中栈主要是用来存放局部变量的,严格意义上来讲栈应该叫线程栈,例如当我们在main方法中执行其他的方法时,首先会给main方法分配一个栈内存,再给其他线程分配一块栈内存,每块栈内存都是独立的。线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题

栈帧则是栈内存里的一小块内存区域,例如当main线程执行main方法时,它会在整个JVM栈内存区域里给main方法分配一块栈内存,在这块专属于main线程的栈内存里,又会给main()方法分配一块栈帧用于存储专属于main()方法的局部变量,而如果此时main方法又调用了其他方法,此时还会在这块只属于main线程的栈内存中分配一块栈帧用于存放只属于该方法的局部变量。即栈中存放的是一个个的栈帧,每一个栈帧对应一个被调用的方法。

Java栈与数据结构中的栈是一致的,即先进后出。

栈帧中还主要包括局部变量表、操作数栈、动态链接、方法出口等

  • 局部变量表:就是用于存储局部变量的(注意如果是创建对象,则在局部变量表中保存的仅仅是对象在堆内存中的地址,即只是一个对象的引用)
  • 操作数栈:用于存放临时的操作数(例如下面代码的1,2,10,30等)
  • 方法出口:保存将要去的方法的的位置(例如下面代码在compute方法栈的方法出口保存了将要取到main方法的System.out.println("test");一行)

样例代码:

public class Test {
    public static int initDate = 666;

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b)*10;
        return c;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.compute();
        System.out.println("test");
    }
}

2. 程序计数器

程序计数器也被称为PC寄存器。用于标识当线程当前执行的代码,如果线程执行的是非native方法,则PC中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则PC中的值是undefined

3. 方法区(元空间)

主要存放字节码的相关信息,例如常量、静态变量、类元信息(即类的组成信息),如果静态变量是类类型,则保存的是该对象在堆内存中的地址引用。方法区被所有线程共享。逻辑上存在,内存上不存在

下图为一个对象的完整组成部分
在这里插入图片描述
由上图可知一个对象不仅有实例数据等,还有对象头,对象头有一个类型指针,它指向了类的元数据地址,即在堆中创建一个对象时,它也同样有一个指针指向方法区所保存的类元信息

4. 本地方法栈

本地方法栈与Java栈的作用于原理非常相似。区别在于Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法Native Method服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构做强制规定,虚拟机可以自由实现它。(Java通过JNI直接调用本地c语言库函数)

5. 堆

Java中的堆是用来存储对象(所有new出来的对象)。堆是被所有线程共享的,在JVM中只有一个堆。这部分空间是Java垃圾收集器管理的主要区域。

堆内存的大小是可以调节的,默认情况下分配的总内存是主机内存的1/4

五、类加载器与双亲委派机制

Bootstrap ClassLoader:启动类加载器(根加载器),它负责加载Java的核心类库,加载如JAVA_HOME/lib目录下的rt.jar(包含System,String等这样的核心类)。这样的核心类库。跟类加载器非常特殊,他不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。

Extension ClassLoadr:扩展类加载器,它负责加载扩展目录JAVA_HOME/jre/lib/ext下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。

App ClassLoader:应用程序类加载器,是加载classpath环境变量所指定的jar包与类路径。一般来说,用户自定义的类就是有App ClassLoader加载的。

类加载器优先级:Bootstrap ClassLoader > Extension ClassLoadr > App ClassLoader

public class Student {
    public static void main(String[] args) {
        Student student1 = new Student();
        Student student2 = new Student();
        Student student3 = new Student();

        System.out.println(student1.getClass().hashCode());
        System.out.println(student2.getClass().hashCode());
        System.out.println(student3.getClass().hashCode());


        Class<? extends Student> aClass = student1.getClass();
        System.out.println(aClass.getClassLoader());
        System.out.println(aClass.getClassLoader().getParent());
        System.out.println(aClass.getClassLoader().getParent().getParent()); //因为在rt包下不存在Student类的根加载器

    }
}

在这里插入图片描述
双亲委派机制:当类加载器收到类加载请求,会将这个请求委托给父类加载器去完成,启动父类加载器检查是否能加载该类,能加载就加载并结束,如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

通俗的讲,就是如果同时存在两个或多个全限定名完全一致的情况下,通

使用双亲委托模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.String,它存在在rt.jar中,无论哪一个类加载器要加载这类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此String类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.String的同名类并放在ClassPath中,那么系统中将会出现多个不同的String类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但永远无法被加载运行。我们看一下下面的案例:

理论上应该打印"自定义的String类",但由于双亲委派机制的存在,这样的代码是会报错的

package java.lang;

public class String {
    public static void main(String[] args) {
        new String().test();
    }

    public static void test() {
        System.out.println("自定义的String类");
    }
}

在这里插入图片描述

六、JVM垃圾回收机制

(一)哪些对象是垃圾

为了确定哪些对象是垃圾,jvm为我们提供了一些算法去判定。常见的判断是否存活有两种方法:引用计数法和可达性分析

1. 引用计数法(Java并未采用)

为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。因此,Java 里没有采用这样的方案来判定对象的“存活性”。

2. 可达性分析

可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。不能到达的则被可回收对象。

在这里插入图片描述
GC Roots 本身是一个出发点,也就是说我们每次进行可达性分析的时候都要从这个初始点出发。换句话说,初始点我们一定是可达的。那么,Java 里有哪些对象可以作为GC Roots呢?主要有以下四种:

  • 虚拟机栈(帧栈中的本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

(二)堆内存模型

在这里插入图片描述
由上图可以看出新生代中存在一个伊甸园区和两个幸存区(基于复制算法,后文讲述),然后是老年代、方法区

对象一开始会进入伊甸园区,当伊甸园区存满之后,就会触发minor GC垃圾回收机制,将没有链接在GC Roots根节点上的对象(无引对象)当做垃圾回收,存活下来的对象就会进入From区,并且此时对象的分代年龄会加1,当伊甸园区第二次存满时,会再次检查哪些对象是无引对象将其回收机(包括From区),然后将伊甸园区和From区中仍然存活的对象都移到To区中,同时分代年龄加1,之后每一次minor GC时,伊甸园区中的对象会进入From区或者To区,同时From区和To区中的对象会来回循环,当分代年龄等于15时,该对象就会进入老年代(web应用中的线程池或者连接池一般都会进入老年代,再如Spring容器中的bean,由于是以单例模式的存在,因此一般都会进入老年代)。

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC),此时会停止其他的用户线程,将服务器资源专注用于垃圾回收,这个过程称为STW过程,它是非常影响性能的一个过程,也正是在这个过程中会产生大量的卡顿。

(三)垃圾回收算法

  • 引用计数法

    引用计数法是一种古老的垃圾手机方法。引用计数器实现很简单:对于一个对象A,有任何一个对象引用了A,nameA的计数器+1;引用是小事,A的计数器-1.当A的引用计数器是0时,A对象就不能被使用了。引用计数法很简单,就是额外的为每一个对象设置一个计数器用来计算引用的数量。

  • 复制算法

    谁空谁是to。当一个对象经历了15次GC还存活,则会进入老年代。好处是没有内存碎片,但缺点是浪费了一部分空间,即多了一份永远为空的To区,在极端情况下(From区满)时性能消耗较大,因此复制算法最佳使用场景是对象存活度较低的时候

  • 标记清除算法

    扫描所有对象,对存活的对象进行标记,没有标记的对象则会清除,但会留下清楚后的空位,省略了复制算法中多存在的一块内存区域

    缺点是会进行两次扫描,会浪费事件,会产生内存碎片

  • 标记压缩算法

    基于标记清除之上,会再进行一次压缩,即再次扫描,向一端移动存活的对象,防止了内存碎片的产生

内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法

内存整齐度:复制算法 = 标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

没有最好的垃圾回收算法,只有最合适的垃圾回收算法,这就需要程序员根据实际情况进行调优,一般来说,年轻代存活率低,适合采用复制算法。老年代区域大且存活率高,适合采用标记清除算法(因为该区内存碎片较少)+ 标记压缩算法混合实现

七、沙箱安全机制

沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。组成沙箱的基本组件是字节码校验器(确保Java类文件遵循Java语言规范)和类装载器(通过双亲委派机制保证安全)

八、Native关键字

Java是一个跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而Java要实现对底层的控制,就需要借助一些其他语言的帮助,这个就是native的作用。

Native method就是一个java调用非java代码的接口,例如Thread.start()方法为本地方法,凡是带了native关键字,说明Java权限已经达不到了,需要调用底层C语言的库。具体体现为native方法会进入本地方法栈,会调用本地方法接口(JNI),然后进入本地方法库

九、关于OOM的调优

Java虚拟机调优主要集中在堆内存的调优上,因为99%的垃圾都源于堆内存,当到达一定阈值或堆内存满后,就会产生OOM(Out of Memory 堆溢出)错误

  1. 尝试扩大堆内存
  2. 分析内存看那个地方出现了问题,使用JProfiler软件进行排查

调整堆内存大小参数的相关命令

  • -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
  • -Xmn — 堆中年轻代的大小
  • -XX:-DisableExplicitGC — 让System.gc()不产生任何作用
  • -XX:+PrintGCDetails — 打印GC的细节
  • -XX:+PrintGCDateStamps — 打印GC操作的时间戳
  • -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
  • -XX:NewRatio — 可以设置老生代和新生代的比例
  • -XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
  • -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
  • -XX:TargetSurvivorRatio:设置幸存区的目标使用率

案例:如下设置了堆的初始大小 和 堆的最大大小都为8M,并打印GC的细节
在这里插入图片描述
此外,我们还可以使用JProfiler监控工具进行排错调优(傻瓜式安装,使用需在IDEA中安装插件,并在设置工具一栏中将其与本地安装的JProfiler绑定,使用命令为 -XX:+HeapDumpOnError[此处的Error为具体的Error类型])
在这里插入图片描述

十、JMM(Java Memory Model)

内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象,不同架构下的物理机拥有不一样的内存模型,Java虚拟机也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。在C/C++语言中直接使用物理硬件和操作系统内存模型,导致不同平台下并发访问出错。而JMM的出现,能够屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性,使得Java程序能够“一次编写,到处运行”

JMM定义了线程工作内存和主内存之间的抽象关系,即Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面讲的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

JMM是围绕着在并发过程中如何处理可见性,原子性,有序性这三个特性而建立的模型:

  • 可见性: JMM提供了volatile变量定义,final。synchronized块来保证可见性
  • 原子性:JMM提供保证了访问基本数据类型的原子性,但实际业务处理场景往往需要更大的范围的原子性保证,所以模型也提供了synchronized块来保证
  • 有序性:HMM提供了volatile和synchronized来保证线程之间操作的有序性
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值