深入理解JAVA虚拟机学习笔记(一)

目录

一、 64位虚拟机

二、java内存区域和内存溢出异常

1. 运行时数据区域

1.1 程序计数器

1.2 Java虚拟机栈

1.3 本地方法栈

1.4 Java堆

1.5 方法区

1.6 直接内存

2. Hotspot虚拟机在Java堆中对象分配、布局和访问的全过程

2.1 对象的创建

2.2 对象的内存布局

2.3 对象的访问定位

2.4 常见VM Args参数

三、 垃圾收集器与内存分配策略

1 概述

2  如何确定对象已经死去

2.1 引用计数算法

2.2 可达性分析算法

2.3 再谈引用

2.4 生存还是死亡

2.5 回收方法区

3 垃圾收集算法

3.1 标记-清除算法

3.2 复制算法

3.3 标记-整理算法

3.4 分代收集算法

4 Hotspot的算法实现

4.1 枚举根节点

4.2 安全点

4.3 安全区域

5 垃圾收集器

5.1 Serial 收集器

5.2 ParNew收集器

5.3 Parallel Scavenge 收集器

5.4 Serial Old 收集器

5.5 Parallel Old 收集器

5.6 CMS 收集器

5.7 G1收集器

5.8 理解GC日志

5.9 垃圾收集器参数总结

6 内存分配和回收策略

6.1 对象优先在Eden区分配

6.2 大对象直接进入老年代

6.3 长期存活的对象将进入老年代

6.4 动态对象年龄判定

6.5 空间分配担保

四、虚拟机性能监控与故障处理工具

1. 概述

2 JDK命令行工具

2.1 jps:虚拟机进程状态工具

2.2 jstat:虚拟机统计信息监控工具

2.3 jinfo: Java配置信息工具

2.4 jmap:Java内存映像工具

2.5 jhat:虚拟机堆转储快照分析工具

2.6 jstack:Java堆栈跟踪工具

2.7 HSDIS:JIT生成代码反汇编

3 JDK的可视化工具

3.1 Jconsole:Java监视与管理控制台

3.2 VisualVM:多和一故障处理工具

五  调优案例分析和实战

1 概述

2 案例分析

2.1 高性能硬件上的程序部署策略

2.2 集群间同步导致的内存溢出

2.3 堆外内存导致的溢出错误

2.4 外部命令导致系统缓慢

2.5 服务器JVM进程崩溃

2.6 不恰当的数据结构导致内存占用过大

2.7 由Window虚拟内存导致的长时间停顿

3 实战: Eclipse运行速度调优

3.1 调优前的程序运行状态

3.2 升级JDK1.6的性能变化以及兼容问题

3.3 编译时间和类加载时间的优化

3.4 调整内存设置控制垃圾收集频率。

3.5 选择收集器降低延迟


一、 64位虚拟机

java程序运行在64位虚拟机上需要付出较大的额外代码

1. 内存问题, 由于指针和各种数据类型对齐补白的原因,通常要比32位系统额外增加10%~30%的内存消耗

2. 64位虚拟机的运行速度在各个测试项中几乎全面落后于32位虚拟机,两者大约有15%左右的性能差距

但是32位虚拟机只能支持最大4G内存

二、java内存区域和内存溢出异常

1. 运行时数据区域

java虚拟机在执行程序的过程中会把他所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的随虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将包括以下几个运行时数据区域,如下图

1.1 程序计数器

程序计数器是一块很小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成

由于java虚拟器的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此每个线程都需要有一个独立的程序计数器,各条线程之前计数器互不影响,独立存储。

如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的位置,如果执行的是native方法,这个计数器值则为空。

1.2 Java虚拟机栈

虚拟机描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口信息。

局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用

1.3 本地方法栈

本地方法栈和虚拟机栈很类似,虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到的Native方法服务。

1.4 Java堆

Java堆在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例

1.5 方法区

方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.5.1 运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,接口等描述信息外,还有一项常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池具备动态性,也即不一定只有在编译期产生,运行期间也可能产生,比较常用的就是String类的intern()方法。

1.6 直接内存

直接内存并不是虚拟机运行时数据区域的一部分,在JDK1.4中新加入了NIO(NEW INPUT/OUTPUT)类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,直接使用Native函数库分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据,直接内存的分配不受Java堆大小的限制,但是会受到本地总内存的限制的

2. Hotspot虚拟机在Java堆中对象分配、布局和访问的全过程

2.1 对象的创建

虚拟机在遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

在类加载通过后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可确认,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

  • 指针碰撞    假设Java堆中的内存是规整的,那么可以用指针作为分界点的指示器,分配内存也就是指针挪动了一段与对象大小相等的距离
  • 空闲列表    虚拟机内存维护一个列表记录那块内存区是可用的

选择那种分配方式由Java堆是否规整决定的,是否规整取决于所采用的垃圾收集器是否带有压缩整理功能,因此在使用Serial、ParNew、等等带Compact过程的收集器时系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

除了划分可用空间之外,还有一个问题就是创建对象是非常频繁的操作,由于是并发情况下是非线程安全的

  •  对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性
  • 把分配内存的动作按照线程划分在不同的空间之中处理,即每个线程在Java堆中预先分配一小块内存称为本地线程分配缓存(Thread Local Allocation Buffer, TLAB),那个线程需要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX+/-UseTLAB参数来设定

内存分配完成后,虚拟机将需要分配的内存空间都初始化为零值(不包括对象头),这一步操作保证对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值;

接下来虚拟机对对象进行必要的设置,如这个对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等等java对象头的相关信息;上面工作完成后从虚拟机角度来看,一个新的对象已经产生,但从Java程序的视角来看,对象创建才刚刚开始-<init>方法还没有执行,所有字段都还为零。执行完init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

2.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

2.2.1 HotSpot虚拟机的对象头包括两部分

  • 第一部分用户存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标识、线程持有的锁,偏向线程ID、偏向时间戳,这部分数据在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称他为“Mark Word”。例如在32位的HotSpot虚拟机中,如果对象处于锁定的状态下,他的Mark Word的32bit空间中的25bit用户存储对象哈希码,4bit用于存储对象的分代年龄,2bit用于存储锁标识位,1bit固定为0,而在其他状态(轻量级锁,重量级锁,GC标记,可偏向)下的对象存储内容表如下

HotSpot虚拟机对象头Mark Word

存储内容锁标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁
指向重量级锁的指针10碰撞(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳,对象分代年龄01可偏向
  • 第二部分为类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针即查找对象的元数据信息并不一定要经过对象本身,如果对象是一个java数组,那么在对象头中还必须有一块用于记录数据长度的数据,因为虚拟机可以通过普通java对象的元数据信息来确定java对象的大小,但是从数据的元数据中却无法确定数据组的大小。

2.2.2 实例数据

实例数据部分是对象真正存储的有效数据,也是程序代码中定义的各种类型的字段内容

2.2.3 对其填充数据

这部分数据不是必然存在的,没有特别的含义,仅仅是起到占位符的作用。由于HotSpot VM 的自动内存管理系统要求对象起始位置地址必须是8字节的整数倍,也即对象大小必须是8字节的整数倍,对象头部分正好是8字节的倍数,因此如果实例数据部分没有对齐时,就需要通过对其填充来补充

2.3 对象的访问定位

我们的java程序主要是通过栈上的reference数据来操作堆上的具体对象,目前主流的访问方式是使用句柄和直接指针两种

  • 句柄访问的话,java堆将会划分成一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含对象实例数据与类型数据各各自的具体地址信息
  • 使用直接指针直访问的话,那么堆对象的布局中就必须考虑如何访问类型数据的相关信息,而reference中存储的直接就是对象地址,对象的地址里直接包含到对象类型数据的指针和对象的实例数据。

这两种访问方式各有优势,句柄访问的最大好处就是reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据部分,而reference本身不需要修改;使用直接指针访问方式的最大好处是速度快,节省了一次指针定位的时间开销(对象访问在java中非常频繁)。虚拟机Sun HotSpot是使用第二种方式访问对象的,但从整个软件开发的范围来看,各种语言使用句柄来访问的情况也十分常见。

操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G(2……31)。

  • String.intern()是一个native方法,他的作用是如果字符串常量池中已经包含一个等于此字符串的对象,则直接返回代表池中这个字符串的string对象,否则将此string对象包含的字符串添加到常量池中,并且返回此string对象的引用。jdk1.6会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用;jdlk1.7不会再复制实例,只是在常量池中记录首次出现的实例引用。
  • 方法区用于存放Class的相关信息,如类型,访问修饰符,常量池,字段描述,方法描述等。JAVA SE API 的通过反射生成的动态代理类和CGlib直接操作字节码运行时生成的动态代理类都会占用大量的方法区内存,以及jsp也会动态生成大量的java类型。
  • 本机直接内存DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值一样。通过DirectByteBuffer分配内存抛出内存溢出异常,其实是没有真正向操作系统申请内存而是通过计算得知内存无法分配就手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。

 

2.4 常见VM Args参数

   -XX:+PrintCommandLineFlags  打印已经被用户或者当前虚拟机设置过的参数

常见VM Args参数

-Xms20m堆最小值
-Xmx20m堆最大值。 最小堆和最大堆不一样,当堆空间不足时会自动进行扩展
-Xmn10m年轻代大小
-XX:NewRatio=3年轻代与年老代的比值
-XX:SurvivorRatio=8Eden区与Survivor区的大小比值    默认8:1:1
-Xss128k每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
-XX:MetaspaceSize=128m初始元数据空间大小
-XX:MaxMetaspaceSize=128m最大元数据空间大小
-XX:+HeapDumpOnOutOfMemoryError 可以在虚拟机发生内存溢出异常时Dump出当前的内存堆转储快照以便时候进行分析
-XX:PerSize=20M方法区内存
-XX:MaxPerSize=20M方法区最大内存
-XX:MaxDirectMemorySize=10M直接内存最大容量
-Xnoclassgc关闭虚拟机对class的垃圾回收功能。
-verbose:class在程序运行的时候输出有多少类被加载
-verbose:gc在程序运行的时候输出gc信息
–verbose:jni输出native方法调用的相关情况,一般用于诊断jni调用错误信息
-XX:+TraceClassLoading可以跟踪显示类加载机制
-XX:+TraceClassUnloading可以跟踪显示类卸载机制
-XX:+PrintGC打印GC信息
-XX:+PrintGCDetails打印GC详细信息
-XX:+PrintGCTimeStamps打印GC耗时时间
-XX:PrintHeapAtGC打印GC前后的详细堆栈信息
-XX:+PrintGCApplicationConcurrentTime打印每次垃圾回收前,程序未中断的执行时间
-XX:+PrintGCApplicationStoppedTime打印垃圾回收期间程序暂停的时间
-Xloggc:gclog.log指定GC log的位置,以文件输出

 

三、 垃圾收集器与内存分配策略

1 概述

程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而死;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(运行期会由于JIT编译期优化存在差异)。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束是,内存自然就随着回收了。但是Java堆和方法区的内存分配和回收都是动态的,垃圾收集器需要关注这部分内存。

2  如何确定对象已经死去

2.1 引用计数算法

给对象中添加一个引用计数器,每当一个地方引用它时,计数器就加1,当引用时效时,计数器就减一;任何时候计数器为零的对象就是不可能再被使用的

2.2 可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始项下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则则证明此对象是不可用的

 

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

2.3 再谈引用

jdk1.2以前,java中引用的定义很传统,如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但是我们希望能描述这样一类对象:当内存空间还足够时,则保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张则可以抛弃这些对象。jdk1.2之后,java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但并非必需的对象,对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在jdk1.2之后,提供了SoftReference类来实现软引用
  • 弱引用也是用来描述非必需对象的,但是他的强度比软引用更弱一点,被弱引用关联的对象只能生成到下一次垃圾收集对象发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在jdk1.2之后,提供了WeakReference类来实现弱引用
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关系的唯一目的就是能在这个对象被收集器收集时收到一个通知,在jdk1.2之后,提供了PhantomReference类来实现虚引用

ThreadLocal其实就是针对每个线程存储的局部变量信息,内部存储结构Entry就是继承了WeakReference,因此使用时需要注意ThreadLocal的生命周期只能到下一次垃圾收集对象发生之前

2.4 生存还是死亡

即使是科大信封分析算法中不可达的对象,也并非是非死不可的,后面至少要经历两次标记过程;如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。对象没有覆盖finalize方法或者finalize方法已经被执行过则没必要执行直接就可以收回了。

当有必要执行finalize方法时,这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待运行结束,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize中成功拯救自己,那么第二次标记时他将会移除即将回收的集合。finalize发放只会被执行一次。

2.5 回收方法区

永久代的垃圾收集主要回收两部分:废弃常量和无用的类。废弃常量的判别很简单,即没有对此常量的引用而且有必要的话,虚拟机就会会首次常量

判断无用的类就稍显复杂下,如下

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

在大量使用反射、动态代理、CGlib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出

3 垃圾收集算法

3.1 标记-清除算法

标记清除算法(Mark-Sweep)分为标记和清除两个阶段;收下标记出所有需要回收的对象,在标记完成后统计回收所有被标记的对象,主要有两个不足点

  1. 效率问题,标记和清楚这两个过程的效率都不高
  2. 空间问题,标记清除后会产生大量的内存碎片,碎片太多会导致无法分配较大的对象

3.2 复制算法

为了解决效率问题,复制算法出现了,他将可用内存按容量划分为大小相等的两块,每次使用完其中一块,就将会存活的对象复制到另一块上,这样就不会产生内存碎片,代价就是内存缩小了。

现在的商业虚拟机都采用的这种算法来收集新生代,因为大部分对象都是朝生夕死的,不需要分为一半,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden区和其中的一块Survivor区,每次回收时将存活的对象辅助到另一个Survivor区,最后清理掉Eden区和Survivor区。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1。当Survivor区不够用时2,就需要依赖老年代进行分配担保

3.3 标记-整理算法

复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低,而且需要有担保机制,因此老年代一般不能直接选用这种算法。标记-整理算法中标记过程和标记清除一样但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向一端移动,然后直接清理掉段边界以外的内存。

3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块,一般为年轻代和老年代。年轻代用复制算法,老年代使用标记-清除算法或者标记-整理算法。

4 Hotspot的算法实现

4.1 枚举根节点

可作为GC Roots的节点主要有全局性的引用(常量或类静态属性)与执行上下文(栈帧中的本地变量表),这些数据很大,如果要逐个检查里面的引用很消耗时间;另一个可达性分析对执行时间的敏感还体现在GC停顿上,GC一定要确保在一个一致性的快照中,这就导致GC进行时必需停顿所有的Java执行线程(Stop The World),即使号称几乎不会发生停顿的CMS收集器中,枚举根节点时也必须要停顿的。

目前主流的虚拟机使用的都是精准式GC,虚拟机是有办法知道哪些地方存放着对象引用,而不需要全局搜索

4.2 安全点

  • 抢先式中断 GC时首先把所有线程中断,如果发现不在安全点上就恢复线程,让其跑到安全点上,目前几乎没有这个的实现
  • 主动式中断  GC时,不中断线程,只是设置个标识,各个线程主动去轮询这个标识,发现中断标识为真时就自己中断挂起

4.3 安全区域

安全区域是对安全点的扩展,主要是解决GC时有的线程已经处理阻塞或者睡眠状态等无法获取CPU时间的问题

5 垃圾收集器

垃圾收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现,如下就是HotSpot虚拟机的垃圾收集器

5.1 Serial 收集器

Serial收集器是一个单线程的收集器,垃圾收集时必须暂停其他所有的工作线程,直接他收集完成。虽然由于GC会导致一定的停顿时间,但他依然是虚拟机运行在Client模式下的默认新生代收集器,他也有着优于其他收集器的优点:简单而高效(与其他收集器的单线程比较),没有线程交互的开销。

5.2 ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集,其余行为包括Serial收集器可用的控制参数(例如 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。除了Serial收集器外,只有ParNew收集器可以和CMS收集器配合工作。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。他默认开启对的收集线程数与CPU的数量相同,可以使用-XX:ParrelGCThreads参数来限制垃圾收集的线程数

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处理等待状态
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行)

5.3 Parallel Scavenge 收集器

Parallel Scavenge收集器是一个新生代收集器,也是使用的复制算法的和并行的多线程收集器。但是他的关注点和其他收集器不同,一般是尽可能的缩短垃圾收集时用户线程的停顿时间,但是Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU运行用户代码的时间与CPU总消耗的时间,即吞吐量 = 运行用户代码时间 / (运行用户代码时间)+ GC垃圾收集时间)。因此该收集器不适合与用户交互的程序,适用于高效率利用CPU时间的后台运算服务

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的。吞吐量默认大小为99,即最大1%(1/(1+99)),由于与吞吐量有密切关系,也被称为吞吐量优先的收集器,该收集器还有一个参数-XX:+UseAdaptiveSizePolicy,打开后就不需要指定新生代大小已经比例,晋升老年代对象大小等细节问题了,他会自动动态的调节这些参数也达到了配置的吞吐量要求,也称为GC自适应的调节策略

5.4 Serial Old 收集器

Serial Old收集器是Serial收集器的老年代版本,单线程收集器,标记-整理算法,主要给Client模式的虚拟机使用,如果在Server模式下可以和Parallel Scavenge收集器搭配使用;还可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用的

5.5 Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程的标记-整理算法。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge 加Parallel Old收集器。

5.6 CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,已给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。

CMS收集器是基于标记-清除算法的,实现较复杂

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两部仍然需要Stop The World。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段进行GC Roots Tracing的过程,重新标记则是为了修正并发标记期间用户程序继续运行而导致变动的对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,由于整个过程耗时最长的并发标记和并发清除都可以和用户一起工作。

CMS的有点事并发收集,低延迟,但也有3个明显的缺点

  • CMS收集器堆CPU资源非常敏感,在并发阶段会占用一部分线程(或者说CPU资源)而导致应用变慢,总吞吐量降低。CMS启动的默认收回线程数(CPU数量 + 3) / 4。i-CMS收集器为了解决这个问题但效果甚微已废弃
  • CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。由于CMS并发清除阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉他们,只好留待下一次GC时再清理到,这一部分垃圾就成为浮动垃圾。因此CMS收集器需要预留一部分空间提供给并发收集时的程序运行使用。jdk1.5时CMS收集器当老年代使用了68%的空间时,就会激活;jdk1.6是92%,也可以通过-XX:CMSInitialingOccupancyFraction的值来设置阈值。如果CMS预留内存不够实用就会出现Concurrent Mode Failure失败,这是虚拟机将启动后备预案:临时启用Serial Old收集器进行老年代的垃圾收集
  • 产生垃圾碎片,CMS是使用标记-清除算法的,一旦碎片太多而无法给大对象分配内存时就会触发Full GC。-XX:+UseCMSCompactAtFullCollection (内存碎片的合并整理),-XX:CMSFullGCsBeforeCompaction (执行多少次不压缩的Full GC后跟着来一次压缩的,默认为0即每次进入Full GC时都进行碎片)。

5.7 G1收集器

G1是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予他的使命是(在比较长期的)未来可以替换掉JDK1.5中发布的CMS收集器。与其他收集器相比,G1具备如下特点

  • 并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-World停顿的时间,部分其他收集其原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中仍然保留。G1收集器不需要其他收集器配合就能独立管理整个GC堆
  • 空间整合:与CMS的标记-清理算法不同,G1从整体上来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于复制算法的,这两种算法都不会产生垃圾碎片
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,可以在指定的时间范围内对最优回收价值的Region进行垃圾回收。G1的堆内存布局和其他收集器有很大区别,他将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。G1不需要对整个Java堆进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的来由)

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之前的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)(找出GC Roots能直接关联到的对象)
  • 并发标记(Concurrent Marking)(从GC Roots开始对对中对象进行可达性分析找出存活的对象)
  • 最终标记(Final Marking)(修正并发标记过程中发生变化的对象)
  • 筛选回收(Live Data Counting And Evacuation)(筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划)

5.8 理解GC日志

[GC (Allocation Failure) [DefNew: 7822K->1024K(9216K), 0.0041441 secs] 7822K->4343K(19456K), 0.0041990 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2048K->0K(9216K), 0.0025372 secs][Tenured: 5367K->5367K(10240K), 0.1102513 secs] 5367K->5367K(19456K), [Metaspace: 3144K->3144K(1056768K)], 0.1128471 secs] [Times: user=0.00 sys=0.00, real=0.11 secs] 
[Full GC (Allocation Failure) [Tenured: 5367K->5350K(10240K), 0.0022073 secs] 5367K->5350K(19456K), [Metaspace: 3144K->3144K(1056768K)], 0.0022343 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 570K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   6% used [0x00000000fec00000, 0x00000000fec8e950, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5350K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  52% used [0x00000000ff600000, 0x00000000ffb399d8, 0x00000000ffb39a00, 0x0000000100000000)
 Metaspace       used 3179K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 336K, capacity 392K, committed 512K, reserved 1048576K

JVM在进行GC时,可能针对三个区域进行垃圾回收分别是新生代、老年代、方法区,大部分时候回收的都是新生代。GC类型主要有以下四种类型。
 

  • 新生代收集(Minor GC/Young GC):只针对新生代的垃圾收集。具体点的是Eden区满时触发GC。 Survivor满不会触发Minor GC 。
  • 老年代收集(Major GC/Old GC):只针对 老年代的垃圾收集。 目前,只有CMS收集器会有单独收集老年代的行为。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

GC日志开头的“[GC” 和”[Full GC“说明这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的,如果有”Full“,说明这次GC说发生了Stop-The-World的(一般是因为出现了分配担保失败之类的问题),如果是 调用System.gc()方法所触发的收集,那么在这里将显示”[Full GC(System)“。

接下来的DefNew,Tenured,Perm表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,DefNew表示Serial收集器(Default New Generation),ParNew表示ParNew收集器(Paralled New Generation),PSYoungGen表示Paralled Scavenge收集器,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的"7822K->1024K(9216K), 0.0041441 secs"含义是”GC前该内存区域已使用容量->GC后该区域已使用容量(该内存区域总容量),该内存区域GC所占用的时间,单位为秒“

紧接着后面的”7822K->4343K(19456K), 0.0041990 secs“含义是”GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)“

”Times: user=0.00 sys=0.02, real=0.00 secs“这三个字段和Linux下的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经历的墙钟时间。

5.9 垃圾收集器参数总结

垃圾收集器相关的常用参数
参数描述
UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
UseParNewGC打开此开关后,使用ParNew + Serial Old的收集器组合
UseConcMarkSweepGC打开此开关后,使用ParNew + CMS + Serial Old的收集器组合。Serial Old收集器作为Concurrent Mode Failure失败后的后备收集器使用
UseParallelGC虚拟机运行在Server模式下的默认值,打开此开关后,使用Paralled Scavenge + Serial Old(PS Mark Sweep)的收集器组合
UseParallelOldGC打开此开关后,使用Paralled Scavenge + Paralled Old的收集器组合
UseG1GC打开此开关后,使用G1收集器
SurvivorRatio新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小,设置这个参数后,大于参数的对象直接在老年代分配
MaxTenuringThreshold晋升到老年代的对象年龄,每个对象再坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy动态调整Java堆中各个区域的大小以及进入老年代的年龄
ParallelGCThreads设置并行GC时进行内存回收的线程数
GCTimeRatioGC时间占总时间的比率,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillis设置GC最大停顿时间,仅在使用Parallel Scavenge收集器时生效
CMSIntiatingOccupanyFraction设置CMS收集器在老年代空间被使用多少后出发垃圾回收,默认值68%,1.7后92%
UseCMSCompactAtFullCollection设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在CMS收集器有效
CMSFullGCsBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再启用碎片整理,,仅在CMS收集器有效

 

查看当前jvm适用哪种垃圾收集器

java -XX:+PrintCommandLineFlags -version

6 内存分配和回收策略

Java体系的内存自动管理其实就是给对象分配内存和回收分配给对象的内存。

6.1 对象优先在Eden区分配

大多数情况下,对象是在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC。将Eden区存活的对象复制到Survivor1区,如果Survivor1区内存不够,直接复制到老年代内存区域。

6.2 大对象直接进入老年代

通过设置-XX:PretenureSizeThreshold参数可以让大对象直接在老年代进行内存分配

6.3 长期存活的对象将进入老年代

虚拟机给每个对象定义一个对象年龄计数器,如果兑现在Eden区生成并经过第一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将移动到Survivor区并且年龄设为1,每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认为115岁),就将晋升到老年代。老年代阈值可以通过-XX:MaxTenuringThreshold设置。

6.4 动态对象年龄判定

虚拟机并不永远的要求对象的年龄必须达到指定的阈值才能晋升到老年代,如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需达到阈值的要求。

6.5 空间分配担保

jdk1.6只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC。

四、虚拟机性能监控与故障处理工具

1. 概述

给系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javvacore文件)、堆转储快照(heapdump/hprof文件)等。经常使用适当的虚拟机监控和分析的工具可以加快我们分析数据、定位解决问题的速度。

2 JDK命令行工具

Sun JDK监控和故障处理工具
名称主要作用
jpsJVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstatJVM Statictics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfoConfiguration Info For Java,显示虚拟机配置信息
jmapMemory Map For Java,生成虚拟机的内存转储快照(Heapdump文件)
jhatJVM Heap Dump Browser,用于分析heapdump文件,他会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstackStack Trace For Java,显示虚拟机线程快照

2.1 jps:虚拟机进程状态工具

jps命令格式

jps [options] [hostid]

jps工具主要选项
选项作用
-q只输出LVMID,省略主类的名称;对于本地虚拟机进程来说,LVMID与操作系统的进程ID是一致的
-m输出虚拟机进程启动时传递给主类main()函数的参数
-l输出主类的全名,如果进程执行的是Jar包,输出Jar路径
-v输出虚拟机进程启动时JVM参数

 

2.2 jstat:虚拟机统计信息监控工具

jstat是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

jstat [option vmid ]  [interval [s/ms] [count] ]

参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。例如 jstat -gc 2765 250 20

选项option代表用户希望查询的虚拟机信息,主要分为3类:类加载、垃圾收集、运行期编译状况。

jstat工具主要选项
选项作用
-class监视类装载、卸载数量、总空间以及类装载所耗费的时间
-gc监控Java堆状况,包含Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间等信息
-gccapacity监视内容和-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcutil监控内容和-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause与-gcutil功能一样,但是会额外输出导致上一次gc产生的原因
-gcnew监视新生代的GC状况
-gcnewcapacity监视内容和-gcnew基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcold监视老年代GC状况
-gcoldcapacity监视内容和-gcold本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcpermcapacity输出永久代使用到的最大、最小空间
-compiler输出JIT编译期编译过的方法、耗时等信息
-printcompilation输出已被JIT编译的方法

2.3 jinfo: Java配置信息工具

jinfo的作用是实时的查看和调整虚拟机各项参数。-sysprops 选项可以把虚拟机进程的System.getProperties()的内容全部打印出来;使用-flag[+/-] name或者-flag name=value 修改一部分运行期可写的虚拟机参数。

jinfo [option] pid

2.4 jmap:Java内存映像工具

jmap命令用于生成堆转储快照。可以-HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件;通过-XX:HeapDumpOnCtrlBreak参数则可以使用[Ctrl] + [Break]键让虚拟机生成dump文件,又或者在linux系统下Kill -3命令发送进程退出信息“吓唬 ”一下虚拟机,也能拿到dump文件。

jmap的作用不仅仅是为了获取dump文件,他还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是那种收集器等。

jmap工具主要选项
选项作用
-dump生成Java堆转储快照。格式为-dump:[live,]format=b,file=[filename],其中live子参数说明是否只dump出存活的对象
-finalizerinfo显示在F-Queue中等待Finalizr线程执行finalize方法的对象,只在Linux/Solaris平台下有效
-heap显示Java堆详细信息,如使用哪种回收期、参数配置、分代状况等。只在Linux/Solaris平台下有效
-histo显示堆中统计信息,包括类、实例数量、合计容量
-permstat以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效
-F当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效
  

 

C:\Users>jmap -heap 15760

2.5 jhat:虚拟机堆转储快照分析工具

jhat命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果可以在浏览器中查看。不过一般不使用jhat直接进行分析,一来分析工作比较耗时且消耗硬件资源,由于加载dump快照文件需要比生成dump文件更大的内存,所以一般64位JDK大内存的服务器上进行;二来jhat功能比较简陋。

2.6 jstack:Java堆栈跟踪工具

jstack命令用于生成虚拟器当前时刻的线程快照。可以用来定位线程长时间停顿如线程间死锁、死循环、请求外部资源导致的长时间等待等等。

jstack工具主要选项
选项作用
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除堆栈外,显示关于锁的附加信息
-m如果调用到本地方法的话,可以显示C/C++的堆栈

 

2.7 HSDIS:JIT生成代码反汇编

3 JDK的可视化工具

  • 启动JConsole
  • 内存监控
  • 线程监控

3.1 Jconsole:Java监视与管理控制台

3.2 VisualVM:多和一故障处理工具

  • VisualVM兼容范围与插件安装
  • 生成、浏览堆转储快照
  • 分析程序性能
  • BTrace动态日志跟踪

五  调优案例分析和实战

1 概述

2 案例分析

2.1 高性能硬件上的程序部署策略

将Java堆扩大至12G,网站经常不定期出现长时间失去响应,导致原因GC停顿导致的,一次Full GC的停顿时间高达14秒

2.2 集群间同步导致的内存溢出

集群间需要信息同步,当网络情况不能满足传输要求时,同步消息在内存中不断堆积,很快产生了内存溢出

2.3 堆外内存导致的溢出错误

垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是不能像年轻代、老年代那样,空间不足就通知收集器进行回收,他只能等待老年代满了后Full GC,然后顺带对堆外内存进行垃圾收集

除了堆和永久代之外,还有以下几个区域占用内存需要注意

  • Direct Memory
  • 线程堆栈
  • Socket 缓存区:每个Socket链接都有Receive和Send两个缓存区,分别占用37KB和25KB
  • JNI代码:JNI使用的内存也不再堆中
  • 虚拟机和GC:虚拟机、GC的代码执行也需要消耗一定的内存

2.4 外部命令导致系统缓慢

Runtime.getRuntime.exec();

该方法即通过Java代码执行shell脚本,但是他在Java虚拟机中是非常耗费资源的操作,即使外部命令本身执行很快,频繁调用时创建进程的开销也非常乐观。执行该命令的过程:首先克隆一个和当前虚拟机拥有相同环境变量的进程,再用这个新进程去执行命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重

2.5 服务器JVM进程崩溃

异步请求第三方服务导致的,由于第三方服务消费速度比较慢,时间越长堆积了越来越多Web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终超出了虚拟机的承受能力后使得虚拟机进程崩溃。

2.6 不恰当的数据结构导致内存占用过大

HashMap占用的内存远远大于数据本身的内存大小。Map<Long,Long>数据结构中只有Key和Value存放的数据才是有效数据,共16b(2*8B)。这两个长整型数据包装成Long对象之后就分别具有8B的MarkWord、8b的Klass指针,再加8B存储数据的long值。在这两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int型的hash字段,为了对齐,还必须添加4B的空白填充,最后还有HashMap中对这个Entry的8B的引用,这样增加两个长整型数字,实际耗费的内存为(Long(24B)* 2) + Entry(32B) + HashMap Ref(8B) = 88B,空间效率为16B/88B = 18%

2.7 由Window虚拟内存导致的长时间停顿

-XX:printGCApplicationStoppedTime  -XX:PrintGCDateStamps -Xloggc:gclog.log 

可以查看GC导致应用停顿的时间

3 实战: Eclipse运行速度调优

三个大非用户时间:类加载时间,编辑时间和垃圾收集时间

3.1 调优前的程序运行状态

 

3.2 升级JDK1.6的性能变化以及兼容问题

-XX:MaxPermSize=256M

3.3 编译时间和类加载时间的优化

jstat -class 16484

-Xverify:none 禁止掉字节码校验过程

编译时间是指虚拟机的JIT编译器(Just In Time Complier)编译热点代码(Hot Spot Code)的耗时。我们知道Java语言为了实现跨平台的特性,Java代码编译出来后形成的Class文件中存储的是字节码(ByteCode),虚拟机通过解释方式执行字节码命令。比起C/C++编译成本地二进制代码来说,速度要慢不少。为了解决程序解释执行的速度问题,JDK1.2以后,虚拟机内置了两个运行时编译器,如果一段Java方法被调用次数达到一定程度,就会被判定为热代码交给JIT即时编译为本地代码,提高运行速度(这就是HotSpot虚拟器名字的由来)。甚至有可能在运行期动态编译比C/C++的编译期静态编译出来的代码更优秀。但是Java运行期编译最大的缺点就是他进行编译需要消耗程序正常的运行时间,这也就是上面所说的编译时间。虚拟机提供了-Xint 禁止编译器运行,强制虚拟机对字节码采用纯解释方式执行(强烈不建议)。

与解释执行相对应的另一方面,虚拟机还有力度更强的编译器:当虚拟机运行在-client模式的时候,使用的是代号为C1的轻量级编译器,另外还有一个代号为C2的相对重量级的编译器能提供更多的优化措施,如果使用-server模式的虚拟机启动程序的话会使用C2编译器

3.4 调整内存设置控制垃圾收集频率。

每当发生一次垃圾收集动作,所有的用户线程都必须跑到最近的一个安全点然后挂起线程等待垃圾回收。这样频繁的GC就会导致很多没有必要的安全点检查、线程挂起及恢复操作。因此最好-Xms和-Xmx参数相同,-Xmn不宜太小,-XX:Permsize和-XX:MaxPermSize相同。

-XX:+DisableExplicitGC屏蔽掉System.gc()的显示调用

3.5 选择收集器降低延迟

-XX:+UseConcMarkSweepGC 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Java虚拟机(JVM)是Java程序的运行环境,它负责将Java字节码转换为可执行的机器码并执行。深入理解Java虚拟机涉及了解JVM的工作原理、内存管理、垃圾回收、类加载机制等方面的知识。 首先,JVM的工作原理是通过解释器或即时编译器将Java字节码转换为机器码并执行。解释器逐条执行字节码指令,而即时编译器将字节码转换为本地机器码,以提高程序的执行效率。 其次,内存管理是JVM的重要任务之一。JVM将内存分为不同的区域,包括堆、栈、方法区等。堆用于存储对象实例,栈用于存储局部变量和方法调用信息,方法区用于存储类的信息。JVM通过垃圾回收机制自动回收不再使用的对象,释放内存空间。 此外,类加载机制也是深入理解JVM的关键内容之一。类加载是将类的字节码加载到内存中,并进行验证、准备、解析等操作。类加载器负责查找并加载类的字节码,而类加载器之间存在着父子关系,形成了类加载器层次结构。 还有其他一些与性能优化、调优相关的内容,如即时编译器的优化技术、垃圾回收算法的选择等,也是深入理解Java虚拟机的重要方面。 总的来说,深入理解Java虚拟机需要对JVM的工作原理、内存管理、垃圾回收、类加载机制以及性能优化等方面有较深入的了解。掌握这些知识可以帮助开发人员编写出更高效、稳定的Java程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值