终于有人讲清楚了JVM

JVM

(1)基本概念
在这里插入图片描述
在这里插入图片描述
(2)运行过程

​ ① Java 源文件—->编译器—->字节码文件
​ ② 字节码文件—->JVM(的解释器)—->机器码

​ 每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

一、线程

​ 这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当线程本地存储、缓冲区分配、同步对象栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。当原生线程初始化完毕,就会调用Java 线程的 run() 方法。Java 线程结束,原生线程随之被回收。操作系统会释放原生线程和 Java 线程的所有资源。

二、JVM内存区域

​ JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 HotspotJVM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。

线程共享区域随虚拟机的启动/关闭而创建/销毁。

直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存,

1、程序计数器(线程私有)

​ 一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
​ 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
​ 这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

2、 虚拟机栈(线程私有)

​ 是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口和异常分派等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。(栈帧随着方法调用而创建,随着方法结束而销毁——抛出了在方法内未被捕获的异常也算作方法结束。)

3、 本地方法区(线程私有)

​ 本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务

4、堆(Heap-线程共享)-运行时数据区

​ 是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

5、方法区/永久代(线程共享)

​ 即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 永久带的内存回收的主要目标是针对常量池的回收和类型的卸载

三、Java运行时内存

在这里插入图片描述

1、新生代

​ 是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

1.1. Eden 区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

1.2. ServivorFrom

​ 上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

1.3. ServivorTo

​ 保留了一次 MinorGC 过程中的幸存者。

1.4. MinorGC 的过程(复制->清空->互换)

​ MinorGC 采用复制算法:首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo 不够位置了就放到老年区),然后,清空 Eden 和 ServicorFrom 中的对象,最后ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

2、老年代

​ 主要存放应用程序中**生命周期长的内存对象。**老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

2.1.MajorGC过程

​ MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

3、 永久代

​ 指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

3.1.JAVA8 与元数据

​ 在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 nativememory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

四、垃圾回收机制与算法

1、如何确定垃圾

1.1. 引用计数法

​ 在 Java 中,引用和对象是有关联的。一个对象如果没有任何与之关联的引用,则说明对象不太可能再被用到,那么这个对象就是可回收对象。实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)

  1. MyObject object1 = new MyObject();
    MyObject object2 = new MyObject();
    object1.object = object2;
    object2.object = object1;
    object1 = null;
    object2 = null;
    

    最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。

1.2.可达性分析

​ 为了解决引用计数法的循环引用问题,Java (还有C#)使用了可达性分析的方法。通过一系列的“GC roots”,对象作为起点搜索。**如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。**要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

2、标记清除算法(Mark-Sweep)

​ 最基础的垃圾回收算法,分为**两个阶段,标注和清除。**标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图在这里插入图片描述

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

3、复制算法(copying)

​ 为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

在这里插入图片描述

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

4、标记整理算法(Mark-Compact)

​ 结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

在这里插入图片描述

5、分代收集算法 VS 分区收集算法

​ 分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(YoungGeneration)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

​ 分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。

5.1. 新生代与复制算法

​ 目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法
在这里插入图片描述

5.2. 老年代与标记整理算法

​ 而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。

五、JAVA 四中引用类型

1、强引用

​ 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用( A a = new A())。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之
一。

2、软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

3、弱引用

​ 弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

4、虚引用

​ 虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

六、GC垃圾收集器

在这里插入图片描述

1、Serial 垃圾收集器(单线程、复制算法)

​ Serial(英文连续)是最基本垃圾收集器,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且**在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。**Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。

2、ParNew 垃圾收集器(Serial+多线程)

​ 实是 Serial 收集器的多线程版本,复制算法,在垃圾收集过程中同样也要暂停所有其他的工作线程。是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。

3、Parallel Scavenge 收集器(多线程复制算法、高效)

​ 新生代垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略(具体细节参数的调节工作由虚拟机完成)也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

4、Serial Old 收集器(单线程标记整理算法 )

​ 主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。

​ 在 Server 模式下,主要有两个用途:

  • 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
  • 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

5、Parallel Old 收集器(多线程标记整理算法)

​ Parallel Old 收集器是Parallel Scavenge的年老代版本,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

6、CMS 收集器(多线程标记清除算法)

​ Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。分为以下 4 个阶段:

6.1.初始标记

​ 只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

6.2.并发标记

​ 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程

6.3.重新标记

​ 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

6.4.并发清除

​ 清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

7、G1 收集器

​ Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收
集器两个最突出的改进是:

  • 基于标记-整理算法,不产生内存碎片。
  • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

​ G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域
的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾
最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收
集效率。

七、Java IO

io包参考Java文档

1、阻塞IO模型

​ 当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。

2、非阻塞IO模型

​ 当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。**用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。**在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取据。

while(true){
    data = socket.read();
    if(data!= error){
        处理数据
        break;
    }
}

3、多路复用 IO 模型

是目前使用得比较多的模型。会有一个线程不断去轮询多个 socket 的状态,并且对到达的事件逐一进行响应。只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。因此,多路复用 IO 比较适合连接数比较多的情况。

另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。但是一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

4、信号驱动 IO 模型

​ 当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。

5、异步 IO 模型

当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。注意,异步 IO 是需要操作系统的底层支持

八、Java NIO

​ NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择区)。**传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道) 和 Buffer(缓冲区)进行操作,**数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

1、Channel

​ Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。例如:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。

2、Buffer

​ 缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

在这里插入图片描述

​ Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。因此,它不能前后移动流中的数据。

​ Java NIO面向缓冲区意味着数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据

3、Selector

​ Selector 类是 NIO 的核心类,**Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。**这样一来,只是用一个单线程就可
以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用
函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护
多个线程,并且避免了多线程之间的上下文切换导致的开销。

4、NIO 的非阻塞

​ IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,该线程在此期间不能再干任何事情了。NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

九、JVM 类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,

在这里插入图片描述

1、加载

这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

2、验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3、准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,比如一个类变量定义为:

public static int v = 8080;

实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是
程序被编译后,存放于类构造器方法之中。但是注意如果声明为:

public static final int v = 8080;

在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v
赋值为 8080。

4、解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中
的:

  1. CONSTANT_Class_info
  2. CONSTANT_Field_info
  3. CONSTANT_Method_info
    等类型的常量。

5、初始化

​ 初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。**到了初始阶段,才开始真正执行类中定义的 Java 程序代码。**初始化阶段是执行类构造器方法的过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值