JVM

 

 核心组件是Java程序运行的核心组件;桌面组件是提供对桌面开发的支持组件;java虚拟机(跨平台的基础);数据库组件,负责数据库连接;安全组件,负责java对外连接的安全;根据组件。

参考文章: https://www.oracle.com/technetwork/java/javase/tech/index-jsp-140763.html

     上图讲述了java为什么是跨平台的,因为在JRE运行时环境,JVM对底层操作系统的差异做了屏蔽,对外只接受统一的class文件。

  从图中可以看出JDK编译环境下.java文件被编译为.class文件,这个JDK在任何操作系统中都是一样的,这也是Java区别于C语言的跨平台的原因,C语言跨平台是在JDK编译时环境下手脚,因此编写不同的操作系统软件需要使用不同的编译器,而java语言不需要这样,java语言编写程序只需要安装JDK,编写软件,然后在任何操作系统下都可以正确运行,根本原因在于JVM对上层屏蔽了操作系统。

  JVM是什么?下面解释JVM:

  其中类加载子系统就是封装过的资源加载系统。

  JVM运行时数据区就是内存,因为.class文件被加载是需要载体去存放的,所以JVM运行时数据区就是内存,而加载的来源可以是多种多样:硬盘,内存,网络等,但是被加载进JVM就是存放到了内存。五大数据区域就不讲了。

  那执行引擎是什么?上面提到了jvm运行时数据区指的就是内存,那么内存的执行者一定是CPU,执行引擎就是负责这块的内容。

以上三个系统就构成了JVM的全部功能。

  这三个模块那个最重要?答案是内存,因为我们的程序就运行在计算机内存中,而内存是计算机最重要的资源,它是计算机工作的地方,也就是JVM的运行时数据区。

计算机给我们的程序提供的计算机资源就是CPU+内存+硬盘,而我们的软件其实就是程序,我们知道程序其实在操作系统中可以笼统的称为进程,而进程又被分为很多子线程,那什么是线程?

当我们启动一个main方法的时候,打开debug,点击快照可以看到下面的内容:

 这里显示正在执行的线程是main线程即我们的主线称。

  在上图中的绿色部分是线程共享区域,而黄色部分是线程私有部分,而我们常说的线程安全问题就发生在绿色部分,绿色部分主要存储了一些对象,静态方法,类信息等线程执行会需要用到的信息,而问题就出现在多个线程如果没有调度就会出现资源污染的问题,也就是线程安全问题,这时候需要使用同步语法来进行线程调度。

  我们应该知道我们的程序具体执行是在虚拟机栈中执行的,因为程序=数据结构+算法,算法在方法中已经声明了,所以我们需要一个数据结构来装在这些算法,这个数据结构就是栈,栈是一种先进后出的数据结构,所以一个main方法总是被先压入栈中,而它包含的其他方法也会被陆续压栈,执行,出栈,直到main方法在我们的main线程中出栈。这就是一个简单的程序在JVM中的执行流程。

  而方法在栈中也有自己的数据结构,就是栈帧,栈帧中保存的就是方法中定义的局部变量以及计算步骤,细分会划分为四点:局部变量表,操作数栈,动态链接,方法出口。每个栈帧都包含这四点,这四点也是栈帧的数据结构。

  .class文件作为这些方法的载体可以被虚拟机识别,但是开发人员只识别.java文件是无法识别.class文件的,java提供了javap命令,这个指令可以对.class文件进行反汇编操作。输入javap就会看到javap得到参数,然后javap -c app.class就可以得到这个class文件的汇编指令,也可以将这些汇编指令保存到一个文本文件中。如下图:

 上面的内容是JVM可以识别的字节码文件中实际的内容。这些指令都是JVM指令,每个指令都代表不同的含义。

上面显示了执行加法运算的栈帧中的运算规则,其中Code是做什么的?

  Code就是我们程序计数器中的内容,程序计数器中显示0,则JVM就会去执行0行这条指令。

 堆中存储了我们需要用到的对象,包括我们实例化和系统实例化的对象,当对象压栈就是虚拟机中的变量指向了堆中的对象,所以我们平常所说的对象就是指向堆中对象的变量。

那么本地方法栈又是什么?首先需要清楚本地方法是什么?本地方法是方法声明中标注有native的方法,这种方法的实现体是我们的JNI,c,c++的实现。

 所以当我们调用Native方法时,虚拟机会单独分配一块本地方法栈来去处理这种本地方法,本地方法栈和虚拟机栈是一样的虚拟结构,只是他们特殊处理。

方法区(元空间):任何类都有自己的类信息,包括属性和方法等,这些信息就存储在元空间。即App.class的类信息,这是个模板。

以上就是JVM的全部信息,下面着重讲下堆,因为堆是Java垃圾回收GC的重点处理对象,所以会牵扯到性能调优等重点知识。

 堆的划分分为新生代和老年代,如下图:

Java为什么需要垃圾回收机制?

  因为Java没有在编程的时候释放无用的内存,而内存是有限的,想要做更多的事就要释放掉无用的内存占用。

 Java是怎么样进行内存回收的呢?

  如上图,通过划分新生代,老年代,以及新生代里面的Eden,from,to的分代回收机制来回收的。 Eden主要存放新创建的对象,from,to其实就是两个交换区,老年代是那些已经熬过了很多次垃圾收集的大对象,这里的大即指生命也指体量。

那为什么新创建的对象都要放在Eden区呢?

  Java的作者是个美国人,美国人普遍都信仰耶稣,圣经中描述伊甸园是亚当和夏娃的被创造的地方,意喻生命的起始,所以Java也是这样,新创建的对象被放到了Eden区域,而当Eden区域将要满时,就会触发一个垃圾回收动作,这个垃圾回收动作叫minorGC,这个minorGC也叫youngGC,这个GC的依据是GC ROOTS来判断是否该对象可以被回收,如果是游离对象就释放内存,即垃圾回收掉,如果不是游离对象就进入到from区,然后让这个对象的对象头的age+1,每个对象被创建的时候age=0,对象创建后包含哪些内容呢?

 概括起来分为对象头、对象体和对齐字节。

对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

下面着重介绍下Mark Word(标记字):

 上图是对象处于5种不同的状态时,Mark Work中的64个位的表现形式,每一行代表一个状态:

2位的lock状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:

 biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。这个地方表明biased_lock只对偏向锁有用,当竞争恶化到轻量级锁以上,该标记就不存在了,即此时该标记位不再提供该属性。

age:4位的age代表对象的年龄,在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:

        1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

        2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。

        3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

        4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

 原文链接:https://blog.csdn.net/scdn_cp/article/details/86491792

  上面讲到了执行一次minorGC,如果在Eden内的对象没有被回收掉,此时会进入到from区,对象头age+1;但是Eden会继续创建对象,当Eden区域再次满时,就会触发第二次minorGC,注意此时from区域的对象变化:

首先from区域内的所有对象的对象头age+1,同时将from区域内的对象全部拷贝到to区域,然后将to区域更名为from,from区域更名为to。

  当Eden区域继续创建新对象,继续触发第n次minorGC,注意此时from区域的变化:

首先from区域内的所有对象对象头age+1,同时将from区域内的对象全部拷贝到to区域,然后将to区域更名为from,from区域更名为to。此时会出现两种情况处理:一种是JVM里面有一个默认配置即年龄多大可以晋升到老年代,默认为15,同时也是最大值,此时会触发一个担保机制,所谓担保机制就是JVM认为此对象是一个老不死的对象,会将这个对象提前进入到老年代区域,此时该对象的age>=15。

  另外一种情况是当老年代区域的内存也快要被占满的时候,就会触发fullGC,此时会触发stw,stop the word停顿,此时整个应用程序就会停顿下来,即整个应用程序无法对外提供任何服务,只有当fullGC完成后,才能继续提供服务。

那么为什么java要采用这种分代垃圾回收机制呢?为了减少fullGC的停顿,因为只有存活15代的对象才会进入老年代,这意味着老年代的对象可回收效率极低,也就意味者更少的fullGC和更少的stw。

想要更直观的了解到JVM内存是怎么样运行的,可以使用JVISUALVM指令打开java自带的工具,它可以监视程序运行时的内存状态,查看某一时刻的内存快照,GC运行状态等等功能,十分神奇。

视频连接:https://ke.qq.com/course/404175?taid=3419627191675599

stop the world

JVM有个叫做“安全点”和“安全区域”的东西,在发生GC时,所有的线程都会执行到“安全点”停下来。

在需要GC的时候,JVM会设置一个标志,当线程执行到安全点的时候会轮询检测这个标志,如果发现需要GC,则线程会自己挂起,直到GC结束才恢复运行。

还有另一种策略是在GC发生时,直接把所有线程都挂起,然后检测所有线程是否都在安全点,如果不在安全点则恢复线程的执行,等执行到安全点再挂起。

但是对于一些没有获得或无法获得CPU时间的线程,就没办法等到它执行到安全点了,所以这个时候只要这个线程是在安全区域的,也可以进行GC,安全区域是一段代码段,在这段代码段中对象的引用关系不会发生变化,所以这个时候进行GC也是安全的。

原文链接:https://www.jianshu.com/p/b1bb31484b8b
posted @ 2020-04-02 20:46  永不熄灭的火  阅读(234)  评论(0编辑  收藏  举报