java虚拟机JVM

tech2024-11-02  22

1 java虚拟机

运行Java字节码的虚拟机,

包括

一套字节码指令集,

一组程序寄存器,

一个虚拟机栈,

一个虚拟机堆,

一个方法区,

一个垃圾回收器。

Java源文件通过编译器编译成相应的.class文件(字节码文件)

.class文件(字节码文件)被jvm中的解释器变成机器码

机器码在不同的操作系统上运行(操作系统的解释器不同,但虚拟机是相同的,所以Java能跨平台)

Java虚拟机包括:

一个类加载器子系统(Class Loader SubSystem)

运行时数据区(Runtime Data Area)

执行引擎和本地接口库(Native Interface Library)

本地接口库通过调用本地方法库与操作系统交互

2 多线程

jvm允许一个进程内同时并发执行多个线程

jvm中的线程与操作系统的线程是相互对应的

jvm后台运行的线程主要有:

虚拟机线程:虚拟机线程挂在JVM到达安全点(SafePoint)时出现

周期性任务线程:通过定时器调度线程来实现周期性任务的执行

GC线程:GC线程支持JVM中不同的垃圾回收活动

编译器线程:编译器线程在运行时将字节码动态编译成本地平台机器码,是JVM跨平台的具体实现

信号分发线程:接收发送到JVM的信号并调用JVM方法

3 jvm内存区域

分为:

线程私有区域

(程序计数器,虚拟机栈,本地方法区):

生命周期与线程相同,随线程启动而创建,随线程的结束而销毁。

3.1程序计数器

用于存放当前运行的线程所执行的字节码的行号指示器。

每个运行中的线程都有一个独立的程序计数器,

当方法正在执行是,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;

如果该方法执行的是Native方法,则程序计数器的值为空(Underfined)

3.2虚拟机栈

描述Java方法的执行过程的内存模型

它在当前栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等信息。

栈帧(Stack Frame,***栈帧也叫过程活动记录,是编译器用来实现过程函数调用的一种数据结构。***)用来存储部分运行是数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。

3.3本地方法区

与虚拟机栈的作用类似,区别就是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。

线程共享区域

(方法区,堆):

随虚拟机的启动而创建,随虚拟机的关闭而销毁。

3.4堆

在jvm运行过程中创建的对象和产生的数据都被存储在堆中,

堆是 被线程共享的内存区域,

也是垃圾收集器进行垃圾回收的最主要的内存区域。

现代jvm采用分代收集算法,

因此Java堆从GC(Garbage Collection ,垃圾回收)的角度可以细分为:新生代,老年代和永久代。

3.5方法区

方法区也被称为永久代,

用于存储常量,静态变量,类信息(class文件,包括:类的版本,字段,方法,接口描述,还有常量信息),即时编译器编译后的机器码,运行时常量池等数据。

常量被存储在运行时常量池中,是方法区的一部分。

静态变量也属于方法区的一部分。

直接内存:

也叫堆外内存,他并不是JVM运行是数据区的一部分,但在并发编程中被频繁使用。

jdk的NIO模块提供的基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,

NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,

然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,

Java进程可以通过堆外内存避免在Java堆和Native堆中来回复制数据带来的资源浪费和性能消耗。

因此:堆外内存在高并发应用场景下被广泛使用(Netty,Flink,Hbase,Hadoop)

4 jvm运行时内存

也叫做jvm堆,从GC的角度可以将JVM堆分为新生代,老年代和永久代。

新生代默认占1/3,老年代默认占2/3,永久代栈很少的堆内存空间。

新生代分为Eden区(8/10),SurvivorFrom区(1/10),SurvivorTo区(1/10)

4.1新生代

JVM新创建的对象(除了大对象)会被存放在新生代,

由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

Eden区(8/10)

Java新创建的对象首先存放在Eden区,如果为大对象,则直接分配到老年代。

大对象的定义:和JVM版本,堆大小,垃圾回收有关,一般为2kb~128kb ,可通过XX:PretenureSizeThreshold设置其大小。

在Eden区的内存空间不足是会触发MinorGC,堆新生代进行一次垃圾回收

SurvivorFrom区(1/10)

保留上一次MinorGC时的幸存者。

SurvivorTo区(1/10)

将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者。

新生代的GC过程叫做MinorGC,采用复制算法实现,具体如下:

1,把Eden和SurvivorForm区中存活的对象复制到SurvivorTo区。如果对象达到老年区标准,则复制到老年代,且年龄加1;如果SurvivorTo区内存不够,也复制到老年代;如果对象是大对象,则也复制到老年代。

2,清空Eden和SurvivorForm区。

3,将ServivorTo区和SurvivorForm区互换。

4.2老年代

主要存放长生命周期的对象和大对象。

老年代的GC过程叫做MajorGC。

老年代,对象比较稳定,MajorGC不会被频繁触发。

在进行MajorGC前,jvm会进行一次MinorGC,

MinorGC后仍然出现老年代 且 老年代空间不足 或 无法找到足够大的连续内存空间分配给新创建的大对象时,

会触发MajorGC进行垃圾回收,释放jvm的内存空间。

4.3永久代

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。

Class在类加载时被放入永久代。

永久代和新生代老年代不同,GC不会在程序运行期间堆永久代的内存进行清理。

这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常

比如:tomcat引用jar文件过多导致JVM内存不足而无法启动。

java8中永久代已经被元数据区取代(也叫做元空间)。

元数据区的作用和永久代相同,区别是:元数据区没有使用虚拟机的内存,而时直接使用操作系统的本地内存。

5 垃圾回收算法

5.1如何确定垃圾

Java采用引用计数法和可达性分析来确定对象是否该被回收

引用计数法容易产生循环引用的问题

可达性分析通过根搜索算法(GC Roots Tracing)来实现

1.引用计数法

Java中要操作对象,必须先获取对象的引用。

在为对象添加一个引用时,计数器加一;

为对象删除一个引用时,计数器减一;

如果一个对象的引用计数为0,则该对象没有被引用可以被回收。

两个对象互为引用时,采用引用计数法,会发生循环引用问题,无法被回收。

2.可达性分析

为了解决引用计数法的循环引用问题。

首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索。

如果GC Roots和一个对象之间没有可达路径,则该对象不可达。

在至少两次标记不可达后,才会被垃圾收集器回收。

5.2 Java中常用的垃圾回收算法

常用的有4种:

标记清除(效率低,内存碎片多)

复制(从Eden,SurvivorForm复制到SurvivoTo)

标记整理

分代收集

6.1Java中的4中引用类型

在Java中一切皆对象,对象的操作时通过该对象的引用实现的,Java中的引用类型有4种,分别为:强引用,软引用,弱引用和虚引用。

6.1.1强引用:

在Java中最常见的就是强引用。

在吧一个对象付给一个引用变量时,这个引用变量就是一个强引用。

有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。

因此,强引用是造成Java内存泄漏的主要原因。

6.1.2软引用

软银用通过Soft Reference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。

6.1.3弱引用

弱引用通过WakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。

6.1.4虚引用

虚引用通过Phantom Reference类实现,虚·······························引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。

9.Java网络编程模型

9.1阻塞I/O模型

是常见的I/O模型,在读写数据时客户端会发生阻塞。

工作流程为:

在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;

在内存数据就绪后,内核将数据复制到用户线程中,并返回I/O执行结果到用户线程,此时用户线程将接触阻塞状态并开始处理数据。

电信的阻塞I/O模型的例子为:

data = socket.read(),

如果内核数据没有就绪,Socket线程就会一志阻塞在read()中等待内核数据就绪。

9.2非阻塞I/O模型

指用户线程在发起一个I/O操作后,无需阻塞便可以马上得到内核返回的一个结果。

如果内核返回的结果为false,则发丝内核数据还没有准备好,需要稍后在发起I/O操作。

一旦内核中的数据准备好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程。

在非阻塞I/O模型中,用户线程需要不断询问内核数据是否就绪,在内存数据还未就绪时,用户线程可以处理其他任务,在内核数据就绪后可立即获取数据并执行相应的操作。

电信的非阻塞I/O模型一般如下:

while(true){ data =socket.read(); if(data == ture){//内核数据就绪 //获取并处理内核数据 break; }else{//内核数据未就绪,用户线程处理其他任务 //其他任务 } }

9.3多路复用I/O模型

是多线程并发贬称惯用的较多的模型,JavaNIO就是基于多路复用I/O模型实现的。

在多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在socket有读写事件时,才会通知用户线程进行I/O读写操作。

多路复用I/O模型中秩序一个线程就可以管理多个Socket,并且在真正有socket读写时间是才会使用操作系统的I/O资源,大大节约了系统资源。

JavaNIO咋i用户的每个线程中都通过selector.select()查询当前通道是否有事件到达,如果没有,则用户线程会一直阻塞。

而多路复用I/O模型通过一个线程管理多个socket通道,在socket有读写事件触发时才会通知用户线程进行I/O读写操作。

因此,多路复用I/O模型咋i连续数众多且消息体不大的情况下有很大优势。

非阻塞I/O模型在每个用户线程中都进行socket状态检查,而在多路复用I/O模型中实在系统内核中进行socket状态检查的,这也是多路复用效率高的原因。

在实际引用中,在多路复用方法体内一般不建议做复杂逻辑运算,制作数据的接收和转发,将具体的业务转发给后面的业务线程处理。

9.4信号驱动I/O模型

咋i用户线程发起一个I/O请求操作后,系统会为该请求对应的socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑;

咋i内核数据就绪时,系统会发送一个信号到用户线程,用户线程在接收到该信号后,会在信号函数中调用对应的I/O读写操作完成实际的I/O请求操作。

9.5异步I/O模型

Java7中提供了Asynchronous I/O操作。

用户线程会发起一个Asynchronous read操作到内核,内核在教授到Asynchronous read请求后会立刻返回一个状态,来说明请求是否成功发起,在此过程中用户线程不会发生任何阻塞,接着,内核会等待数据准备完成并将数据复制到用用户线程中,在数据复制完成后内核会发送一个信号到用户线程,通知用户线程asynchronous读操作已完成。

在异步I/O模型中,用户线程不需要关心真个I/O操作时如何进行的,只需要发起一个请求,在接收到内核返回的成功或失败信号时说明I/O操作以及完成,直接使用数据即可。

9.6Java I/O

在整个Java.io包中最中的时5个类和1个接口。

5个类指的是File,OutputStream,InputStream,Writer,Reader,

1个接口指的时Serializable。

9.7Java NIO

Java NIO 的实现主要实际三大核心内容:

Selector(选择器),Channel(通道),Buffer(缓冲区)。

Selector用于监听多个Channel的事件,比如连接打开或数据到达。

因此,一个线程可以实现对多个数据Channel的管理。

传统I/O基于数据流进行I/O读写操作;而Java NIO基于Channel和Buffer进行I/O读写操作,并且数据总是被从Channel读取到Buffer,或者从Buffer写入Channel中。

Java NIO 和传统I/O的最大区别:

(1) I/O时面向流的,NIO时面向缓冲区的:

在面向流的操作i中,数据只能在一个流中连续进行读写,数据没有缓冲,因此字节流无法前后移动。

在NIO中每次都是将数据从一个Channel读取到一个Buffer中,在从Buffer写入Channel中,因此可以方便地在缓存区中进行数据的前后移动等操作。

(2)传统I/O的流操作时阻塞模式的,NIO的流操作时非阻塞模式的。

在传统I/O下,用户线程子啊调用read()或write()进行I/O读写操作时,该线程将一直被阻塞,直到数据被读取或数据被完全写入。

NIO通过selector监听channel上事件的变化,在channel上有数据发生变化时通知该线程进行读写操作,

对于读请求而言,在通道上有可用的数据时,线程将进行Buffer的读操作,在没有数据时,线程可以执行其他业务逻辑操作。

对于写操作而言,在使用一个线程执行写操作将一些数据写入某通道时,只需将channel上的数据异步写入Buffer即可,Buffer上的数据会被异步写入目标channel上,用户线程不需要等待整个数据完全被写入目标channel就可以继续执行其他业务逻辑。

1.channel

Channel和I/O中的Stream(流)类似,只不过Stream是单向的(例:Input Stream,OutputStream),而Channel是双向的,既可以用来进行读操作,也可以用来进行写操作。

NIO中Channel的主要实现有:FileChannel,DatagramChannel,SockerServer,Server Socket Channel,分别对应文件的I/O,UDP,TCPI/O的Socket Client和SockerServer操作。

2.Buffer

Buffer实际上是一个容器,其内部通过一个连续的字节数组存储I/O上的数据。

在NIO中,channel在文件,网络上对数据的读取或写入都必须经过Buffer。

客户端在向服务段发送数据时,必须先将数据写入Buffer中,然后将Buffer中的数据写到服务端对应的Channel上。服务端在接收数据时必须通过Channel将数据读入Buffer中,然后从buffer中读取数据并处理。

在NIO中,Buffer是一个抽象类,对不同的数据类型有不同的buffer实现类。常用的buffer实现类有:ByteBuffer,IntBuffer,CharBuffer,LongBuffer,

DoubleBuffer,FloatBuffer,ShortBuffer。

3.Selector

Selector用于检测多个注册的Channel上是否有I/O事件发生,并对检测到的I/O事件进行相应的响应和处理。

因此通过一个Selector线程就可以实现对多个Channel的管理,不必为每个连接都创建一个线程,避免线程资源的浪费和多线程之间的上下文切换导致的开销。

同时,Selector只有在Channel上有读写事件发生时,才会调用I/O函数进行读写操作,可极大减少系统开销,提高系统的并发量。

4.JavaNIO使用

MyServer服务端实现类,在该类中定义了

serverSocketChannel用于ServerSocketChannel的简历和端口的绑定;

byteBuffer用于不同Channel之间的数据交互;

selector用于监听服务器各个 Channel上数据的变化并做出响应。

在类构造函数中调用了初始化ServerSocketChannel的操作,定义了listener方法来监听Channel上的数据变化,解析客户端的数据并对客户换的请求做出响应。

public class MyServer { private int size = 1024; private ServerSocketChannel serverSocketChannel; private ByteBuffer byteBuffer; private Selector selector; private int remoteClientNum = 0; public MyServer(int port){ try { initChannel(port); } catch (IOException e) { e.printStackTrace(); System.exit(-1); } } public void initChannel(int port) throws IOException { //打开 Channel serverSocketChannel = ServerSocketChannel.open(); //设置为非阻塞模式 serverSocketChannel.configureBlocking(false); //绑定端口 serverSocketChannel.bind(new InetSocketAddress(port)); System.out.println("listener on port: " + port); //创建选择器 selector =Selector.open(); //向选择器注册通道 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //分配缓冲区的大小 byteBuffer = ByteBuffer.allocate(size); } //监听器,用于监听Channel上的数据变化 private void listener() throws Exception { while (true) { //返回的int值表示有多少个Channel处于就绪状态 int n =selector.select(); if (n == 0){ continue; } //每个selector对应多个SelectionKey,每个SelectionKey对应一个Channel Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); //如果SelectionKey处于连接就绪状态,则开始接收客户端的连接 if (key.isAcceptable()){ //获取Channel ServerSocketChannel server = (ServerSocketChannel) key.channel(); //Channel接收连接 SocketChannel channel =server.accept(); //Channel注册 remoteClientNum++; System.out.println("online clinent num = " + remoteClientNum); write(channel,"hello client 123".getBytes()); //远程客户端的连接数统计 } //如果通道以及处于读就绪状态,则读取通道上的数据 if (key.isReadable()){ read(key); } iterator.remove(); } } } private void read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); int count; byteBuffer.clear(); //从通道中读取数据到缓冲区 while ((count =socketChannel.read(byteBuffer)) > 0){ //byteBuffer写模式变为读模式 byteBuffer.flip(); while (byteBuffer.hasRemaining()){ System.out.println((char)byteBuffer.get()); } byteBuffer.clear(); } if (count < 0) { socketChannel.close(); } } private void write(SocketChannel channel, byte[] writeData) throws IOException { byteBuffer.clear(); byteBuffer.put(writeData); //byteBuffer写模式变为读模式 byteBuffer.flip(); //将缓冲区的数据写入通道中 channel.write(byteBuffer); } private void registerChannel(Selector selector,SocketChannel channel,int opRead) throws IOException { if (channel == null){ return; } channel.configureBlocking(false); channel.register(selector,opRead); } public static void main(String[] args) { try { MyServer myServer = new MyServer(9999); myServer.listener(); } catch (Exception e) { e.printStackTrace(); } } }

定义MyClient类来实现客户端的Channel逻辑,

其中,

connectServer方法用于和服务端建立连接,

receive方法用于接收服务端发来的数据,

send2Server用于想服务器发送数据。

public class MyClient { private int size = 1024; private ByteBuffer byteBuffer; private SocketChannel socketChannel; public void connectServer() throws IOException { socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999)); socketChannel.configureBlocking(false); byteBuffer =ByteBuffer.allocate(size); receive(); } private void receive() throws IOException { while (true) { byteBuffer.clear(); int count; //如果没有数据可读,则read方法一直阻塞,知道读取到新的数据 while ((count = socketChannel.read(byteBuffer)) > 0){ byteBuffer.flip(); while (byteBuffer.hasRemaining()) { System.out.print((char)byteBuffer.get()); } send2Server("say hi".getBytes()); byteBuffer.clear(); } } } private void send2Server(byte[] bytes) throws IOException { byteBuffer.clear(); byteBuffer.put(bytes); byteBuffer.flip(); socketChannel.write(byteBuffer); } public static void main(String[] args) throws IOException { new MyClient().connectServer(); } }

10 JVM的类加载机制

JVM的类加载分为5个阶段:加载,验证,准备,解析,初始化。

在类初始化完成后就可以使用该类的信息,在一个类不再被需要是可以从JVM中卸载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MIvaAjmJ-1599141436763)(C:\Users\ly\AppData\Roaming\Typora\typora-user-images\image-20200901154723429.png)]

1.JVM的类加载阶段

1.加载

JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。

类加载过程主要包含将class文件读取到运行区域的方法区内,在对中创建java.lang.Class对象,并封装类在方法去的数据结构的过程,在读取Class文件时既可以通过文件的形式读取,也可以通过jar包,war包读取,还可以通过代理自动生产Class或其他方式读取。

2.验证

主要用于确保Class文件符合当前虚拟机的要求,报站该虚拟机自身的安全,只有通过验证的class文件才能被JVM加载

3.准备

主要工作是在方法区为类变量分配内存空间并设置类中变量的初始值。

初始值指不同数据类型的默认值,这里需要注意final类型的变量和非final类型的变量在准备阶段的数据初始化过程不同。

public static long value = 1000;

静态变量value在准备阶段的初始值是0,将value设置为1000的动作是在对象初始化时完成的。

public static final int value = 1000;

JVM在编译阶段后会为final类型的变量value生成对应的ConstantValue属性,在准备阶段会根据ConstantValue数学将value赋值。

4.解析

JVM会将常量池中的符号引用替换为直接引用。

5.初始化

主要通过执行类狗爪槭的方法为类进行初始化。

方法时在编辑阶段有编译器自动收集类中静态语句块和变量的复制操作组成的。

JVM规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行。

2.类加载器

JVM提供了3种类加载器,分别是启动类加载器,扩展类加载器和应用程序类加载器。

1.启动类加载器:复制加载Java_HOME/lib目录种的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。

2.扩展类的加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。

3.应用程序类加载器:复制加载用户路径(classpath)上的类库。

处理上述3种类加载器,我们也可以通过继承java.alng.ClassLoader实现自定义的类加载器。

3.双亲委派机制

JVM通过双亲委派机制对类进行加载。

双亲委派机制指

​ 一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类完成,,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。

​ 若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,知道该类被成功加载,若找不到该类,则JVM会抛出ClassNotFound异常。

双亲委派类加载机制的类加载流程:

1.将自定义加载器挂载到应用程序类加载器。

2.应用程序类加载器将类加载请求委托给扩展类加载器。

3.扩展类加载器将类加载请求委托给启动器加载器

4.启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载

5.扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载

6.应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载

7.在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFound异常。

双亲委派机制的核心是保障类的唯一性和安全性。

如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程。

最新回复(0)