1、虚拟机的自动内存管理和C/C++的区别
C/C++开发程序时需要为每一个new操作去写对应的delete/free操作,不容易出现内存泄漏和溢出问题。而Java程序将内存控制权交给了Java虚拟机
2、JVM的运行机制
1、Java程序的具体运行过程如下:
- Java源文件被编译器编译成字节码文件
- JVM将字节码文件编译成相应操作系统的机器码
- 机器码调用相应操作系统的本地方法库执行相应的方法
2、虚拟机结构
Java虚拟机包括类加载器子系统、运行时数据区、执行引擎、本地接口库等
3、JVM的内存区域
1、内存区域划分: 线程私有区域、线程共享区域和直接内存
- 线程私有区域的生命周期与线程相同
- 线程共享区域的生命周期与虚拟机相同
- 直接内存也叫堆外内存,它不是JVM运行时数据区的一部分,但在并发编程中被频繁调用
2、程序计数器: 线程私有,无内存溢出问题
- 每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空
- 程序计数器属于“线程私有”的内存区域,它是唯一没有Out Of Memory(内存溢出)的区域
3、虚拟机栈: 线程私有,描述Java方法的执行过程
- 虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表、操作数栈、动态链接、方法出口等信息。
- 栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈
4、本地方法区: 线程私有
- 本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务
5、堆: 也叫作运行时内存,线程共享
- 在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域
- 由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永久代
6、方法区: 线程共享
- 方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据
- 永久代的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少
- 在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中
4、JVM的运行时内存
1、JVM的运行时内存也叫作JVM堆:可分类为如下图所示
2、新生代: Eden区、ServivorTo区和ServivorFrom区
- JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收
- Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代
- ServivorTo区:保留上一次MinorGC时的幸存者
- ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者
- 新生代的GC过程叫作MinorGC,采用复制算法实现
3、老年代
- 老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫作MajorGC,采用标记清除算法
- 在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间
4、永久代
- 永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
- Class在类加载时被放入永久代。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out Of Memory异常
- 需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。
5、确定垃圾
1、如何确定垃圾: Java采用引用计数法和可达性分析来确定对象是否应该被回收
2、引用计数法
- 在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收
- 引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收
3、可达性分析
- 具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的
- 不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。
6、垃圾回收算法
- 4种算法:标记清除、复制、标记整理、分代收集
1、标记清除算法
- 标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间
- 由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题
2、复制算法
- 复制算法是为了解决标记清除算法内存碎片化的问题而设计的。
- 复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可
- 复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费
3、标记整理算法
- 标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存
4、分代收集算法
- 无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法
- 分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特点选择了不同的算法
- 大部分JVM在新生代都采用了复制算法,JVM将新生代进一步划分为一块较大的Eden区和两块较小的Servivor区,Servivor区又分为ServivorFrom区和ServivorTo区。JVM在运行过程中主要使用Eden区和ServivorFrom区,进行垃圾回收时会将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,然后清理Eden区和ServivorFrom区的内存空间
- 老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法
- 在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。在永久代主要回收废弃的常量和无用的类
- JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。
- 在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。 在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Servivor区的对象经过一次GC后仍然存活,则其年龄加1。在默认情况下,对象在年龄达到15时,将被移到老年代
7、Java中的4中引用类型
- 强引用:在Java中最常见的就是强引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用;有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。
- 软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收
- 弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收
- 虚引用:虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态
8、分代收集算法和分区收集算法
- 分代收集算法:JVM根据对象存活周期的不同将内存划分为新生代、老年代和永久代,并根据各年代的特点分别采用不同的GC算法
- 分区收集算法:分区算法将整个堆空间划分为连续的大小不同的小区域,对每个小区域都单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小区域内存的大小灵活使用和释放内存
- 分区收集算法可以根据系统可接受的停顿时间,每次都快速回收若干个小区域的内存,以缩短垃圾回收时系统停顿的时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收
9、垃圾收集器
- JVM针对新生代和老年代分别提供了多种不同的垃圾收集器
- 针对新生代提供的垃圾收集器有Serial、ParNew、Parallel Scavenge
- 针对老年代提供的垃圾收集器有Serial Old、Parallel Old、CMS
- 还有针对不同区域的G1分区收集算法
1、Serial垃圾收集器: 单线程,复制算法
- Serial垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束
- 对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。
2、ParNew垃圾收集器: 多线程,复制算法
- ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程模式工作,除此之外和Serial收集器几乎一样
- ParNew垃圾收集器在垃圾收集过程中会暂停所有其他工作线程,是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器。
3、Parallel Scavenge垃圾收集器: 多线程,复制算法
- Parallel Scavenge收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务
4、Serial Old垃圾收集器: 单线程,标记整理算法
- Serial Old垃圾收集器是Serial垃圾收集器的老年代实现,同Serial一样采用单线程执行,不同的是,Serial Old针对老年代长生命周期的特点基于标记整理算法实现
- Serial Old垃圾收集器是JVM运行在Client模式下的老年代的默认垃圾收集器。
5、Parallel Old垃圾收集器: 多线程,标记整理算法
- Parallel Old垃圾收集器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现
6、CMS垃圾收集器
- CMS(Concurrent Mark Sweep)垃圾收集器是为老年代设计的垃圾收集器
- 其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性
7、G1垃圾收集器
- G1(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域
- 相对于CMS收集器,G1垃圾收集器两个突出的改进:基于标记整理算法,不产生内存碎片;可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收
10、网络编程模型
1、阻塞I/O模型
- 阻塞I/O模型的工作流程为:在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;在内存数据就绪后,内核将数据复制到用户线程中,并返回I/O执行结果到用户线程,此时用户线程将解除阻塞状态并开始处理数据
- 典型的阻塞I/O模型的例子为data = socket.read(),如果内核数据没有就绪,Socket线程就会一直阻塞在read()中等待内核数据就绪
2、非阻塞I/O模型
- 非阻塞I/O模型指用户线程在发起一个I/O操作后,无须阻塞便可以马上得到内核返回的一个结果
- 如果内核返回的结果为false,则表示内核数据还没准备好,需要稍后再发起I/O操作。一旦内核中的数据准备好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程
3、多路复用I/O模型
- 在多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在Socket有读写事件时,才会通知用户线程进行I/O读写操作。
- 多路复用I/O模型是多线程并发编程用得较多的模型,Java NIO就是基于多路复用I/O模型实现的
- 在实际应用中,在多路复用方法体内一般不建议做复杂逻辑运算,只做数据的接收和转发,将具体的业务操作转发给后面的业务线程处理
4、信号驱动I/O模型
- 在信号驱动I/O模型中,在用户线程发起一个I/O请求操作时,系统会为该请求对应的Socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑;
- 在内核数据就绪时,系统会发送一个信号到用户线程,用户线程在接收到该信号后,会在信号函数中调用对应的I/O读写操作完成实际的I/O请求操作
5、异步I/O模型
- 在异步I/O模型中,用户线程会发起一个asynchronous read操作到内核,内核在接收到synchronous read请求后会立刻返回一个状态,来说明请求是否成功发起,在此过程中用户线程不会发生任何阻塞
- 接着,内核会等待数据准备完成并将数据复制到用户线程中,在数据复制完成后内核会发送一个信号到用户线程,通知用户线程asynchronous读操作已完成
6、Java NIO
- Java NIO的实现主要涉及三大核心内容:Selector(选择器)、Channel(通道)和Buffer(缓冲区)
- 传统I/O基于数据流进行I/O读写操作;而Java NIO基于Channel和Buffer进行I/O读写操作,并且数据总是被从Channel读取到Buffer中,或者从Buffer写入Channel中
- Channel和I/O中的Stream(流)类似,只不过Stream是单向的,而Channel是双向的,既可以用来进行读操作,也可以用来进行写操作。
- Buffer实际上是一个容器,其内部通过一个连续的字节数组存储I/O上的数据
- Selector用于检测在多个注册的Channel上是否有I/O事件发生,并对检测到的I/O事件进行相应的响应和处理
11、JVM的类加载机制
1、JVM的类加载阶段: 加载、验证、准备、解析、初始化。
- 加载:类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程
- 验证:主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载
- 准备:主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值
- 解析:JVM会将常量池中的符号引用替换为直接引用
- 初始化:主要通过执行类构造器的方法为类进行初始化。方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。JVM规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行;在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法
2、类加载器: 启动类加载器、扩展类加载器和应用程序类加载器
- 启动类加载器:负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库
- 扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
- 应用程序类加载器:负责加载用户路径(classpath)上的类库
3、双亲委派机制
- JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中
4、OSGI
- OSGI(Open Service Gateway Initiative)是Java动态化模块化系统的一系列规范,旨在为实现Java程序的模块化编程提供基础条件
- 基于OSGI的程序可以实现模块级的热插拔功能,在程序升级更新时,可以只针对需要更新的程序进行停用和重新安装,极大提高了系统升级的安全性和便捷性
参考
- 书籍:《Offer来了》