Java相关内容:JVM

要想进阶成Java中级开发工程师,有一些东西总是绕不过去的,Java的知识体系里,像IO/NIO/AIO,多线程,JVM,网络编程,数据库,框架等必须有一定了解才行。最近在准备面试,所以对这些东西也做个记录。本篇记录的是JVM相关。

注:本篇博客写得有点沉闷,建议可以看看下面【老刘 码农翻身】写的这篇,很佩服能写成这么有意思的小故事。

我是一个Java class

1.JVM是什么?

要点:JVM概念/作用 类加载 运行时数据区 垃圾回收

JVM就是Java 虚拟机。主要功能就是将class字节码文件解释成机器码让计算机能识别运行。我们说Java程序是"一次编译,导出运行"的原因,就是只要在不同的操作系统上安装不同的虚拟机后,就能识别我们的class字节码文件,从而转换为不同平台的机器码来执行。

具体细节就是,当我们的程序运行时,JVM首先要通过类加载子系统(类加载器)加载所需要的类的字节码,class文件被jvm装载以后,最后由执行引擎会配合一些本地方法最终完成class文件的执行。然后在执行的过程中,需要一段内存空间来存储数据,JVM可以对这段内存空间进行分配和释放,这段内存空间我们称为运行时数据区。

JVM主要就是包含了类加载子系统、执行引擎、运行时数据区这三个内容。还有垃圾回收器和本地方法接口等模块。

这个过程可以由下面这张图说明

2.JVM如何加载字节码文件?

要点: 类加载过程 类初始化场景 类加载器 双亲委派机制

类加载过程

之前说过,通过类加载子系统可以把我们的class字节码文件加载到JVM中。那类加载子系统又什么?

类加载子系统概念:在JAVA虚拟机中,存在着多个类装载器,称为类加载子系统。也就是说实际上是类装载器把字节码文件加载到JVM中的。我们先不说类加载器,先说类加载器加载类的过程。类的生命周期是从被加载到虚拟机内存中开始,到卸载出内存结束。对应的过程有 加载----验证----准备----解析-----初始化----使用-----卸载 共七个阶段。从加载到初始化都属于类加载的过程。下面说一下这五个过程的作用。 (验证+准备+解析 = 连接)

加载:通过一个类的全限定名查找此类的二进制字节流,并将这些静态数据转换成方法区中的运行时数据结构,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

运行时数据结构就是该二进制字节流代表的类的类信息、常量、静态变量、静态方法、即时编译器编译后的代码等

验证:验证Class文件的字节流中包含信息符合当前虚拟机要求,并且不会危害虚拟机自身安全。

用命令  javap -v class路径  可以打开class的内容。程序按照其中的顺序执行的。

准备:为类变量在方法区分配内存,和给类变量赋予初始值。

如原句是static int a = 5; 的话先给a赋值为0。注意只是类变量,即static修饰的变量,final static修饰的在编译的时候就会分配了,不包含实例对象(没有static修饰的),实例变量将会在对象实例化时随着对象一起分配在java堆中

解析:将常量池内类、字段、方法等的符号引用替换为直接引用的过程

符号引用以一组符号来描述所引用的目标,包括1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。目标不一定在内存中。直接引用是能够找到目标的指针或句柄,此时引用的目标必定已经在内存中

初始化:执行静态变量的初始化,包括静态变量的赋值和静态初始化块的执行。

初始化的场景有且只有以下5种情况:
①.当虚拟机启动,需要执行main()的主类,JVM首先初始化该类。

②.当初始化一个类时,父类没有初始化,则先初始化父类。

③.创建新对象(new)

④.调用类的静态成员和静态方法。

⑤.使用java.lang.reflect包的方法对类进行反射调用,如果该类没有初始化,则初始化。

类加载到JVM后,就会为其中的对象分配内存空间。一个对象需要多大的内存空间在类加载完成后就确定了。除了堆外。栈和TLAB也能分配空间存储对象。然后一般就是开始按main()方法执行我们的程序了。

类加载器

上面知道了类加载的过程,也知道是通过类加载器来进行类加载的。现在说一下类加载器。

JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

 启动(Bootstrap)类加载器:启动类装入器负责将jre/lib/rt.jar下一些核心类库加载到内存中。由C++实现,属于虚拟机实现的一部分,它并没有继承java.lang.ClassLoader类。开发者不可以直接使用。

 扩展(Extension)类加载器:扩展类加载器责将jre/lib/ext下的一些扩展性的类库加载到内存中。开发者可以直接使用。

 系统(System)类加载器:系统类加载器负责将系统类classpath路径下的类库加载到内存中。是程序默认类加载器。开发者可以直接使用,也叫 Application ClassLoader。

 自定义类加载器(custom class loader):除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

用 class对象的getClassLoader()可以得到类加载器。

自定义类加载器的方法:通过继承java.lang.ClassLoader,然后重写findClass()方法实现自定义的类加载器。

protected Class<?> findClass(String name) throws ClassNotFoundException {
	try {
		byte[] data = loadByte(name);   //name为class文件所在位置,读成一个byte[]
		return defineClass(name, data, 0, data.length);    //define类对象
	} catch (Exception e) {
		e.printStackTrace();
		throw new ClassNotFoundException();
	}
}

类加载双亲委派机制

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

这种机制能够让越基础的类越由顶层加载器加载,这样就能保证类的一致性, 防止内存中出现多份同样的字节码。

相同类名称的类如果由不同的类加载器加载,也认为是两个不同的类

3.运行时数据区

当把需要执行的类加载进JVM后,在程序执行的过程中,需要一段内存空间来存储数据,JVM可以对这段内存空间进行分配和释放,这段内存空间我们称为运行时数据区。JVM规范中运行时数据区分为五个区域: 程序计数器,虚拟机栈,本地方法栈,堆,方法区。下面说一下这五个区域的作用。

程序计数器:指向当前线程正在执行的字节码指令的地址(行号),保证在线程切换后也能继续从原来的位置执行下去。分支、循环、跳转、异常处理等基础功能也需要依赖这个计数器来完成

该区域中,JVM规范没有规定任何OutOfMemoryError情况。

虚拟机栈:存放当前线程运行的方法所需要的数据、指令、返回值和返回地址。基本单位是栈帧。

当线程调用一个方法时,jvm就会压入一个新的栈帧到这个线程的栈中。栈帧中包括一些局部变量表、操作数栈、动态链接方法、返回值、返回地址等信息。(通过 -Xss 参数设置)

如果方法的嵌套调用层次太多(如递归调用),栈的深度大于虚拟机所允许的深度,会产生StackOverflowError溢出异常。
如果java程序启动一个新线程时没有足够的空间分配,即扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常。

本地方法栈:与虚拟机栈类似,但它是为虚拟机用到的本地方法服务。一般情况下,我们无需关心此区域。

虚拟机通过调用本地方法接口,实现了Java和其他语言的通信(主要是C&C++)

堆:由所有线程共享,用于存放实例对象。

堆内存就是JVM管理的内存中最大的一块,堆中分为新生代和老年代,是垃圾回收的主要区域。

堆内存超过限制时,抛出OutOfMemoryError异常。  (通过-Xms -Xmx参数设置)

方法区:存储已经被虚拟机加载的类信息、常量、静态变量、静态方法、运行时常量池、即时编译器编译后的代码等

类信息如类的完整有效名称,类的修饰符等等,方法区内存超过限制 OutOfMemoryError异常。 

4.分代、分配对象内存、垃圾回收

上面说的运行时数据区中,只是简单说了堆是用来存放实例对象的, 也说了堆是垃圾回收的主要内容。那么,堆中是怎么分代的呢?它又是怎么样给对象分配内存的呢?这些对象又是怎么样回收的呢? 且看下文。

分代

为什么要分代呢?因为我们的对象都是存在堆中的,堆的容量是有限的,所以要把堆中的一些没用的对象进行回收。那什么回收什么样的对象,什么时候回收多久回收,怎么高效地回收都是不小问题。JVM的开发人员就提出了分代的思想。分代后,把不同生命周期的对象放在不同代上,不同代上采用适合这个代的回收算法,这样就能比较高效地回收资源、控制内存了。

下面左边这块散发着绿光的就是线程共享的堆了。堆中分为年轻代和老年代。

其中年轻代中又分为Eden区和两个大小相等的Survivor区。Java 中的大部分对象都是朝生夕灭,所以大部分新创建的对象都会被分配到Eden区,当Eden区当在Eden申请空间失败时,就会出发Minor GC,然后将Eden和其中一个Survivor区上还存活的对象复制到另外一个Survivor区中,然后一次性清除Eden代和这个Survivor区,也就是说两个Survivor中总有一个是空的。

然后Survivor 区中的对象每经过一次 Minor GC年龄就会 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁),这些对象就会成为老年代。或者Survivor空间中相同年龄的对象大小总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象就可以进入老年代。然后一些比较大的对象则是直接进入到老年代。但是老年代的内存也不是无限的啊,万一从年轻代过来的对象老年代也装不下呢,所以JVM中还有一个空间分配担保,就是在发生Minor GC之前,虚拟机会检查老年代中的最大的可用连续内存空间如果大于之前每次晋升老年代对象的平均大小或者新生代中存活对象的总大小,则Minor GC是安全的,老年代还装得下,触发一次Minor GC就行;否则说明老年代也装不下了,这时候就会进行一次Full GC。

另外,HotSpot虚拟机中除了堆中的年轻代和老年代外,JDK1.8前还有一个永久代,在1.7之前在的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收,Java8元空间(metaSpaces)取代了永久代。元空间并不在虚拟机中,而是使用本地内存。元空间优势:不存在永久代溢出的问题,并且不再需要调整和监控永久代的内存空间。提高了内存利用率,且更利于垃圾回收。但是

-Xms  -Xmx   最大堆大小,最大堆大小

-XX:NewSize  XX:MaxNewSize  设置年轻代大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

-XX:NewRatio=n  设置年老代和年轻代的比值,默认为2. 即 年轻代: 老年代 = 2:1。典型设置为4。

-XX:SurvivorRatio=n  年轻代中Eden区与两个Survivor区的比值,默认为 8:1:1

-XX:MaxTenuringThreshold  设置Survivor中可以进入老年代的对象年龄值,默认15

-XX:PretenureSizeThreshold  设置可以直接进入老年代的大小,默认3M

-XX:PermSize 和 -XX:MaxPermSize  1.8中永久代的已经失效

在元空间中:
-XX:MetaspaceSize,表示初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

分配对象内存

类加载完成后,会为其中的对象分配内存空间。一个对象需要多大的内存空间在类加载完成后就确定了。我们说堆是用来存放对象的。那除了堆之外,还有哪里能分配对象吗? 答案是有的。那就是栈上分配TLAB

栈上分配

JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。然后对象可以在函数调用结束后自行销毁,垃圾收集系统的压力将会小很多,从而提高系统性能。 

要做到栈上分配依赖两个技术:逃逸分析和标量替换。逃逸分析可以分析一个新对象的使用范围。就是你在方法中定义了一个变量后,它可能会作为调用参数传递到其他方法中(方法逃逸)。也可能被外部线程访问到(线程逃逸)。这时候这个对象就相当于逃逸了,如果这个变量没有逃逸的话,证明这个变量是线程私有的。这时候就允许将对象打散分配在栈上。比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配,即标量替换。

-XX:+DoEscapeAnalysis  启用逃逸分析 (默认打开)

-XX:+EliminateAllocations 开启标量替换 (默认打开)

TLAB

TLAB(Thread Local Allocation Buffer 线程本地分配缓冲区)指JVM在新生代Eden区中为每个线程开辟的私有区域。默认占用Eden Space的1%。因为TLAB是私有的,没有锁的开销,所以Java程序中很多不存在线程共享的小对象,通常是在TLAB上优先分配,这些小对象也适合被快速地回收。

所以给对象分配内存的过程如下:

1)编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是堆上分配则继续尝试。

2)尝试TLAB上能够直接分配对象则直接在TLAB上分配,如果现有的TLAB不足以存放当前对象则重新申请一个TLAB,并再次尝试存放当前对象。如果还放不下则继续尝试。

3)尝试对象能否直接进入老年代,如果能够进入则直接在老年代分配。否则再继续尝试。

4)最后选择是年轻代的Eden。(具体分配方式:指针碰撞 / 空闲列表)

堆是所有线程共享的,因此在堆上分配内存需要加锁,就多了锁的开销。无论是TLAB还是栈都是线程私有的,私有即避免了竞争(当然也可能产生额外的问题例如可见性问题),这是典型的用空间换效率的做法。

PS: 在堆上分配内存的方式或策略,有指针碰撞和空闲列表。
指针碰撞:为对象分配内存空间时,如果被内存空间是规整的,只要把空闲指针向空闲内存方向挪动即可。
空闲列表:为对象分配内存空间时,如果内存空间不是规整的,需要有一个“空闲列表”用于记录哪些内存是可用的,并从可用内存中分配足够大小的内存出来,并修改“空闲列表”;

另外补充一个内容,上面说的都是对象的分配,我们也说java是面向对象编程,那么这个对象又是怎样的呢?它有什么结构?我们怎么找到这个对象? 下面补充一些对象的内容。

对象结构: 对象头、实例数据、对齐填充

对象头:由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据:用来存储对象真正的有效信息,包括父类继承下来的和自己定义的。这是我们定义和关心的。

对齐填充:jvm要求对象起始地址必须是8字节的整数倍(8字节对齐)

对象分配过程:
   
1)当JVM遇到new指令时,首先JVM需要在运行时常量池查找这个类对应的类符号引用,并且检查这个符号代表的类是否被加载,解析和初始化过,如果没有找到就需要把这个类加载到运行时常量池。 (1.7的运行时常量池在方法区,1.8后在堆)
    2)根据类的相关信息,为这个要创建的对象分配一定大小的内存。
    3)JVM将对象的内存空间除对象头外初始化为0,这就是为什么JAVA代码中的全局变量可以不用初始化也可以使用的原因。此外,JVM还会为对象设置对象头。如设置对象所属的类,对象哈希码,锁标识状态,GC分代年龄等。
    经过上面几步,虚拟机认为一个对象已经创建完毕,但是从程序来看,对象还没有初始化,因此需要根据代码初始化各个变量。初始化变量后,一个可用的对象就创建好了。

对象定位方式:句柄池 / 直接指针

垃圾回收

垃圾回收,就包括两个内容,一个是垃圾,一个是回收,怎样才是垃圾? 垃圾怎样回收?

垃圾

JVM中,关于“垃圾”的判断,大部分对象根据"判断算法"判定,少部分对象根据"引用"来判定。

“判断算法” 判断垃圾:引用计数法可达性分析法。

      引用计数法就是给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;在任何时刻计数器的值为0的对象就是不可能使用的,也就是可被回收的对象。引用计数法比较简单、高效。但无法解决对象相互引用的问题。所以主流的JVM并没有选用这种算法,而是采用可达性分析法。

      可达性分析法是把内存中的每一个对象都看作一个节点,并且定义了一些对象作为根节点“GC Roots”。如果一个对象中有另一个对象的引用,那么就认为第一个对象有一条指向第二个对象的边。JVM会起一个线程从所有的GC Roots开始往下遍历,当遍历完之后如果发现有一些对象不可到达,那么就认为这些对象已经没有用了,需要被回收。

可达性分析法的关键就在于GC Roots的定义,在java中,可以作为GC Roots的对象有四种:
1)虚拟机栈中的引用的对象
2)类中static修饰的静态变量引用的对象 
3)类中static final修饰的常量引用的对象
4)本地方法栈中引用的对象
注:JDK1.7静态变量和常量池在方法区,1.8后移动至堆

“引用” 判断垃圾:强引用,软引用,弱引用,虚引用

      强引用:类似于“Object obj = new Object()”的引用,只要obj的生命周期没结束,或者没有显示地把obj指向为null,那么JVM就永远不会回收这种对象。

方法中的强引用,在方法运行完成后就退出方法栈,对应堆中对象会被回收
全局变量中的强引用,为其赋值为null且再没有调用过,才能帮助回收此对象

      软引用:JVM正常运行时,软引用和强引用没什么区别,但是当内存不够用时,濒临逸出的情况下,JVM的垃圾收集器就会把软引用的对象回收。在JDK中提供了SoftReference类来实现软引用。

软引用可用来实现内存敏感的高速缓存,例如网页缓存、图片缓存、浏览器的后退按钮,当按后退时,如果内存吃紧,这时候就对软引用的对象回收了,只要重新构建。如果内存还充足,那这个软引用还没有被回收,我们可以直接从这个软引用获取内容。(如  Object obj = new Object();  SoftReference<Object> softReference = new SoftReference<Object>(obj); )

      弱引用:弱引用的对象将会在下一次的gc被回收,不管JVM内存被占用多少。在JDK中使用WeakReference来实现弱引用。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。因为弱引用不会影响它的引用对象在垃圾回收判断中的判断。

      虚引用:虚引用是最脆弱的引用,我们没有办法通过一个虚引用来获得对象,即使在没有gc之前。虚引用必须和一个引用队列配合使用。在JDK中使用PhantomReference来实现虚引用。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队 列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

这里补充一个:
1) system.gc()并不是你调用就马上执行的, 而是根据虚拟机的各种算法来来计算出执行垃圾回收的时间,另外,程序自动结束时不会执行垃圾回收的。 
2) 对象被回收时,要经过两次标记,第一次标记,如果finalize()被重写,那么垃圾回收并不会去执行finalize,第二次标记,如果对象不能在finalize中成功拯救自己,那真的就要被回收了。

回收

说完垃圾判断,最后就到如何回收的内容了。包括垃圾回收的算法,垃圾回收的节点,具体的垃圾回收器和垃圾回收的其它内容

垃圾回收算法:标记 - 清除算法,复制算法,标记 - 整理算法,分代收集算法(大部分JVM的垃圾收集器使用)

      标记 - 清除算法:标记清除算法(Mark-Sweep)是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除被标记的对象。这种算法实现简单,运行高效,但清除之后会产生大量不连续的内存碎片。

      复制算法:复制算法(Copying)将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。复制算法实现简单,运行高效,不会产生碎片。
但是每次相当于只能使用内存的一半,不能充分使用内存。

      标记 - 整理算法:标记整理算法(Mark-Compact)标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。这种算法能充分利用内存且不会产生内存碎片。但在标记-清除的基础上还需进行对象的移动,成本相对较高。

      分代收集算法:分代收集算法是目前大部分JVM的垃圾收集器采用的算法,根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为新生代和老年代。新生代中对象的存活率低,存活期会相对会比较短一些,选用复制算法来进行内存回收。老生代中,对象的存活率比较高,并且相对存活期比较长一些,一般使用的是标记 - 整理算法

注:新生代的空间并不是按照1:1的比例来划分,而是分为一块较大的Eden空间和两块较小的Survivor空间(8:1:1)

垃圾回收的节点:
 
1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。
  2)如果Eden区内存已满,会进行一次minor gc
  3)如果老年代内存不足,会进行一次full gc
  4)1.7 持久代(Perm)被写满,也可能导致full gc
        1.8 达到元空间(MataSpace)初始空间大小,也会触发full gc进行类型卸载
  5)手动调用System.gc(); 此时也有可能调用full gc

垃圾回收器

有 Serial 、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1 七种垃圾收集器。可见Serial 、ParNew、Parallel Scavenge是属于新生代收集器;Serial Old、Parallel Old、CMS属于老年代收集器。G1在整堆

上图连线的收集器可搭配使用
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Serial Old(老年代)
jdk1.9 默认垃圾收集器G1 (未来的趋势)

Serial收集器:Serial是最基本也是发展最悠久的收集器。它作用于年代代,是一种单线程垃圾收集器,采用复制算法,这就意味着在其进行垃圾收集的时候需要暂停其他的线程,也就是之前提到的”Stop the world“。

优点:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的收集效率。此外,到目前为止,Serial收集器依然是Client模式下的默认的新生代垃圾收集器。

ParNew收集器:可以把这个收集器理解为Serial收集器的多线程版本。即年轻代+多线程+复制算法+stop-the-world。一般用ParNew配合老年代的CMS收集器使用。

-XX:+UseParNewGC 表示要强制使用parNew收集器在新生代回收空间
-XX:+UseConcMarkSweepGC  设置老年代使用CMS。
-XX:+ParallelGCThreads  设置执行垃圾收集的线程数

Parallel Scavenge:ParallelScavenge收集器作用于新生代,它采用复制算法,是并行的多线程收集器,但是用户仍处于等待状态。目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。而高吞吐量为目标,就是减少垃圾收集时间,让用户代码获得更长的运行时间。主要适合在后台运算而不是太多交互的任务,比如需要与用户交互的程序,良好的响应速度能提升用户的体验。

-XX:MaxGCPauseMillis  设置每次年轻代垃圾回收的最长时间(最大暂停时间);
-XX:GCTimeRatio  设置垃圾回收时间占程序运行时间的百分比;
-XX:+UseAdaptiveSizePolicy  自动选择年轻代区大小和相应的Survivor区比例;

Serial Old:Serial Old是Serial的老年代版本,也是老年代的默认收集器,它是一个单线程的,使用标记整理算法,工作过程中也会stop-the-world。

 -XX:+UseParallelGC:JVM参数中默认选择年轻代垃圾收集器为并行收集器,而年老代仍旧使用串行Serial Old收集器。

Parallel Old:Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。(需要手动开启)

 -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0后支持的对年老代并行Parallel Old收集器

CMS:CMS收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。CMS收集器主要优点:并发收集,低停顿。但CMS有三个明显的缺点:(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大;(2)CMS收集器无法处理浮动垃圾 ;(3)CMS是基于“标记-清除”算法实现的收集器,会产生垃圾碎片。

-XX:CMSInitiatingOccupancyFraction : 1.6后CMS收集器的启动阀值已经提升至92%。即老年代使用92%内存后Full GC
-XX:+UseCMSCompactAtFullCollection:开关参数(默认开启的),用于在进行FullGC时开启内存碎片合并整理过程
-XX:CMSFullGCsBeforeCompaction:用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的 (默认值为0, 每次)

G1:G1 GC是Jdk7的新特性之一,且未来计划替代CMS。G1将堆空间划分成了大小相等互相独立的heap区块。每块区域既有可能属于Old区、也有可能是Young区。 G1在全局标记阶段(global marking phase)并发执行, 以确定堆内存中哪些对象是存活的。标记阶段完成后,G1就可以知道哪些heap区哪里垃圾最多。它会首先回收这些区,通常会得到大量的自由空间. 这也是为什么这种垃圾收集方法叫做Garbage-First(垃圾优先)的原因:第一时间处理垃圾最多的区块。

-XX:+UseG1GC  使用G1垃圾回收器。 使用场景:
1).服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
2).应用在运行过程中会产生大量内存碎片、需要经常压缩空间
3).想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

垃圾回收其它内容就暂时不细写了:

如何减少垃圾回收:尽量减少临时对象的使用,对象不用时最好显式置为Null ,使用StringBuffer,分散对象创建或删除的时间等

如何查看GC日志: jps jmap jstat 等命令

如何进行垃圾回收调优
      通过-Xns -Xnm合理控制堆的大小,通过-Xsn控制年轻代和老年代大小。
      通过NewRatio控制新生代老年代比例(默认是1:2)
      通过MaxTenuringThreshold控制进入老年前生存次数(默认是15次)

以4核8G内存的服务器为例,可对tomcat做以下配置:
 -Xms256m    #最小堆内存
 -Xmx2048m    #最大堆内存
 -Xss512k    #每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M。
 #对堆区的进一步细化分:新生代、中生代、老生代
 -XX:NewSize=256m    #表示新生代初始内存的大小
 -XX:MaxNewSize=512m    #表示新生代可被分配的内存的最大上限;当然这个值应该小于 -Xmx的值;
 -XX:PermSize=128m    表示非堆区初始内存分配大小,permanent size(持久化内存)
 -XX:MaxPermSize=256m    #表示对非堆区分配的内存的最大上限
 -XX:+UseBiasedLocking    #使用偏见的锁,使得锁更偏爱上次使用到它线程。在非竞争锁的场景下,即只有一个线程会锁定对象,可以实现近乎无锁的开销。

linux下查找java进程占用CPU过高原因

vim Demo.java
public class Demo {
    //测试啊
    public static void main(String[] args){
        new Thread(() -> {while (true) {}}).start();
    }
}


javac Demo.java
jar -cvf Demo.jar Demo.class
nohup java -cp Demo.jar Demo &

1) top 定位到java程序所在的进程,得到pid如18799

  2) top -H -p 18799查看具体哪个线程占用过高,得到tid如18809

3) jstack 18799| grep `printf %x 18809` -A 10

注:jstack pid可以列出java进程中的所有线程,但其中的线程id是16进制,所以printf %x 18809将线程id转为16进制(4979), -A 指找到结果后显示后续10行

"Thread-0" #8 prio=5 os_prio=0 tid=0x00007fbb60102800 nid=0x4979 runnable [0x00007fbb3dabb000]
   java.lang.Thread.State: RUNNABLE
	at Demo.lambda$main$0(Demo.java:4)  (可以看到此行)
	at Demo$$Lambda$1/135721597.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fbb600b6000 nid=0x4977 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fbb600b3000 nid=0x4976 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

剩下的就是分析原因和修改代码了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值