JVM相关

1.4java虚拟机
关于Java虚拟机,在面试的时候一般会问的大多就是①Java内存区域、②虚拟机垃圾算法、③虚拟机垃圾收集器、④JVM内存管理、⑤JVM调优、⑥Java类加载机制这些问题了
在这里插入图片描述
其实执行一个类就是将它的字节码丢到JVM中去运行
jvm由类加载子系统、运行时数据区和字节码执行引擎三部分组成。比如说执行一个类,其实就是将它的字节码丢到JVM里去运行。首先会由类加载子系统将字节码文件装载到运行时数据区(其实就是jvm的内存区域),最终再由字节码执行引擎来运行内存区域里面的代码(字节码执行引擎是负责执行代码的JVM的子系统)。
面试常见问题:
1.java为什么是跨平台的语言,这个过程是什么
java的跨平台特性其实是java虚拟机做到的,比如说初学java需要去官网下jdk(有Linux版本和Windows版本等)不同平台对应的jdk的底层有基于这个特定操作系统平台实现好的JVM,所以将代码放到不同的操作系统上运行实际上是放到了不同版本的jdk里面的jvm去执行代码,不同jvm会生成对应该操作系统的机器码。
jvm是从软件层面屏蔽掉不同操作系统在底层硬件与指令上的区别
在这里插入图片描述
(扩展:JRE是支持Java程序运行的核心类库)
2.介绍下 Java 内存区域(运行时数据区)/内存划分
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,如图:
JDK1.8以前:
在这里插入图片描述
JDK1.8:
在这里插入图片描述
线程私有的:
•程序计数器•虚拟机栈•本地方法栈
线程共享的:
•堆•方法区•直接内存(非运行时数据区的一部分)
1)程序计数器(PC寄存器)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

从上面的介绍中我们知道程序计数器主要有两个作用:
•字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
•在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
(程序计数器实际上是动态变化的,每运行完一行代码,字节码执行引擎都会去修改程序计数器的值)
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2)java虚拟机栈(也可以叫做线程栈)
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,
每一个线程运行jvm会给这个线程分配一块自己独立的内存空间,用于存放程序运行过程中它的局部变量所需要的内存空间。当线程执行方法时,会给该方法在这一大块栈内存空间里分配一块专属内存空间用来存放方法的局部变量,jvm为了区分不同方法的局部变量内存作用域范围,将每个方法都划一块自己独立的内存空间,这块内存空间叫做栈帧内存区域(栈帧的结构是数据结构中的栈FIFO,栈这种数据结构和程序的运行很贴合)。
虚拟机栈描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
在这里插入图片描述
局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)即放局部变量的一些表(一般来说new出来的对象放在堆中,局部变量表中放的是对象在堆内存的位置即存放的是地址也可以理解为指针)。
操作数栈:操作数在程序执行运算过程中临时中转存储的内存空间(内部数据结构也是用栈来存储的)
动态链接:当父类中的一个方法只有在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用; 对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。
方法出口:方法出口里放的就是标识着一个方法执行完应该返回到main方法的具体哪一行代码去执行
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
•StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。•OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
扩展:那么方法/函数如何调用?
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java方法有两种返回方式:
•return 语句。
•抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
3)本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
(新旧系统相互调用(旧系统一般用的c语言,新系统用的java,c语言内存分配 、内存回收都需要自己去做,而java语言是jvm帮忙做的)就是通过native实现的,字节码执行引擎去找native这行代码在操作系统底层库函数里找它对应的c语言的实现)
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4)堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:新生代又分为Eden空间和Survivor区(From Survivor、To Survivor)。默认内存比例,老年代占整个堆的三分之二,年轻代占整个堆的三分之一(年轻代中伊甸园区和Survivor区的大概配比为8:1:1)
进一步划分的目的是更好地回收内存,或者更快地分配内存。
在这里插入图片描述
(new出来的对象放在堆中的伊甸园区,如果Eden区放满了的话,册灰姑娘徐还在运行往里放对象的话,字节码执行引擎底层就会开启一个线程堆Eden区执行垃圾收集即minor gc,…具体垃圾回收过程下面讲解)
5)方法区(元空间)
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(字节码文件最终会被加载到方法区里面,代码最终由字节码执行引擎去执行)。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
JDK 1.8 的时候,方法区被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。
我们可以使用参数: -XX:MetaspaceSize 来指定元数据区的大小。与永久区很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
在JDK1.8之前叫做永生代/持久代,在JDK1.8之后叫元空间,严格意义上来讲,在JDK1.8之后不算做java运行时数据区/java内存区 的一部分(严格上来说方法区用的不是java虚拟机分配的内存),这块区域用的是直接内存,也就是说物理内存
6)运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
在这里插入图片描述
7)直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

(线程栈/虚拟机栈,本地方法站,程序计数器 是每个线程独有的;堆和方法区是所有线程共享的内存区)

说说Java中的内存分配?
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作它用。
堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因。但是在写程序的时候,可以人为的控制。

3.java堆和栈的区别
堆和栈都是Java用来在RAM中存放数据的地方。

(1)Java的堆是一个运行时数据区,类的对象从堆中分配空间。这些对象通过new等指令建立,通过垃圾回收器来销毁(存储的是数组和对象(其实数组就是对象))。如果一个数据消失,这个实体也没有消失(实体用于封装数据),还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了
(2)堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。

(1)存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),这些基本变量主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和对象的引用。先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
(2)栈的优势是,存取速度比堆快。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。

堆和栈的联系
一般来说,new出来的对象放在堆里,局部变量表这块内存区域里放的是对象在堆内存的位置及存放的是地址/引用,也可以理解为指针(方法区也是同理,如果方法区中的静态变量也是new出来的话及对象类型的静态变量,那么和栈中的局部变量也是一样,放的是对象在堆中的内存位置)。
4.虚拟机垃圾算法
(Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收,因为Java语言规范并不保证GC一定会执行。回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。
ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。)

jvm的垃圾回收首先需要标注出可回收的对象,采用的是可达性分析算法(对象存活性判断,常用的方法有两种:1.引用计数法; 2.对象可达性分析。由于引用计数法存在互相引用导致无法进行 GC 的问题,所以目前 JVM虚拟机多使用对象可达 性分析算法)
可达性分析算法:将GC Roots对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象称该对象为可达的,其余的对象都是非可达的
(不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了,finalize()方法最终判定对象是否存活;第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则,直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GCRoots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GCRoots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收)
finalize作用
Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
GC Roots根节点:线程栈中的局部变量、本地方法栈中的变量、方法区中的静态变量、常量
在这里插入图片描述
详细见:https://blog.csdn.net/luzhensmart/article/details/81431212
常见的jvm的垃圾回收算法有以下三类,列举如下:
1. 标记清除算法:
标记-清除算法是最基础的算法,像它的名字一样算法分为“标记”和“清除”两个阶段,首先需要标记出所需要回收的对象,标记完成后统一收集被标记的对象。
优点: 实现简单。
缺点: 产生不连续的内存碎片,导致相应的内存使用率过低;“标记”和“清除”的执行效率都不高,标记和清除的过程耗时过高。
标记-清除算法执行过程图:
在这里插入图片描述
2.复制算法
复制算法就是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。
在这里插入图片描述
适用场景:如果系统中的垃圾对象很多,复制算法需要复制的存活对象就会相对较少
优点:可确保回收的内存空间是没有碎片的。
缺点:空间利用率低,因为复制算法的代价是将系统内存空间折半,只使用一半空间,而且如果内存空间中垃圾对象少的话,复制对象也是很耗时的,因此,单纯的复制算法也是不可取的。

现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

3.标记整理算法
当对象存活率较高时,复制算法就会存在问题了。效率将会变低。如果不想浪费50%的空间,就需要额外的空间进行分配担保。因此,老年代一般不适用这种算法。根据老年代的特点,有人提出了另外一种标记-整理算法,也称标记-压缩算法,标记-整理算法采用和标记清除算法一样的对象“标记”,但后续不会对可回收对象进行清理,而是将存活的对象往一端空闲空间移动,然后清理边界以外的内存空间。
优点: 解决了内存碎片问题,比复制算法空间利用率高。
缺点: 因为有局部对象移动,相对效率不高。
标记-整理算法执行过程图
在这里插入图片描述
4.分代收集算法
目前商用虚拟机都采用的是分代收集的算法,这种算法按照对象存活周期把内存分为几块,一般Java中分为新生代和老年代。把存活率低的对象分到新生代使用复制算法提高垃圾回收的性能,老年代则存放存活率高的对象,使用标记-清除和标记-整理的算法,提高内存空间使用率。
在这里插入图片描述
垃圾回收执行细节
下面介绍一下HotSpot虚拟机在执行垃圾回收时的一些细节
HotSpot虚拟机: 它是Sun JDK和OpenJDK自定的虚拟机,也是目前使用最广泛的虚拟机。
垃圾回收流程: Java虚拟机在内存回收之前,为了保证内存的一致性,必须先暂停程序的执行,也就是传说中的Stop The World(简称STW),在使用可达性分析算法枚举GC Roots,标记出死亡对象,再进行垃圾回收。
垃圾回收遇到的问题: 那既然是要暂停程序的运行,就一定要保证停止的时间足够短,并且可控,不然带来的灾难将是毁灭性的。
解决方案:显然HotSpot在设计的时候也考虑到了这个问题,所以在JIT编译的时候就会使用OopMap数据结构来记录栈和寄存器上的引用,这样虚拟机就直接知道了那些地方存放着对象的引用(OopMap数据结构存储了普通对象的指针引用)
安全点(Safepoint)
在OopMap的协助下,HotSpot可以快速的完成GC Roots枚举,但导致OopMap内容变化的指令很多,而且如果给每个对象生成对应的OopMap,会造成大量额外的空间,这会导致GC成本很高,所以HotSpot只会在“特定的位置”生成对应的OopMap,这些位置就成为“安全点”。
HotSpot也并不是任何时刻都会停顿下来进行GC,只会在程序都到底安全点之后才会GC,所以安全点的设置不能太少,让GC等待时间太长,也不能太多增大运行时的成本。
安全点的两种线程中断方式
抢断式中断:不需要线程的执行代码去主动配合,当发生GC时,先强制中断所有线程,然后如果发现某些线程未处于安全点,恢复程序运行,直到进入安全点为止。

主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起。目前所有商用虚拟机全部采用主动式中断。
安全区域(Saferegion)
安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然HotSpot不可能一直等待被阻塞或休眠的线程正常唤醒执行;此时就引入了安全区的概念。
安全区(Saferegion):安全区域是指在一段区域内,对象引用关系等不会发生变化,在此区域内任意位置开始GC都是安全的;线程运行时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,HotSpot发起GC时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了GC Roots枚举(或这个GC过程),如果完成了就继续执行,否则将继续等待直到收到可以安全离开的Safe Region的信号为止。

5.虚拟机垃圾收集器
什么是Java垃圾回收器
Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Allocation)、自动回收(Garbage Collect)功能,这两个操作都发生在Java堆上(一段内存快)。某一个时点,一个对象如果有一个以上的引用(Rreference)指向它,那么该对象就为活着的(Live),否则死亡(Dead),视为垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作。有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收。

说垃圾收集器之前先明确几个概念:
并发和并行
这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。
串行(serial):单线程,按顺序依次执行多任务;
并行(Parallel):在垃圾收集语境中指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):在垃圾收集语境中指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
这里穿插一道面试题:
并发与并行的区别
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。
并行
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
如下图所示:
在这里插入图片描述
所以,并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。
并行和并发的区别
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、

只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

Minor GC 和 Full GC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
下面来说垃圾收集器:
GC收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
在这里插入图片描述
图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
(1)Serial收集器
顾名思义,是一个单线程串行垃圾回收器,使用的是复制算法,运行时会stop the world
但并非一无是处,在单核系统上运行减少了上下文交互开销,如果gc不是频繁发生可获得较高的单线程回收效率。Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
在这里插入图片描述
特性
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
应用场景
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
(2)ParNew收集器
相当于serial收集器的多核版,也采用复制算法,并行,运行时仍然stop the world。JDK 5引入CMS收集器后,它是默认的和CMS收集器搭配的新生代收集器。不过在单核系统中,并不比serial收集器效率高。
在这里插入图片描述
特性
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
应用场景
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
Serial收集器 VS ParNew收集器
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
(3)Parallel Scavenge收集器
在这里插入图片描述
特性
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
应用场景
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
对比分析
Parallel Scavenge收集器 VS CMS等收集器
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,ParNew、CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。

Parallel Scavenge收集器 VS ParNew收集器
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

GC自适应的调节策略
Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

parallel scavenge收集器为什么不能CMS配合使用
首先讲一下Hotspot,HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用,所谓的“mix-and-match”。
而ParallelScavenge与G1则不在这个框架内,而是各自采用了自己特别的框架。这是因为新的GC实现时发现原本的分代式GC框架用起来不顺手。

(4)Serial Old收集器
在这里插入图片描述
特性
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,串行,运行时会stop the world,使用标记-整理算法
应用场景
Client模式
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。

Server模式
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

(5)Parallel Old收集器
特性
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程、并行,运行时会stop the wolrd,采用的是标记-整理算法,强调吞吐量优先。
应用场景
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。
(6)CMS收集器
在这里插入图片描述
特性
CMS(Concurrent Mark Sweep并发标记清除收集器)收集器是一种以获取最短回收停顿时间为目标的收集器(响应时间优先的垃圾收集器)。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字可以看出CMS收集器是一个并行的、基于“标记—清除”算法实现的老年代收集器,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
1,初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象即标记目标对象是否可达,速度很快,需要“Stop The World”。
2,并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程即将不可达对象标记为垃圾对象。
3,重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录即认垃圾对象仍然没有根引用,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
4,并发清除(CMS concurrent sweep)
并发清除阶段会清除对象即清理垃圾对象。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
优点
CMS是一款优秀的收集器,它的主要优点在名字上(全称是Concurrent Mark-Sweep Collector)已经体现出来了:并发收集、低停顿。
缺点

  • CMS收集器对CPU资源非常敏感、运行时可能造成应用程序变慢
    其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
    CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
  • CMS收集器无法处理浮动垃圾(GC过程中产生的新垃圾)
    CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败(GC期间预留内存无法满足需要)而导致另一次Full GC的产生。
    由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  • CMS收集器会产生大量空间碎片
    CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
    空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
    (7)G1收集器
    在这里插入图片描述
    特性
    G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
  • 并行与并发
    G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集
    与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合
    与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿
    这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
    G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

执行过程
G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)
    初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象即标记目标对象是否可达,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking)
    并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,将不可达对象标记为垃圾对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking)
    最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation)
    筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

总结
虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了。

6.垃圾回收的过程
垃圾回收主要集中在两个区域:按照java虚拟机内存模型,即堆区,方法区的元数据区;
堆区垃圾收集过程如下:
1,对象创建后放在堆的新生代的eden区;
2,当内存不足的时候或者周期性的触发minorGC,把没有标记的对象复制到survive区,标记的对象直接回收;
3,jvm的生存周期内不断的循环: 触发minorGC,service区转换为From区,不断的把Eden和From区还存在的对象复制到to区,
并进行整理,防止碎片化;把存在周期即分代年龄超过jvm设置的阈值的对象复制到老年代old区(大对象,survivor区放不下的对象也放入老年代;web应用程序中的静态变量、数据库对象、连接池对象、缓存对象等最终都会变成“老不死”对象被放入老年代);
4,当整个堆区内存不足的时候,触发fullGC,重新整理eden,from,to,old区,一般会造成系统的处理能力急速下降。

元数据区的垃圾回收:主要是当某些类型不再使用的时候,从元数据区卸载。

动态年龄判断机制:当前放对象的survivor区域里,一批对象的总大小大于这块survivor区域内存大小的50%,那么此时大于等于这批对象年龄最大值的对象就可以直接进入老年代了(动态年龄判断机制一般是在munor gc之后触发的)

还有一些会问到的:
java 当中的四种引用
强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在 GC 上:
强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空
间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。
如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为 null,这样一
来的话,JVM 在合适的时间就会回收该对象。
软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会
被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
弱引用
如果一个对象只有弱引用,那就类似于可有可无的生活用品。弱引用和软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没
有引用,虚引用并不会决定对象的生命周期,在任何时候都可能被垃圾回收器回收。

调用 System.gc()会发生什么?
通知 GC 开始工作,但是 GC 真正开始的时间不确定。

GC是什么,为什么要使用它
GC是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域,从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

手动GC回收

public class JVMDemo05 {
 	public static void main(String[] args) {
  		JVMDemo05 jvmDemo05 = new JVMDemo05(); 
  		//jvmDemo05 = null; 
  		System.gc(); 
  } 
  protected void finalize() throws Throwable {
   		System.out.println(“gc在回收对象…”); 
   } 
   }

垃圾回收时的停顿现象(STW)
垃圾回收的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以更高效的执行,大部分情况下,会要求系统进如一个停顿的状态。停顿的目的是为了终止所有的应用线程,只有这样的系统才不会有新垃圾的产生。同时停顿保证了系统状态在某一个瞬间的一致性,也有利于更好的标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。

GC经常回收好不好
不好,GC在回收线程的时候,其他线程全部等待。时间很短,速度快,看不到效果。

频繁Full GC问题
频繁FULL GC有可能有如下几种原因:

  • 老年代空间不足。
  • 永生代或者元数据空间不足。
  • System.gc()方法调用。
  • CMS GC时出现promotion failed和concurrent mode failure
  • YoungGC时晋升老年代的内存平均值大于老年代剩余空间
  • 有连续的大对象需要分配

接下来介绍两种方法对Full GC 问题进行排查:
①利用GC日志
在VM options处设置JVM参数,我这里是将GC日志输出到D盘的iFit_gc.log文件中,GC参数设置可以参考下图
在这里插入图片描述
②利用JVM性能调优神器,即VisualVM

7.JVM内存管理
jvm内存管理从jvm内存划分、各个部分详细解读、垃圾回收这几个方面来说
8.JVM调优
一.jvm分为年轻代,年老代,持久代
1.年轻代:年轻代主要存放新创建的对象,垃圾回收会比较频繁。(稍微讲细一点就是即可,年轻代分成Eden Space和Suvivor Space。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space,如果对象仍然存活,则复制到Suvivor Space。)
2.年老代:年老代主要存放JVM认为生命周期比较长的对象(在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到OldGen。)
3.持久代:持久代主要存放类定义、字节码和常量等很少会变更的信息。
二.引出gc算法
年轻代使用的是复制算法(避免频繁创建对象导致碎片过多,一般会对算法优化来规避算法占用内存的问题,优化后有效内存能近乎达到百分之90,估计也不会问那么多,点到为止)
年老代使用的标记-整理算法(因为较少的发生gc,使用标记整理算法提高内存利用率)
直观的对比:
效率:复制算法>标记-整理算法
内存整齐度:复制算法=标记-整理算法
内存利用率:标记-整理算法>复制算法
三.如何实施调优
jvm参数设置,根据机器性能为程序运行分配合理区大小
四.善后工作
使用jdk自带的jvisualvm,jconsole等工具监测程序是否发生线程阻塞,内存泄漏,以及观察gc频率是否存在异常等

调优总结
初始堆值和最大堆内存内存越大,吞吐量就越高。
最好使用并行收集器,因为并行手机器速度比串行吞吐量高,速度快。
设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
减少GC对老年代的回收。

说下你熟悉那些jvm参数调优
-XX:+UseSerialGC 设置使用新生代串行和老年代串行回收器
XX:+UseParNewGC新生代并行回收器

你是通过什么方式实现JVM优化的
  1》调整新生代、s0、s1比例以及调整新生代 老年代的大小,目的是减少GC次数
  2》不要显示的调用system.GC()
  3》对象不用的时候显式的置为null
  4》尽量减少局部变量的使用
  5》尽量使用基本数据类型而不使用封装类
  6》尽量少使用静态变量,因为静态变量属于类的变量,是全局变量,会一直占用资源
  7》尽量分散的创建对象,不要一次性创建多个对象。
  
9.对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于,检查HandlePromotionFailure(是否允许担保失败)设置,如果true则只进行Monitor GC,如果false则进行FullGC。
    在这里插入图片描述

10.Java 的内存模型 JMM
在这里插入图片描述
Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model, JMM)来屏蔽掉各层硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,**线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。**不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的关系如上图。

11.两个线程之间是如何通信的呢
共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
在这里插入图片描述
例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

  • 1.首先,线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去
  • 2.然后,线程 B 到主内存中去读取线程 A 之前更新过的共享变量
    在这里插入图片描述
    消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在 Java 中典型的消息传递方式就是 wait() 和 notify()。

12.内存屏障
解析:在这之前应该对重排序的问题有所了解,这里我找到一篇很好的文章分享一下:Java内存访问重排序的研究
答:内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

13.内存屏障为何重要
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操 作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。如果把 这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。

14.类似-Xms、-Xmn这些参数的含义
堆内存分配:

  • JVM初始分配的内存由-Xms指定,默认是物理内存的1/64
  • JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4
  • 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
  • 因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

非堆内存分配:

  • JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
  • 由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
  • -Xmn2G:设置年轻代大小为2G。
  • -XX:SurvivorRatio,设置年轻代中Eden区与Survivor区的比值。

15.内存泄漏和内存溢出
概念:

  • 内存溢出指的是内存不够用了。
  • 内存泄漏是指对象可达,但是没用了。即本该被GC回收的对象并没有被回收
  • 内存泄露是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。

内存泄漏的原因分析:

  • 长生命周期的对象引用短生命周期的对象
  • 没有将无用对象置为null

16.Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
Java中对象的创建就是在堆上分配内存空间的过程,此处说的对象创建仅限于new关键字创建的普通Java对象,不包括数组对象的创建。
大致过程如下:
1)检测类是否被加载
当虚拟机执行到new时,会先去常量池中查找这个类的符号引用。如果能找到符号引用,说明此类已经被加载到方法区(方法区存储虚拟机已经加载的类的信息),可以继续执行;如果找不到符号引用,就会使用类加载器执行类的加载过程,类加载完成后继续执行。
2)为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
具体的分配内存有两种情况:第一种情况是内存空间绝对规整,第二种情况是内存空间是不连续的。

  • 对于内存绝对规整的情况相对简单一些,虚拟机只需要在被占用的内存和可用空间之间移动指针即可,这种方式被称为指针碰撞。
  • 对于内存不规整的情况稍微复杂一点,这时候虚拟机需要维护一个列表,来记录哪些内存是可用的。分配内存的时候需要找到一个可用的内存空间,然后在列表上记录下已被分配,这种方式成为空闲列表。

分配内存的时候也需要考虑线程安全问题,有两种解决方案:

  • 第一种是采用同步的办法,使用CAS来保证操作的原子性。
  • 另一种是每个线程分配内存都在自己的空间内进行,即是每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),分配内存的时候再TLAB上分配,互不干扰。

3)为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
4)对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。
5)执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于Java程序来说还需要执行init方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了init方法之后,这个对象才真正能使用。

到此为止一个对象就产生了,这就是new关键字创建对象的过程。

17.对象的内存布局是怎样的
对象的内存布局包括三个部分:对象头,实例数据和对齐填充。

  • 对象头:对象头包括两部分信息,第一部分是存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁等等。第二部分是类型指针,即对象指向类元数据的指针。
  • 实例数据:就是数据啦
  • 对齐填充:不是必然的存在,就是为了对齐的嘛

18.对象的访问定位的两种方式(句柄和直接指针两种方式)
对象的访问定位有两种:句柄定位和直接指针

  • 句柄定位:Java堆会画出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
    在这里插入图片描述
  • 直接指针访问:java堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
    在这里插入图片描述
    比较:使用直接指针就是速度快,使用句柄reference指向稳定的句柄,对象被移动改变的也只是句柄中实例数据的指针,而reference本身并不需要修改。

19.JVM工作原理
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
在这里插入图片描述
(java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行)
运行jvm字符码的工作是由解释器来完成的。解释执行过程分三步进行:
代码的装入、代码的校验、和代码的执行。
装入代码的工作由“类装载器classloader”完成。类装载器负责装入运行一个程序需要的所有代码,这也包括程序代码中的类所继承的类和被调用的类。当类装载器装入一个类时,该类被放在自己的名字空间中。除了通过符号引用自己名字空间以外的类,类之间没有其他办法可以影响其他类。在本台计算机的所有类都在同一地址空间中,而所有从外部引进的类,都有一个自己独立的名字空间。这使得本地类通过共享相同的名字空间获得较高的运行效率,同时又保证它们与从外部引进的类不会相互影响。当装入了运行程序需要的所有类后,解释器便可确定整个可执行程序的内存布局。解释器为符号引用与特定的地址空间建立对应关系及查询表。通过在这一阶段确定代码的内布局,java很好地解决了由超类改变而使子类崩溃的问题,同时也防止了代码的非法访问。随后,被装入的代码由字节码校验器进行检查。校验器可以发现操作数栈益处、非法数据类型转化等多种错误。通过校验后,代码便开始执行了
Java字节码的执行有两种方式:
1)即时编译方式:解释器先将字节编译成机器码,然后再执行该机器码。
2)解释执行方式:解释器通过每次解释并执行一小段代码来完成java字节码程序的所有操作。

20.Java类加载机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值