JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
image.pngimage.png
image.png image.png
Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系
image.png线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内
线程共享区域随虚拟机的启动/关闭而创建/销毁。
程序计数器(线程私有)
是当前线程所执行的字节码的行号指示器,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。
虚拟机栈(线程私有)
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法区(线程私有)
本地方法区和Java Stack作用类似, 区别是虚拟机栈为执行Java方法服务, 而本地方法栈则为 Native方法服务
堆(Heap-线程共享)-运行时数据区
创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。
方法区/永久代(线程共享)
用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据。
垃圾回收
image.png
。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
13/04/2018 Page 27 of 283
可达性分析
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收。
分代收集算法
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃 圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代与复制算法
目前大部分JVM的GC 对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代 划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用 Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块Survivor空间中。
老年代与标记复制算法
而老年代因为每次只回收少量对象,因而采用Mark-Compact算法。
- JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类, 常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
- 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。
- 当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中。
java中的四种引用类型
1,强引用
把一个对象赋给一个引用变量,这个引用变量就是一个强引用,,它处于可达状态,它是不可能被垃圾回收机制回收的,即 使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之 一。
2,软引用
软引用需要用SoftReference类来实现,内存足够时不会被回收,内存足够时可以被回收
3,弱引用
使用WeakReference类来实现,比软引用的生存期更短,只要垃圾回收一运行,总会回收
4,虚引用
用PhantomReference类实现,不能单独使用,必须和引用队列联合使用,虚 引用的主要作用是跟踪对象被垃圾回收的状态。
分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次GC 所产生的停顿
GC垃圾收集器
1,Serial垃圾收集器(单线程复制算法)
Serial(英文连续)是最基本垃圾收集器,使用复制算法,依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。它不但只会使用一个 CPU 或一条线程去完成垃圾收集工 作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 简单高效,没有线程切换。
2, ParNew 垃圾收集器(Serial+多线程)
ParNew垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。 ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。【Parallel:平行的】 ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java 虚拟机运行在Server模式下新生代的默认垃圾收集器。
3, Parallel Scavenge 收集器(多线程复制算法、高效)
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个 重要区别。
4, Serial Old 收集器(单线程标记整理算法 )
运行在Client默认的java虚拟机默认的年老代垃圾收集器。
5, Parallel Old 收集器(多线程标记整理算法)
6, CMS 收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
初始标记 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
整个过程分为以下4个阶段:
初始标记
只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。
并发清除
清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并 发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS收集器的内存回收和用户线程是一起并发地执行。
7,G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,
相比与CMS 收集器,G1 收 集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾 最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收 集效率。
停顿时间科普
在我们的生产环境中,我们不断发现一些运行在JVM上的应用程序,偶尔会因为记录JVM的GC日志,而被后台的IO操作(例如OS的页缓存回写)阻塞,出现长时间的STW(Stop-The-World)停顿。在这些STW停顿的过程中,JVM会暂停所有的应用程序线程,此时应用程序会停止对用户请求的响应,这对于要求低延迟的系统来说,因此所导致的高延迟是不可接受的。
当JVM管理的Java堆空间进行垃圾回收,JVM可能会停顿,并对应用程序造成STW停顿。根据在启动Java实例时指定的JVM选项,GC日志文件会记录不同类型的GC和JVM行为。
在实践活动中,我们通过最优吞吐量和最短停顿时间来评价jvm系统的性能
吞吐量越高算法越好
暂停时间越短算法越好
首先让我们来明确垃圾收集(GC)中的两个术语:吞吐量(throughput)和暂停时间(pause times)。 JVM在专门的线程(GC threads)中执行GC。 只要GC线程是活动的,它们将与应用程序线程(application threads)争用当前可用CPU的时钟周期。 简单点来说,吞吐量是指应用程序线程用时占程序总用时的比例。 例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒。
术语”暂停时间”是指一个时间段内应用程序线程让与GC线程执行而完全暂停。 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。 如果说一个正在运行的应用程序有100毫秒的“平均暂停时间”,那么就是说该应用程序所有的暂停时间平均长度为100毫秒。 同样,100毫秒的“最大暂停时间”是指该应用程序所有的暂停时间最大不超过100毫秒。
阻塞IO模型
用户线程在IO操作读取数据的时候,阻塞自身以让出cpu,数据就绪,内核拷贝数据到用户线程并返回结果给用户线程时,停止阻塞
非阻塞IO模型
用户线程通过不停的轮询数据读取结果从而完成读取数据,数据没有就绪就返回error,继续read询问,不需要让出cpu等待,开销大。
多路复用IO模型
利用通道selector.selecct()去轮询每个通道是否达到事件,没有事件则一直阻塞。只有真正与读写事件时,才进行IO读写操作。通过一个线程去轮询socket状态实现对多个socket的管理。
非阻塞IO 中,不断地询问socket状态 时通过用户线程去进行的,而在多路复用IO 中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
响应体如果大的话,会导致迟迟不检查其他状态。
信号驱动IO模型
用户线程给socket注册信号函数,内核数据就绪发信号给用户线程,用户线程在信号函数调用IO读写。
异步IO模型
不会阻塞用户线程也不需要轮询的IO模型,只需要用户线程发起请求,等待内核完成将数据拷贝到线程的信号,期间可以执行其他操作,收到信号后直接使用数据,而信号驱动的IO模型则是被通知数据已备好,便自己调用IO函数进行读写。内核接受asynchronous read,便完成一切。
java IO包2分为字节流和字符流
image.png
NIO(非阻塞多路复用IO模型)
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。而NIO基于 Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件
NIO和传统IO 之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。
非阻塞的核心在于线程在某个通道读取数据时,不是进行阻塞而是继续做其他事情。通道读取数据时,只能得到目前能用的数据。
image.png image.png
jvm类加载机制
image.png加载:生成java.lang.class对象。作为方法区类的数据入口
验证:class文件中的字节流信息是否符合虚拟机的要求
准备:方法区分配变量内存空间,静态整型变量在准备阶段会初始值为0,编译阶段才会赋正式的值。
解析:指虚拟机将常量池中的符号引用替换为直接引用的过程。
初始化:到了初始阶段,才开始真正执行类中定义的Java程序代码。初始化阶段是执行类构造器<client>方法的过程。
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初 始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
3种类加载器:
启动类加载器(Bootstrap ClassLoader)
扩展类加载器(Extension ClassLoader)
应用程序类加载器(Application ClassLoader)
双亲委派
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载 器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器最终得到的都是同样一个Object对象。
OSGi(Open Service Gateway Initiative),是面向Java的动态模型系统,是Java动态化模块化系统的一系列规范。 OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。
不遵守了类加载的双亲委托模型。 OSGi旨在为实现Java程序的模块化编程提供基础条件,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分,这对企 业级程序开发来说是非常具有诱惑力的特性。
网友评论