面试资料-JAVA基础知识-JVM

JVM:

Java虚拟机是执行字节码文件(.class)的虚拟机进程。Java源程序(.java)被编译器编译成字节码文件(.class)。然后字节码文件,将由java虚拟机,解释成机器码(不同平台的机器码不同)。利用机器码操作硬件和操作系统。
因为不同的平台装有不同的JVM,它们能够将相同的.class文件,解释成不同平台所需要的机器码。正是因为有JVM的存在,java被称为平台无关的编程语言。

Java怎样实现一次编译到处运行?
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,JAVA语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令(机器码)。
为什么JVM不直接将源码进行编译成机器码去执行
(1)准备工作太过繁琐
JVM每次进行编译的时候都会对源代码进行各种检查,纠错
(2)兼容性
JVM不仅仅可以给java语言编译成的class文件进行解释,还可以对任何语言,只要是解释为.class字节码都可以解释

jvm类生命周期:

JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。
加载,查找并加载类的二进制数据(将类的class文件读入到内存),在Java堆中也创建一个java.lang.Class类的对象。类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。
连接,连接又包含三块内容:验证、准备、初始化。
1)验证,文件格式、元数据、字节码、符号引用验证; 验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用
初始化,为类的静态变量赋予正确的初始值
使用,new出对象程序中使用
卸载,执行垃圾回收
举例:
用户创建了一个Student对象,运行时JVM首先会去方法区寻找该对象的类型信息,没有则使用类加载器classloader将Student.class字节码文件加载至内存中的方法区,并将Student类的类型信息存放至方法区。
接着JVM在堆中为新的Student实例分配内存空间,这个实例持有着指向方法区的Student类型信息的引用,引用指的是类型信息在方法区中的内存地址。
在此运行的JVM进程中,会首先起一个线程跑该用户程序,而创建线程的同时也创建了一个虚拟机栈,虚拟机栈用来跟踪线程运行中的一系列方法调用的过程,每调用一个方法就会创建并往栈中压入一个栈帧,栈帧用来存储方法的参数,局部变量和运算过程的临时数据。上面程序中的stu是对Student的引用,就存放于栈中,并持有指向堆中Student实例的内存地址。
JVM根据stu引用持有的堆中对象的内存地址,定位到堆中的Student实例,由于堆中实例持有指向方法区的Student类型信息的引用,从而获得add()方法的字节码信息,接着执行add()方法包含的指令。将stu指向null

JVM GC

字节码文件:
什么是Class文件
Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧,可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
Java源文件在被Java编译器编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。
一个典型的class文件分为:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes这十个部分,用一个数据结构可以表示如下:

1、magic
在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。
2、minor_version 和 major_version
紧接着魔数的四个字节是class文件的此版本号和主版本号。
随着Java的发展, class文件的格式也会做相应的变动。 版本号标志着class文件在什么时候, 加入或改变了哪些特性。 举例来说, 不同版本的javac编译器编译的class文件, 版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同, 一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件, 而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件, JVM会抛出java.lang.UnsupportedClassVersionError 。具体的版本号变迁这里不再讨论, 需要的读者自行查阅资料。
3、constant_pool
在class文件中, 位于版本号后面的就是常量池相关的数据项。 常量池是class文件中的一项非常重要的数据。 常量池中存放了文字字符串, 常量值, 当前类的类名, 字段名, 方法名, 各个字段和方法的描述符, 对当前类的字段和方法的引用信息, 当前类中对其他类的引用信息等等。 常量池中几乎包含类中的所有信息的描述, class文件中的很多其他部分都是对常量池中的数据项的引用,比如后面要讲到的this_class, super_class, field_info, attribute_info等, 另外字节码指令中也存在对常量池的引用, 这个对常量池的引用当做字节码指令的一个操作数。此外,常量池中各个项也会相互引用。
常量池是一个类的结构索引,其它地方对“对象”的引用可以通过索引位置来代替,我们知道在程序中一个变量可以不断地被调用,要快速获取这个变量常用的方法就是通过索引变量。这种索引我们可以直观理解为“内存地址的虚拟”。我们把它叫静态池的意思就是说这里维护着经过编译“梳理”之后的相对固定的数据索引,它是站在整个JVM(进程)层面的共享池。
class文件中的项constant_pool_count的值为1, 说明每个类都只有一个常量池。 常量池中的数据也是一项一项的, 没有间隙的依次排放。常量池中各个数据项通过索引来访问, 有点类似与数组, 只不过常量池中的第一项的索引为1, 而不为0, 如果class文件中的其他地方引用了索引为0的常量池项, 就说明它不引用任何常量池项。class文件中的每一种数据项都有自己的类型, 相同的道理,常量池中的每一种数据项也有自己的类型。 常量池中的数据项的类型如下表:

4、access_flag
保存了当前类的访问权限
5、this_cass
保存了当前类的全局限定名在常量池里的索引
6、super class
保存了当前类的父类的全局限定名在常量池里的索引
7、interfaces
保存了当前类实现的接口列表,包含两部分内容:interfaces_count 和interfaces[interfaces_count]
interfaces_count 指的是当前类实现的接口数目
interfaces[] 是包含interfaces_count个接口的全局限定名的索引的数组
8、fields
保存了当前类的成员列表,包含两部分的内容:fields_count 和 fields[fields_count]
fields_count是类变量和实例变量的字段的数量总和。
fileds[]是包含字段详细信息的列表。
9、methods
保存了当前类的方法列表,包含两部分的内容:methods_count和methods[methods_count]
methods_count是该类或者接口显示定义的方法的数量。
method[]是包含方法信息的一个详细列表。
10、attributes
包含了当前类的attributes列表,包含两部分内容:attributes_count 和 attributes[attributes_count]
class文件的最后一部分是属性,它描述了该类或者接口所定义的一些属性信息。attributes_count指的是attributes列表中包含的attribute_info的数量。
属性可以出现在class文件的很多地方,而不只是出现在attributes列表里。如果是attributes表里的属性,那么它就是对整个class文件所对应的类或者接口的描述;如果出现在fileds的某一项里,那么它就是对该字段额外信息的描述;如果出现在methods的某一项里,那么它就是对该方法额外信息的描述。

通过示例代码来手动分析class文件:
案例一:

public class Hello{
      private int test;
      public int test(){
            return test;
        }
    }

编译成class文件:
十六进制结果

便于阅读,采用javap命令进行解析:反汇编

案例二:

解析后:

0: iconst_1 将int常量1进行放入操作数栈。这里稍微做个拓展,如果将float常量2进行入栈操作,name该指令是fconst_2。
1: invokestatic #2 调用常量池中序号为#2的静态方法,这里调用的是 Integer.valueOf()方法,表示将该int类型进行装箱操作,变为Integer类型
4: astore_1 在索引为1的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量a。
5: iconst_2 将int常量2进行放入操作数栈
6: invokestatic #2 调用常量池中序号为#2的静态方法,这里调用的是 Integer.valueOf()方法,表示将该int类型进行装箱操作,变为Integer类型
9: astore_2 在索引为2的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量b。

10: aload_1 从索引1的本地变量中加载一个int值,放入操作数栈

11: invokevirtual #3 调用常量池中序号为#3的实例方法,这里调用的是 Integer.intValue()方法
14: aload_2 从索引1的本地变量中加载一个int值,放入操作数栈

15: invokevirtual #3 调用常量池中序号为#3的实例方法,这里调用的是 Integer.intValue()方法
18: iadd 把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。

19: invokestatic #2调用常量池中序号为#2的静态方法,这里调用的是 Integer.valueOf()方法
22: astore_3 在索引为3的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量c。

23: return 方法结束

类加载器:

类加载机制:
全盘负责:
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

TOMCAT类加载机制:
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
tomcat 为了实现隔离性,违背了java 推荐的双亲委派模型,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
只是自定义的classLoader顺序不同,但顶层还是相同的,还是要去顶层请求classloader。

当tomcat启动时,会创建几种类加载器:
1 Bootstrap 引导类加载器
加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
2 System 系统类加载器
加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
3 Common 通用类加载器
加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
4 webapp 应用类加载器
每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
当应用需要到某个类时,则会按照下面的顺序进行类加载:
  1 使用bootstrap引导类加载器加载
  2 使用system系统类加载器加载
  3 使用应用类加载器在WEB-INF/classes中加载
  4 使用应用类加载器在WEB-INF/lib中加载
5 使用common类加载器在CATALINA_HOME/lib中加载
为什么java文件放在Eclipse中的src文件夹下会优先jar包中的class?
这是因为Eclipse中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class 中。
而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。
因此肯定是 java文件或者JSP文件编译出的class优先加载。

JVM模型:

方法区和堆是所有线程共享的内存区域;
jvm栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
程序计数器,
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。记录线程当前运行的代码的位置。当该线程的cpu时间片到了被挂起了,其他线程抢占了,该进程只能先挂起,后面恢复的时候通过计数器记录的位置开始继续。
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
也是线程私有的,它的生命周期与线程相同。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
动态连接:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError: 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。
本地方法栈,
与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
堆(Heap)
是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(部分可以在栈上)。
堆是垃圾收集器管理的主要区域,具体划分见下节。
方法区,
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(每个类的信息,包括类的名称、方法信息、字段信息)、常量、静态变量、即时编译器编译后的代码等数据。常量池(见下面),用来存储编译期间生成的字面量和符号引用。

运行时常量池:
运行时常量池是方法区的一部分。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用,比如全局变量。在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte, Short, Integer, Long, Character, Boolean;(即创建此包装类对象后,放入常量池中)前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。
但是,两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
在JDK1.7之前运行时常量池(包含字符串常量池)全部存放在方法区,也就是永久代。
(即在HotSpot虚拟机中,方法区就是永久代!)
在JDK1.7字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区(永久代)。
在JDK1.8字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间,hotspot移除了永久代用元空间(Metaspace)取而代之,。
字符串intern方法:
intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用。
如果不存在,
JDK 1.7前,原来在常量池中找不到时,复制一个放到常量池;
JDK 1.7后,将在堆上的地址引用复制到常量池。
元空间:
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用系统本地内存。
元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。
NIO(New Input/Output) 类引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  1. 字符串存在永久代中,现实使用中易出问题, 由于永久代内存经常不够用或发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen。整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

Java 对象的创建过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

GC:

堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为年轻代和老年代,再具体一点可以分为EdenSurvivor(又可分为From Survivor和To Survivor)、Tenured。
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值(默认为 15岁)对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发
java对象内存申请过程:
1.JVM会试图为相关Java对象在Eden中初始化一块内存区域;当Eden空间足够时,内存申请结束。否则到下一步;
2.JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
3.Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
4.当old区空间不够时,JVM会在old区进行major collection;
5.垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";

当新生代满了会触发minor GC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
在老年代满了会触发major GC,老年代对象存活时间比较长,因此FullGC发生的频率比较低。
当两者都满了时,会触发Full GC同时作用于新生代和老年代,清理整个堆空间。
Stop-The-World:
在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
Java中一种全局暂停的现象,jvm挂起状态。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互。类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。
多半由于jvm的GC引起,如:
1.老年代空间不足。
2.永生代(jkd7)或者元数据空间(jkd8)不足。
3.System.gc()方法调用。
4.CMS GC时出现promotion failed和concurrent mode failure
5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间
6.有连续的大对象需要分配
除了GC还有以下原因:
1.Dump线程–人为因素。
2.死锁检查。
3.堆Dump–人为因素。

回收的过程:
1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);
2)当Eden区满了或放不下了,这时候其中存活的对象会复制到from区。
这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉。
3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存。
4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次,就会进入年老代了。
5)当老年代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)。

为什么要区分新生代和老生代?
堆中区分的新生代和老年代是为了垃圾回收,新生代中的对象存活期一般不长,
而老年代中的对象存活期较长,所以当垃圾回收器回收内存时,新生代中垃圾回收效果较好,
会回收大量的内存,而老年代中回收效果较差,内存回收不会太多。

默认大小:
指令:java -XX:+PrintFlagsFinal -version |findstr /i "HeapSize PerSize ThreadStackSize"查看
堆内存默认初始大小为物理内存的64分之一,或者合理的最小值。我的机器物理内存是16G, 因此默认就是0.25G,也就是256M。
默认最大堆, 物理内存的四分之一,或者1G。我机器物理内存16G,默认最大值就是4G
默认大小比例:
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
eden : from : to = 8 : 1 : 1

JAVA 四中引用类型:
强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
是指创建一个对象并把这个对象赋给一个引用变量。
Object object =new Object();
或者:
String str =“hello”;
强引用有引用变量指向时永远不会被垃圾回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

软引用
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。
另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。
MyObject aRef = new MyObject();
SoftReference aSoftRef=new SoftReference(aRef);

弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
WeakReferencereference=new WeakReference(new People(“zhouqian”,20));
举例:
弱引用的生命周期不超过一个gc回收。在ThreadlocalMap中,其中的Key采用的就是弱引用实现,这种方式能防止内存泄漏。
在弱引用下,我们使用完ThreadLocal对象,它就会被回收,也就是说key值会为null;
但是value就不一样了,它是强引用,会与线程的声明周期一样,线程不停止就会一直存在,这样就可能造成OOM问题

虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new String(“hello”), queue);

如何判断对象可以被回收?
1 引用计数算法:
这算法原理很简单,就是当我们的对象没引用的时候,有一个计数器,比如count,就会count ++,同理,如果被应用的对象设为null,那么count --,直到 count = 0的为止。当JVM 进行回收的时候,那么他会找到这些count = 0 的对象,默认没有被使用,就进行回收了。
简单的来说,在JVM中的栈中,如果栈帧中指向了一个对象,那么堆中的引用计数器的值就会加1,当这个栈帧指向null时,对象的引用计数器就减1。
这个算法,理论上是挺好的,但是当对象相互引用的时候就无法进行了,举个例子:比如A 类,里面有个B类的引用 b,同时B类里面有个A类的引用a,当我们同时A a= new A(),B b = new B();这时候是一个强引用,都会count ++,当我们让里面的引用a.b =b,b.a=a;的时候就是第二次引用,count 都变成了2,如果这时让a =null,b =null,count – ,当我们回收的时候发现count = 1;JVM 就不会回收了,那么就会浪费内存,这也是引用计数算法的主要劣势,这里也顺便分析下这种算法:
1. 需要单独的字段存储计数器,增加了存储空间的开销;
2. 每次赋值都需要更新计数器,增加了时间开销;
3. 垃圾对象便于辨识,只要计数器为0,就可作为垃圾回收;
4. 及时回收垃圾,没有延迟性;
5. 不能解决循环引用的问题(当对象之间相互指向时,两个对象的引用计数器的值都会加1,而由于两个对象时相互指向,所以引用不会失效,这样JVM就无法回收。)
2 可达性分析算法
这种算法可以对象循环引用的问题,基本原理是:通过一个叫“GC ROOT”根对象作为起点,然后向下节点搜索,搜索路径叫引用链,也就是我们常说的引用,当我们从ROOT 找不到任何一条路径相连的对象的情况下,就可以判定可以回收了。
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

GC ROOT 回收对象的范围包括:
a. 虚拟机栈(栈帧中的本地变量表)中引用的对象
b. 方法区中类静态属性引用的对象
c. 方法区中常量引用的对象
d. 本地方法栈中JNI(native方法)引用的对象
e. JVM自身持有的对象,由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots
f. 正在被用于同步的各种锁对象
对象被回收前的挣扎:
虽然有可达性分析算法来判定对对象状态,但这并不是对象是否被回收的条件,对象回收的条件远远比这个复杂,比如无法通过ROOT找到的对象,也不一定会回收,会进入一个死缓的阶段,至少需要进行两次标记才会确定该对象是否被回收。
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法(是Object类的方法,该方法可将此对象与GC Roots建立联系)。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。如果没有建立联系,在finalize()方法中执行该对象必须要执行的代码(因为该对象马上就要被回收),例如释放资源,这与try finally中finally的作用是一样的。
当对象没有覆盖finalize()方法,或者finalize() 方法已经被虚拟机调用过,虚拟机都视为“没必要执行”。 JVM 对对象的finalize()只会被执行一次,这里仅仅做了解,不建议重写该方法,因为这样会干扰对象的回收调用机制,而且运行代价很高。
如果该对象被判定为有必要执行finalize(),那么对象会被放置在一个F-Queue 的队列中,并由一个优先级较低的Finalizer 线程去执行,这里的执行是 JVM 会触发这个方法,但并不保证等待他运行结束,因为finalize() 方法执行慢,或者死循环,会影响该队列其他元素执行。
执行finalize() 方法就会进行第二次标记,然后等待JVM 进行回收了,而在finalize() 方法执行的同时,可以对对象进行“拯救”,也就是说在执行方法内部,再次对对象进行引用,那么对象就复活了。

除了上述那些对象以外,还有一些废弃常量的回收,比如:有一个一个字符串"ABC" 已经进入常量池中,但是当前系统没有任何地方引用该对象,该常量就会被清除常量池。
同时还有一些废弃的类,或者无用的类也会被回收,这里一些判定条件有:
a.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
b.加载该类的ClassLoader 已经被回收
c.该类的java.lang.Class 对象在没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM的永久代中会发生垃圾回收么?
Minor GC不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

GC算法

有4种:
标记 -清除算法,
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。

复制算法,
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
适合于新生代,因为周期短。当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。
实践中会将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间 (from和to),每次使用Eden和其中一块Survivor。当回收时,将Eden和某一个Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

标记-整理算法,(压缩)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
适用于老年代。

标记整理算法与标记清除算法最显著的区别是:
标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存活对象进行处理,因此其不会产生内存碎片。标记整理算法的作用示意图如下:

分代收集算法,
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
一个线程OOM后,其他线程还能运行吗?
答案是还能运行.
java中OOM是分很多类型的;比如:堆溢出(“java.lang.OutOfMemoryError: Java heap space”)、永久带溢出(“java.lang.OutOfMemoryError:Permgen space”)、不能创建线程(“java.lang.OutOfMemoryError:Unable to create new native thread”)等很多种情况。
通过JVM堆空间的变化可以看到,当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行!
线程不像进程,一个进程中的线程之间是没有父子之分的,都是平级关系。即线程都是一样的, 退出了一个不会影响另外一个。
因此,答案是如果主线程抛异常退出了,子线程还能运行。
但是有一个例外情况,如果这些子线程都是守护线程,那么子线程会随着主线程结束而结束。

内存泄露与内存溢出

内存泄露:
内存申请后,用完没有释放,造成可用内存越来越少。
本意是申请的内存空间没有被正确释放,导致后续程序里这块内存被永远占用(不可达),而且指向这块内存空间的指针不再存在时,这块内存也就永远不可达了,内存空间就这么一点点被蚕食。
借用别人的比喻就是:比如有10张纸,本来一人一张,画完自己擦了还回去,别人可以继续画,现在有个坏蛋要了纸不擦不还,然后还跑了找不到人了,如此就只剩下9张纸给别人用了,这样的人多起来后,最后大家一张纸都没有了。
内存溢出:
用户实际的数据长度超过了申请的内存空间大小,导致覆盖了其他正常数据,容易造成程序异常,严重的,攻击者可以以此获取程序控制权。
是指存储的数据超出了指定空间的大小,这时数据就会越界,举例来说,常见的溢出,是指在栈空间里,分配了超过数组长度的数据,导致多出来的数据覆盖了栈空间其他位置的数据,这种情况发生时,可能会导致程序出现各种难排查的异常行为,或是被有心人利用,修改特定位置的变量数据达到溢出攻击的目的。
而Java中的内存溢出,一般指【OOM:发生位置】这种Error,它更像是一种内存空间不足时发生的错误,并且也不会导致溢出攻击这种问题,举例来说,堆里能存10个数,分了11个数进去,堆就溢出了1个数,JVM会检测、避免、报告这种问题,所以虽然实际上JVM规避了内存溢出带来的问题,但在概念上来说,它确实是溢出才导致的,只是Java程序员在看到这个问题时,脑袋里的反应会是“内存不够了,咋回事,是不是又是哪个大对象没释放”之类,而不是像C程序员“我被攻击了/程序咋写的搞溢出了”。同时对于Java来说,传统意义的溢出攻击也无法奏效,因为Java的数组会检查下标,对超出数组下标的赋值会报ArrayOutOfIndex错误。
而内存泄露的话,个人意见在Java里是不存在的,gc采用根搜索算法时,不可达的对象会被回收,gc是会搜索回收这些空间的,由于程序员个人问题,没用的对象不回收但可达,这种情况能不能界定为内存泄露。
原因不外乎有两点:
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
最常见的OOM情况有以下三种:
 java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
 java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
 java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
引起内存溢出的原因有很多种,常见的有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小
内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。
3.检查是否有大循环重复产生新对象实体。
4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况

如何设置JVM参数?
1.在eclipse设置JVM参数
打开eclipse-窗口-首选项-Java-已安装的JRE(对在当前开发环境中运行的java程序皆生效,也就是在eclipse中运行的java程序)编辑当前使用的JRE,在缺省VM参数中输入:
-Xmx1024m -Xms1024m -Xmn256m -Xss16m
或者在运行一个java程序的时候执行:
java -Xmx1024m -Xms1024m -Xmn256m -Xss16m Test
Test是一个class文件。
2. 在Tomcat服务器上设置JVM参数
set CATALINA_OPTS=-Xmx512m -Xms512m -Xmn64m -Xss2m 或者
set JAVA_OPTS=-Xmx512m -Xms512m -Xmn64m -Xss2m
设置CATALINA_OPTS 和 JAVA_OPTS都是一个道理,在启动tomcat的时候设置参数。
两者区别是JAVA_OPTS在tomcat停止的时候也会执行这个命令。

-Xms设置堆内存的最小空间大小。
-Xmx设置堆的最大空间大小。
-Xmn: 设置新生代大小
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
-XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两
个。如: 3,表示 Eden: Survivor=3: 2,一个 Survivor 区占整个年轻代的 1/5
-Xss设置每个线程的堆栈大小。
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。
收集器设置
-XX:+UseSerialGC: 设置串行收集器
-XX:+UseParallelGC: 设置并行收集器
-XX:+UseParalledlOldGC: 设置并行年老代收集器
-XX:+UseConcMarkSweepGC: 设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数。
-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间
-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。
-XX:ParallelGCThreads=n: 设置并发收集器年轻代收集方式为并行收集时,使用的 CPU
数。并行收集线程数。
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。老年代空间大小=堆空间大小-年轻代大空间大小
典型配置:
java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

GC垃圾回收器:7

【新生代收集器 3:】
1.Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。它是采用复制算法的新生代收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。
2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。
3.Parallel Scavenge(并行回收)收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。采用多线程。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
【老年代收集器3:】
4.Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
5.Parallel Old 收集器
Parallel Old 是Parallel Scavenge收集器的老年代版本,
使用多线程和“标记-整理/压缩”算法。
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。
“标记-清除”算法。
1)初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 有两部分:
标记老年代中所有的GC Roots对象,如下图节点1;
标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;

2)并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。与用户线程同时运行。从“初始标记”阶段标记的对象开始找出所有存活的对象。

3)预清理阶段:前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;

4)重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”。
5)并发清除(CMS concurrent sweep)
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

【横跨整个堆内存,通用】:
7. G1收集器
标记-整理+复制算法
G1收集器的优势:
(1)并行与并发
(2)分代收集
(3)空间整理 (标记整理算法,复制算法)
(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)
在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的灰机效率

默认的垃圾回收器是Parallel Scavenge + Parallel Old

常用的垃圾回收期有ParNew,CMS,G1三种垃圾回收器
Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象
工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然后让他们垃圾回收,这个现在一般写后台java系统几乎不用
ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。下周会着重分析这两个垃圾回收器
G1垃圾回收器:统一收集新生代和老年代,采用了更加优秀的算法和设计机制

查看JAVA内存的工具:

jps
jps主要用来输出JVM中运行的进程状态信息。
jstack
生成线程快照
可以用来查看Java进程内的线程堆栈信息。

-l long listings,会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况
在线上问题排查线程锁信息时,jstack是个非常好用的工具,结合应用日志可以迅速定位到问题线程。
jstat
命令可以查看堆内存各部分的使用量,以及加载类的数量。
用于持续观察虚拟机内存中各个分区的使用率以及GC的统计数据。

上述各个列的含义:
S0C、S1C、S0U、S1U:young代的Survivor 0/1区容量(Capacity)和使用量(Used)。0是FromSurvivor,1是ToSurvivor。
EC、EU:Eden区容量和使用量
OC、OU:年老代容量和使用量
MC、MU:元数据区(Metaspace)已经committed的内存空间和使用量
CCSC、CCSU:压缩Class(Compressed class space)committed的内存空间和使用量。
YGC、YGT:young代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时
GCT:GC总耗时
可以通过分区占用量上看到,在第2-3秒之间发生了一次YGC。YGC次数+1,并且Survivor from区的内存空间从1233.7->0,Survivor from从0->1536。Eden区也释放了很多内存空间。其他变化的空间占用也有元数据区以及元数据区的压缩Class区。Compressed class space也是元数据区的一部分,默认是1G,也可以关闭。
jmap
生成打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。
jmap可以用来查看堆内存的使用详情。内存各个分区可以通过jmap -heap pid来查看。

jmap -histo:live 6 | more
查看堆内存中的对象的数目,占用内存(单位是byte),如果带上live则只统计活对象

以上示例的排序是按照占用内存字节数倒序的。class name列中”[C,[B,[I “是代表char,byte,int.”[L+类名”代表其他实例。这种写法跟Class文件的Java的类型表述含义是一致的。
jhat
分析java堆的命令,可以将堆中的对象以html的形式显示出来
jconsole
堆内存使用量、线程数、类加载数和CPU占用率;内存选项可以查看堆中各个区域的内存使用量和左下角的详细描述(内存大小、GC情况等);线程选项可以查看当前JVM加载的线程,查看每个线程的堆栈信息,还可以检测死锁;VM概要描述了虚拟机的各种详细参数。

jvisualvm
jvm的详细参数和程序启动参数;监视展示的和jconsole的概览界面差不多(CPU、堆/方法区、类加载、线程);线程和jconsole的线程界面差不多;抽样器可以展示当前占用内存的类的排行榜及其实例的个数;Visual GC可以更丰富地展示当前各个区域的内存占用大小及历史信息

JVM调优:

原则:

  1. MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。
  2. GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。
  3. GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。

JVM性能调优方法和步骤:
1.监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
• 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
• FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
• 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放
• 之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
2.生成堆的dump文件
通过JMX的MBean生成当前的堆(Heap)信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
3.分析dump文件
打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件: Visual VM、IBM HeapAnalyzer、JDK 自带的Hprof工具、Mat(Eclipse专门的静态内存分析工具)推荐使用。
   注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。
4.分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
   注:如果满足下面的指标,则一般不需要进行GC:
• Minor GC执行时间不到50ms;
• Minor GC执行不频繁,约10秒一次;
• Full GC执行时间不到1s;
• Full GC执行频率不算频繁,不低于10分钟1次;
5.调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。
6.不断分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

JVM调优参数参考

  1. 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
  2. 年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
  3. 年轻代和年老代设置多大才算合理
    • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的耗时;小的年老代会导致更频繁的Full GC
    • 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
       如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
       在抉择时应该根据以下两点:
       (1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
    (2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
  4. 在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法:
    -XX:+UseParallelOldGC 。
  5. 线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。
       理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

你可能感兴趣的:(java)