JVM基础
jvm:运行在操作系统上的假想的计算机
1:Java文件的编译和解释
2:Java线程
3:JVM组成
4:GC回收算法
5:Java引用
6:GC垃圾收集器
7:Java IO/NIO
8:类加载
执行过程
Java源文件——编译器javac——class文件(字节码文件,二进制文件)
字节码文件——解释器(jvm)java——机器码文件
每一台平台解释器不同,但是虚拟机相同,跨平台的原因。
一个程序对应一个虚拟机,多个程序对应多个虚拟机, 虚拟机之间数据不共享
什么是平台:操作系统及其硬件环境。跨平台即代码的运行不依赖于操作系统及其硬件环境。
c语言不是跨平台的,因为不同操作系统下的编译器编译代码得到不同文件,不能在其他操作系统上运行。即由Windows编译器得到的exe文件不能在Linux上运行
Java跨平台:因为编译后都得到class文件,可以在不同的操作系统上的解释器上执行。
并且注意:
不同的系统下有不同的JVM,所以JVM不是跨平台
JAVA依赖于JVM,JVM给JAVA提供了运行环境,所以JAVA是跨平台的。
程序执行的方式:
第一是编译执行:C它把源程序由特定平台的编译器一次性编译为平台相关的机器码,优点是执行速度快,缺点是无法跨平台;
第二是解释执行,如HTML,JavaScript,它使用特定的解释器,把代码一行行解释为机器码,类似于同声翻译,它的优点是可以跨平台,缺点是执行速度慢,暴露源程序;
第三种是先编译后解释,Java
关于JVM的解释过程:
解释执行:即逐条将字节码翻译成机器码并执行 [在解释执行过程中,每当为 Java 方法分配栈桢时,Java 虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果]
第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。
前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
线程
jvm线程和原生操作系统线程具有直接映射关系
虚拟机线程 (VM thread)
这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the- world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程
这线程中断事件,用来调度周期性操作的执行。
GC 线程
这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程
这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程
这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理
jvm内存区域
执行引擎:编译器,垃圾收集
数据:
共享数据区:方法区,堆
私有数据区:虚拟机栈,本地方法栈,程序计数器
本地库接口 用于连接本地方法库
线程私有数据生命周期和操作系统线程相同
线程共享区域和虚拟机生命周期相同
私有数据
程序计数器:
是当前线程所执行的字节码的行号指示器。
在解释执行中,因为是从头开始的。如果存在多线程并发执行,但线程被挂起时,需要记录正在执行的代码,保存在程序计数器中。
虚拟机栈:
由栈帧组成,每一个方法对应一个栈帧,栈帧随着方法创建而创建,随着方法销毁而销毁。
栈帧由局部变量表、操作数栈、动态链接、方法出口
1、局部变量表:是一组变量值的存储空间,用于存放方法参数通过调用此方法传入的参数和局部变量不是成员变量,而是方法内申请的变量,八种基本类型 对象的引用
2、操作树栈:常称为操作数栈,是一个后入先出栈。方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
3、动态连接:栈帧持有一个指向方法区常量池中所属方法的引用
在Class文件中的常量持中存有大量的符号引用。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
4、方法返回地址:方法的返回分为两种情况
一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者
一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法.
不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置
本地方法栈:Native 方法服务
共享数据:
堆:创建的对象本身和数组都保存在 Java 堆内存中
数组同样是对象 int a[]=new int [3] 所以一切new的对象本身都在堆中
对象又包括成员变量和方法,成员变量在堆中
本地方法区:我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量(final ,“abc”)、静态变量(static)、即时编译器编译后的代码class等数据,方法的入口地址,运行时常量池.
注意常量池和静态变量在jdk1.7后放入堆中
类信息:
文件编译后形成class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是class常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池。
运行时常量池:存在于内存中,是class常量池被加载这里的加载时解释的一部分到内存之后的版本
显式的String常量
String a = “holten”;
String b = “holten”;
第一句代码执行后就在常量池中创建了一个值为holten的String对象;
第二句执行时,因为常量池中存在holten所以就不再创建新的String对象了。
此时该字符串的引用在虚拟机栈里面。
String对象
String a = new String(“holtenObj”);
String b = new String(“holtenObj”);
Class被加载时就在常量池中创建了一个值为holtenObj的String对象,第一句执行时会在堆里创建new String(“holtenObj”)对象;
第二句执行时,因为常量池中存在holtenObj所以就不再创建新的String对象了,直接在堆里创建new String(“holtenObj”)对象。
Java堆的内存回收机制
如何确定垃圾
引用计数法:引用计数为0则说明是可回收对象。可能有循环引用的问题
可达性分析:如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
GC算法
分代收集算法:
Generational Collecting
新生代(1/3):Eden(8/10) 、ServivorFrom(1/10)、ServivorT(1/10)
老年代(2/3):
1:Eden
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
2:ServivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
3:ServivorTo
保留了一次 MinorGC 过程中的幸存者
MinorGC:复制算法
1:eden、servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域,同时年龄+1
如果有对象的年龄以及达到了老年的标准,则赋值到老年代区
如果 ServicorTo 不够位置了就放到老年区
2:清空 eden、servicorFrom
清空 Eden 和 ServicorFrom 中的对象;
3:ServicorTo 和 ServicorFrom 互换
ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom没有进行复制
//算法导致实际只能使用新生代百分之90的空间
老年代:存放稳定的对象。
MinorGC,使得有新生代的对象晋身入老年代,当空间不够用时触发 MajorGC。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC :
采用标记清除算法:
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
永久代:指内存的永久保存区域,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常
元数据区:
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池(常量池)和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制
引用
Java中的四种引用类型:
//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
1:强引用
String str = new String(“abc”);
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError
如果想中断引用关系str=null 此时JVM就可以适时回收内存
2:软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
3:弱引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);
4:虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,用 PhantomReference 类来表示
GC垃圾收集器
Stop-The-World(STW):所有正在工作的线程停止,主要是GC引起的
安全点(Safepoint)即程序执行时并非在所有地方都能停顿下来开始GC,只有到达安全点才会暂停。
抢先式中断(Preemptive Suspension):在GC发生时,首先把所有线程全中断,若发现有线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。(此方式几乎没有虚拟机还在采用)
主动式中断(Voluntary Suspension):当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
新生代垃圾收集器:Serial,ParallelNew,Parallel Scavenge
老年代垃圾收集器:SerialOld,ParallelOld,CMS
新生代:复制算法
Serial:单线程
使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束
没有线程交互的开销,可以获得最高的单线程垃圾收集效率
Client 模式下默认的新生代垃圾收集器
ParallelNew:多线程
开启和 CPU 数目相同的线程数,仍然要暂停其他所有工作的线程
Server 模式下新生代的默认垃圾收集器
Parallel Scaveng:多线程
关注的是程序达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。我的理解为会自适应调整GC时间来让程序获得高吞吐量
年老代:标志整理算法
SerialOld:单线程
ParallelOld:多线程
CMS:最主要目标是获取最短垃圾回收停顿时间
分为几个阶段:
初始标记: 标记GC Roots能直接关联的对象。 会STW
并发标记:从初始标记对象开始标记所有可达对象。不会STW
并发预处理:标记新对象,即新生代晋升对象等对象。不会STW
重标记(remark):从GC Roots开始扫描对象进行可达性分析,标记可回收对象。会STW
并发清理:回收可回收的对象。不会STW
这里解释了为什么不可达对象不等于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收
Java IO/NIO
Java io:data = socket.read();socket本意为套接字,为ip+端口,我理解为一个数据载体
阻塞io:未请求到数据即让出cpu
非阻塞io:在 while 循环中不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高
多路复用io:一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用率
外多路复用 IO 效率高于非阻塞 IO 模型:非阻塞 IO 通过用户线程不断地询问 socket 状态,多路复用 IO 通过内核线程轮询每个 socket 状态,这个效率要比用户线程要高的多。
多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件 迟迟得不到处理,并且会影响新的事件轮询。
信号驱动 IO 模型
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作
异步 IO 模型
在异步 IO 模型中,用户线程发起 read 操作,立刻就可以开始去做其它的事。然后,内核会等待数据准备完成,然后内核将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了
所以两者的区别在于,信号驱动是内核准备数据线程读,而异步io内核准备数据并读取到线程
Javaio包:
Java nio :new io
NIO 主要有:Channel(通道),Buffer(缓冲区), Selector。
传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区 中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开, 数据到达)。因此,单个线程可以监听多个数据通道。
Channel:“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的而 Channel 是双向 的,既可以用来进行读操作,又可以用来进行写操作
Buffer:缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由
buffer
Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。用一个单线程就可以管理多个通道,也就是管理多个连接。
优点:
1:
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。
数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
2:
IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO 的非阻塞模式, 使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
这里我理解为如果服务器和客户端使用socket,必须同时读和写,所以导致线程一直等待。而如果是buffer,则可以暂时放入buffer,等需要的时候才读写,不需要服务器和客户端同步
JVM类加载机制
一:加载
这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口
二:验证
确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
三:准备
这里我理解为静态类都会分配,这也解释了为什么普通类不能有静态变量和静态方法
为类变量和常量分配内存并设置初始值阶段,即在方法区中分配这些变量所使用的内存空间。
注意这里所说的初始值概念,类变量定义为:
public static int v = 8080;
实际上变量 v 在准备阶段过后的初始值为0而不是8080,将 v 赋值为 8080 的 put static指令是程序被编译后,存放于类构造器方法之中。
但是注意如果声明常量:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080
四:解析静态链接
常量池的符号引用转化为直接引用
符号引用:可以不存在于内存中
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。
直接引用:
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中
五:初始化:
真正执行类中定义的 Java 程序代码,执行类构造器方法。理解执行静态变量赋值和静态语句快
方法是由编译器自动收集类中的静态变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
类加载器:
1:启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2:扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3:应用程序类加载器(Application ClassLoader):
负责加载用户路径(classpath)上的类库。
双亲委派:
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
来源:CSDN
作者:取个程序猿的名字
链接:https://blog.csdn.net/weixin_45680007/article/details/103455485