JDK (Java Development Kit) : java语言的软件开发包。包括Java运行时环境JRE。
JRE (Java Runtime Environment) :Java运行时环境,包括JVM。
JVM (Java Virtual Machine) :一种用于计算机设备的规范。
Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH) 来加载 Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。
ClassLoader 顾名思义就是类加载器。Java源代码首先被jvm编译成.class文件。类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法去这个类的各种数据的访问入口;
验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟自身的安全;
准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
解析:解析阶段是虚拟机将常量池内的符号(Class文件内的符号)引用替换为直接引用(指针)的过程。
初始化:初始化阶段是类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码)。
父类静态域——》子类静态域——》父类成员初始化——》父类构造块——》父类构造方法——》子类成员初始化——》子类构造块——》子类构造方法;
类加载检查:虚拟机遇到一条 new 指令时,首先检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
1.如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行;
2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制;
避免类的重复加载,保护程序安全,防止核心API被随意篡改。
JNDI(Java Naming and Directory Interface,Java命名和目录接口)便是最典型的例子。JND需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码那该怎么办?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的 setContextClassLoaser()方法进行设置。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
例如JDBC在rt里面定义了这个SPI,那MySQL有MySQL的JDBC实现,Oracle有Oracle的JDBC实现,反正我java不管你内部如何实现的,反正你们都得统一按我这个来,这样我们java开发者才能容易的调用数据库操作。所以因为这样那就不得不违反这个约束啊,Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。因为它的手只能摸到<JAVA_HOME>\lib中,其他的它无能为力,这就违反了自下而上的委托机制了。
不可以。因为在类加载中,会根据双亲委派机制去寻找当前java.lang.String是否已被加载。由于启动类加载器已在启动时候加载了所以不会再次加载,因此使用的String是已在java核心类库加载过的String,而不是新定义的String。
Java源程序.java通过编译器编译成字节码.class文件,也就是计算机可以识别的二进制文件。
根据 Java 虚拟机规范的规定,class 文件格式采用一种类似于 C 语言的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基础数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据结构,所有表都习惯性地以 _info 结尾。表用于描述有层次关系的复合结构的数据,整个 class 文件本质上就是一张表。
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法,finalize()只会被调用一次。finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
1)类的完整类名必须一致,包括包名。
2)加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
主动使用,分为七种情况:
1)创建类的实例
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法I
4)反射(比如:Class.forName(“com.atguigu.Test”))
5)初始化一个类的子类
6)Java虚拟机启动时被标明为启动类的类
7)JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
浅拷贝:只是增加了一个指针指向已存在的内存地址,
深拷贝:是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
java -XX:+TraceClassLoading 具体类
Java -verbose 具体类
JDK1.7相对于JDK1.6,主要的变化就是将永久代中的字符串常量池移到堆内存中,交由堆管理。我们知道堆是JVM内存管理的主要区域,那么将字符串常量池放到堆内存中更方便高效的对字符串常量进行管理和垃圾回收。
而JDK1.8相对于JDK1.7来说,主要区别有两点,一是将虚拟机栈和本地方法栈合二为一了,二是移除永久代,增加了元数据区,元数据区使用本地内存,只受计算机内存大小的限制。而永久代使用的还是堆内存空间,受堆内存大小的限制。
记录正在执行的虚拟机字节码指令的地址。为了线程切换后能恢复到正确的位置,每个线程都需要一个独立的程序计数器,各个线程之间互不影响,独立存储,这也就是所谓的“线程私有区域”。
描述方法执行的内存模型,每个方法在执行时都会创建一个栈帧,每个栈帧存放的是局部变量表,操作数栈,动态链接,方法出口等信息,方法被调用到执行完成对应的是一个栈帧从入栈到出栈的过程。是线程私有的。
为虚拟机使用到的Native方法服务。注在HotSpot虚拟机中直接就把本地方法栈和虚拟机栈合二为一了。
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在jdk1.8中已经去除了永久代,改用只受计算机本地内存大小限制的元空间来实现方法区,元空间参数(-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1024M)。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
1. 由于PermGen内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。
2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
i++:刚开始学java时候,i++先使用i,然后再自加1,为什么是这样呢?
底层步骤:
1.从局部变量表取出 i 并压入操作数栈。(入栈)
2.然后对局部变量表中的i自增1,将操作栈栈顶值取出使用。(自加1,出栈)
3.最后使用操作数栈的栈顶值更新局部变量表,如此线程从操作栈读到的是自增之前的值。(更新)
++i:刚开始学java时候,++i先自加1然后再使用i,为什么是这样呢?
----------------------------------------------------------
底层步骤:
1.先对局部变量表的 i 自增 1。(自加1)
2.然后取出并压入操作数栈。(入栈)
3.再将操作栈栈顶值取出使用。(出栈)
4.最后使用栈顶值更新局部变量表,线程从操作栈读到的是自增之后的值。(更新)
--------------------------------------------
之前之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为可能 i 被从局部变量表取出,压入操作数栈,操作数栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。
如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError。
堆内存存储对象实例。我们只要不断地创建对象。并保证gc roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。就会在对象数量到达最大。堆容量限制后,产生内存溢出异常。
public class Cat {
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new Cat());
}
}
}
1.栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
2.当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
3.调整参数-xss去调整jvm栈的大小;
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
运行时数据区 | 是否存在 | 是否存在 |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是 | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是 | 是 |
Java GC(Garbage Collection)垃圾回收机制,是Java与C++/C的主要区别。JVM通过GC来回收堆和方法区中的内存,这个过程是自动执行的。因此作为Java开发者,不需要专门去编写内存回收和垃圾清理代码,也不需要处理内存泄露和溢出的问题。虽然java不需要开发人员显示的分配和回收内存,这对开发人员确实降低了不少编程难度,但也可能带来一些副作用:
1.有可能不知不觉浪费了很多内存;
2.JVM花费过多时间来进行内存回收;
3.内存泄露;
Java GC机制主要完成3件事:确定哪些内存需要回收;确定什么时候需要执行GC;如何执行GC。
1.引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
2.可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots的对象开始向下搜索,如果一个对象到 GCRoots 没有任何引用链相连时,则说明此对象不可用。
在 java 中可以作为 GC Roots 的对象有以下几种:
(1)虚拟机栈中引用的对象方法区类静态属性引用的对象方法区常量池引用的对象本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记
(2)如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。
(3)如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
GC按照回收区域又分为两大种类型:部分收集和整堆收集。
部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集。其中又分为:
1. 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。
2. 老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
3. 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。
1.在初始阶段,新创建的对象被分配到Eden区,S0和S1的两块空间都为空。
2.当Eden区满了的时候,Minor GC 被触发 。经过扫描与标记,不存活的对象被回收,存活的对象被复制到S0,并且存活的对象年龄都增大一岁。
3.当Eden区又满的时候,Minor GC再次被触发。此时Eden区 和 S0区存活的对象要复制到S1。需要注意的是,此时Eden区和S0区被清空,S0中的对象复制到S1后其年龄要加1。
4.当Eden区再次又满的时候,MinorGC则重复上面过程,将Eden区 和 S1区存活的对象复制到S0。此时Eden区和S1区被清空,S0中的对象复制到S1后其年龄要加1。
5.经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold默认是15),就会被从年轻代Promotion到老年代。
6.新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
7.老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
JDK1.2 以前,一个对象只有被引用和没有被引用两种状态。后来,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。
一共有 4 种:标记-清除算法、复制算法、标记整理算法、分代收集算法;
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
● 效率问题,标记和清除两个过程的效率都不高;
● 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。复制算法的执行过程如下图:
一般虚拟机都采用这种算法来回收新生代,因为新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块Survivor不可用),只有 10% 的内存会被“浪费”。当然如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
复制算法适合年轻代,不适合老年代。因为在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是复制算法需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
"分代收集"(Generational Collection)算法是根据对象存活周期的不同将内存划分为几块并采用不同的垃圾收集算法。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
进行垃圾收集时,必须暂停其他所有工作线程,Sun将这种事情叫做"Stop The World"。
如果新生代的垃圾收集器为Serial和ParNew,并且设置了-XX:PretenureSizeThreshold参数,当对象大于这个参数值时,会被认为是大对象,直接进入老年代。
Young GC后,如果对象太大无法进入Survivor区,则会通过分配担保机制进入老年代。
对象每在Survivor区中“熬过”一次Young GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,可以通过-XX:MaxTenuringThreshold设置),就将会被晋升到老年代中。
如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
在HotSpot虚拟机中,Eden区和Survivor区的默认比例为8:1:1,即-XX:SurvivorRatio=8,其中Survivor分为From Survivor和ToSurvivor,因此Eden此时占新生代空间的80%。
Minor GC是当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC(每次Minor GC会清理年轻代的内存)。因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
Major GC指发生在老年代的GC,对象从老年代消失时,我们说"Major Gc"或"Full GC"发生了。出现了MajorGc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程),也就是在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC,Major GC的速度一般会比MinorGc慢1e倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了;
Full GC是对年轻代和老年代都进行垃圾回收,Full GC 是开发或调优中尽量要避免的,这样暂时时间会短一些。
Minor GC触发条件: 当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Sp3ace区复制时,对象大小大于To Space可存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
最基本的垃圾收集器,使用复制算法,单线程,虽然收集垃圾时需要暂停其他所有的工作线程,但简单高效,是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC。
是 Serial 收集器的多线程版本 ,除了多线程进行GC外,其他与Serial一样,默认开启和 CPU 数目相同的线程数 。是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
1)可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代
2)这里的多线程指的是垃圾收集时,多线程并行,并不是垃圾收集与程序运行并行
3)收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
4)单CPU环境下,因为线程切换,性能较差。
关注程序的吞吐量,即吞吐量优先。主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
1)吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) ; 吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
2)收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
3)参数配置
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
-XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间),单位是毫秒。 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel进行控制。该参数使用需谨慎。
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。 取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略 。在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills,让虚拟机自己完成调优工作。
1)是Serial的老年代版本,收集垃圾时也需要暂停其他所有的工作线程。
2)是Client模式下默认的老年代垃圾收集器
3)Server模式下,搭配新生代的Parallel Scavenge 收集器使用(在 JDK1.5 之前版本中)。同时也作为老年代中使用 CMS 收集器的后备垃圾收集方案(当CMS发生Concurrent Mode Failure)。
1)Parallel Scavenge的老年代版本
2)吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
3)若相同对于吞吐量要求较高,可以Parallel Scavenge搭配Parallel Old使用。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程可以分为6个步骤,包括:初始标记、并发标记、预处理、重新标记、并发清除、重置。
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,但是CMS还远达不到完美的程度,它有以下3个明显的缺点:
(1)CMS收集器对CPU资源非常敏感。
(2)CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
(3)CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器。与其他GC收集器相比,G1具备如下特点:并行与并发、分代收集、空间整合、可预测的停顿。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
Mixed GC是G1垃圾收集器特有的收集方式,Mixed GC大致可划分为全局并发标记(global concurrent marking)和拷贝存活对象(evacuation)两个大部分:
global concurrent marking是基于SATB形式的并发标记,包括以下4个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)、最终标记(Final Marking)、清理(Clean Up)。Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去,然后回收原本的region的空间。
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 适用于单CPU环境下的Client模式 |
Paraller Old | 并行 | 老年代 | 标记-整理(压缩)算法 | 标记-整理(压缩)算法 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记-整理(压缩)算法 | 响应速度优先 | 响应速度优先 |
1)多数的Java应用不需要在服务器上进行GC优化;
2)多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3)在应用上线前,先考虑将JVM参数设置到最优;
4)减少对象创建的数量;
5)减少全局变量和大对象;
6)GC优化是最后不得已才使用的手段,在实际应用中,分析GC情况优化代码比优化GC参数要多得多;
通过看监控中的jvm是否有fgc,频繁fgc才需要优化(频繁fgc需要抓紧改配置)
1)JDK的命令行工具
Sun JDK监控和故障处理命令有jps、jstat、jmap、jhat、jstack、jinfo
jps(虚拟机进程状况工具):显示指定系统内所有的HotSpot虚拟机进程。 jstat(虚拟机统计信息监视工具):用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 jinfo(Java配置信息工具):jinfo的作用是实时地查看和调整虚拟机各项参数。
jmap(Java内存映像工具):dump堆到文件,可用于对文件的分析。
jhat(虚拟机堆转储快照分析工具):jhat命令与jmap搭配使用,来分析jmap生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。
jstack(Java堆栈跟踪工具):jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈 的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循 环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿 的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些 什么事情,或者等待着什么资源。
2)JConsole
Jconsole(Java Monitoring and Management Console)是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监测工具。jconsole使用jvm的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。
概览:包括堆内存使用情况、线程、类、CPU使用情况四项信息的曲线图。
3)VisualVM
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。
相比一些第三方工具,VisualVM有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。
Visual GC 是常常使用的一个功能,需要通过插件按照,可以明显的看到年轻代、老年代的内存变化,以及gc频率、gc的时间等!
触发 java.lang.OutOfMemoryError:最常见的原因就是应用程序需要的堆空间需要的是大的,但是 JVM 提供的却是小的,从而导致内存溢出。这个解决方法就是提供大的堆空间即可。
除此之外还有复杂的原因:内存泄露。特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space 错误。
1.确保有足够的堆空间来正常运行你的应用程序,在 JVM 的启动配置中增加如下配置:-Xmx1024m。
2.流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个 峰 值 , 并 且 这 个 峰 值 已 经 超 过 了 设 计 之 初 预 期 的 阈 值 , 那 么 以 前 正 常 的 功 能 将 会 停 止 , 并 触 发java.lang.OutOfMemoryError
3.Java heap space 异常解决方案,如果你的应用程序确实内存不足,增加堆内存会解决 GC overhead limit 问题,就如下面这样,给你的应用程序 1G 的堆内存:java -Xmx1024m com.yourcompany.YourClass。