0%

最近项目在一次更新后,发现定时任务无法执行了,根据日志推断,任务在执行到某一行后莫名其妙的就中断了,这不科学。排查了一下,发现这是ScheduledThreadPoolExecutor的一个坑。

起因

故障原因是由于NoClassDefDoundErorr引起的,由于依赖冲突,导致某个类找不到了,所以当代码执行到某行时,类加载器找不到这个类,就抛一个Error出来,任务线程也就中断了。按照一般的逻辑,本次定时任务被中断,到了下次开始运行的时间应该可以继续执行吧,这就是ScheduledThreadPoolExecutor的坑了:定时任务遇到异常导致线程死亡后,该任务将不会被继续执行。

解决办法

  1. 在向ScheduledThreadPoolExecutor提交任务时,可以拿到一个ScheduledFuture<?>,通过它可以捕捉到ExecutionException,也就是任务执行中抛出的异常(错误)。
  2. 在代码中try-catch-all。
    感觉第二种方式不太优雅。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}

注意将Runnable包装为ScheduledFutureTask的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Overrides FutureTask version so as to reset/requeue if periodic.
*/
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}

问题就在ScheduledFutureTask.super.runAndReset(),我们一探究竟。

1
2
3
4
5
6
7
8
9
10
/**
* Executes the computation without setting its result, and then
* resets this future to initial state, failing to do so if the
* computation encounters an exception or is cancelled. This is
* designed for use with tasks that intrinsically execute more
* than once.
*
* @return {@code true} if successfully run and reset
*/
protected boolean runAndReset()

ScheduledThreadPoolExecutor直接使用了FutureTaskrunAndReset(),注意:

Failing to do so if the computation encounters an exception or is cancelled

所以如果需要支持在出现异常的情况下重复运行,需要自己手动来重写该方法。

译自IBM Developerworks
Thanks for the memory, Linux
Understanding how the JVM uses native memory on Windows and Linux

原文链接

前言

Java堆是我们在写程序时最常用的内存区域,它是存放所有Java对象的位置(译者注:现在未逃逸对象已经使用了栈上分配)。JVM旨在使我们不受主机特性的影响,所以在探讨内存的时候,很自然的就会想起堆内存。毫无疑问,你肯定遇到过由于对象泄漏或者堆内存过小导致的堆内存溢出,并且可能已经掌握了一些调试相关问题的手段。但是,当使用使用Java处理更多数据和更多并发时,你可能会遇到无法使用常规手段修复的内存溢出错误,即使堆内存未满,也会抛出错误(OutOfMemoryError)的情况。发生这种情况时,你需要了解Java运行时环境(JRE)内部的情况。

Java应用在Java运行时的虚拟化环境中运行,但运行时本身也是用编程语言(例如C)开发的、使用native资源(包括内存)的程序。与Java应用使用的Java堆内存不同,native内存是系统运行时进程可用的内存。所有虚拟资源(包括Java堆和Java线程)都必须与虚拟机运行时使用的数据一起存储在native内存中。这意味着宿主机硬件和操作系统对native内存的限制会影响使用者对Java应用的操作。

本文是在不同平台上涵盖相同主题的两篇文章之一。这两者中,你将了解native内存是啥,Java运行时如何使用它,它看起来是啥,以及如何调试native内存溢出错误。本文主要介绍在Windows和Linux平台,并不关注任何特定的运行时(JVM)实现。配套文章介绍了AIX,重点介绍了IBM®DeveloperKit for Java。 (该文章中关于IBM实现的信息对于AIX以外的平台也是如此,因此如果你在Linux上使用IBM Developer Kit for Java或者在IBM 32位Runtime Environment for Windows上,可能会发现该文章也很有用。)

回顾native内存

首先,我将解释操作系统和底层硬件对native内存的限制。如果你比较熟悉编程语言管理动态内存(例如C语言),可以直接跳到下一部分。

硬件限制

Native进程的许多限制是由硬件而不是操作系统引起的。每台计算机都有一个处理器和一些内存(RAM)。处理器拥有有一个或多个处理单元,将数据流解释为要执行的指令,执行整数、浮点运算以及其他高级计算。处理器有许多寄存器,用作执行计算的工作存储器;寄存器大小决定了单个计算可以使用的最大容量。

处理器通过存储器总线连接到物理存储器。物理地址长度(处理器用于索引物理RAM的地址)限制了可以寻址的内存数量。例如,16位物理地址可以从0x0000到0xFFFF寻址,包括2 ^ 16 = 65536个唯一的存储单元。如果每个地址引用一个存储字节,则16位物理地址将允许处理器寻址64KB的存储器。

可以使用一定数量的位来描述处理器。这取决于寄存器的大小,但有例外(比如390 31位)其中它指的是物理地址长度。对于桌面和服务器平台,一般是31、32或64位;对于嵌入式设备和微处理器,可以低至4位。物理地址大小可以与寄存器位宽相等,也可以更大或更小。对于兼容的操作系统来说,大多数64位处理器可以运行32位程序。

表1列出了一些流行的Linux版本和Windows架构及其寄存器位宽和物理地址长度:

架构 寄存器位宽(bits) 物理地址长度(bits)
(现代)英特尔X86 32 32
36使用物理地址扩展(奔腾Pro及更高版本)
x86 64 64 目前48位(范围后期会增加)
PPC64 64 在POWER 5上为50位
390 31-bit 32 31
390 64-bit 64 64

表1.一些常用处理器架构的寄存器位宽和物理地址长度

操作系统与虚拟内存

对于不使用操作系统的程序来说,可以使用处理器寻址范围内的所有内存。但是为了享受多任务和硬件抽象等功能,大部分开发者还是会使用操作系统。

在Windows和Linux等多任务操作系统中,许多程序共用包括内存在内的系统资源。每个程序都需要分配物理内存才能工作。理想情况下,可以设计这样一个操作系统,使每个程序可以直接使用物理内存,并且保证只使用由系统分配的内存。一些嵌入式操作系统的工作方式就类似这样,但在由多个未经过集成测试的程序组成的环境中并不实用,因为任何程序都可能破坏其他程序或操作系统本身的内存。

虚拟内存允许多个进程共享物理内存,而且不会破坏彼此的数据。在使用虚拟内存的操作系统(例如Windows,Linux等其他操作系统)中,每个程序都有自己的虚拟地址空间:物理地址的逻辑区域,其大小由该系统的地址长度决定(31,32或桌面和服务器平台的64位)。进程中的虚拟地址空间可以映射到物理内存、文件或任何其他可寻址存储设备。操作系统可以将物理内存中数据移入和移出交换区域(Windows上的页面文件或Linux上的交换分区),以便充分利用物理内存。当程序试图使用虚拟地址访问存储器时,OS与片上硬件结合将该虚拟地址映射到物理位置。该位置可以是物理RAM,文件或页面文件/交换分区。如果已将某片内存区域移动到交换空间,则在使用之前将会把它重新加载到物理内存中。图1展示了虚拟内存通过进程地址空间映射以实现共享资源的工作原理:
图1.虚拟内存将进程地址空间映射到物理资源

每个程序实例都会作为一个进程运行。Linux和Windows上的进程是有关操作系统控制的资源(例如文件和套接字)信息的集合,通常对应一个虚拟地址空间(在某些架构中大于一个),和至少一个执行线程。

虚拟地址长度可以小于处理器的物理地址长度。英特尔x86 32位最初有32位物理地址,允许处理器处理4GB存储空间。后来增加加了物理地址扩展(PAE)功能,把物理地址长度扩展到36位,支持安装和寻址最多64GB的RAM。PAE支持操作系统将32位4GB虚拟地址空间映射到更大的物理地址空间,但它并不支持每个进程具有64GB虚拟地址空间。即,如果在32位的英特尔服务器上使用超过4GB的内存,则无法把全部内存直接映射到单个进程中。

Address Windowing Extensions功能允许Windows进程将其32位地址空间的一部分作为滑动窗口映射到更大的内存区域。 Linux使用基于将区域映射到虚拟地址空间的类似技术。这意味着虽然用户无法直接引用超过4GB的内存,但可以使用更大的内存区域。

内核态与用户态

虽然每个进程都有自己的地址空间,但用户程序并不能全部使用。地址空间分为用户态和内核态。内核位于操作系统中,包含了计算机硬件接口、调度程序以及提供网络和虚拟内存等服务等功能。

作为计算机引导序列的一部分,操作系统内核运行并初始化硬件。一旦内核配置了硬件及其自身的内部状态,就会启动第一个用户态进程。如果用户程序需要使用操作系统提供的服务,它可以执行系统调用从而跳转到内核程序,然后内核程序执行请求。对于诸如读取和写入文件,联网以及启动新进程等操作,通常需要系统调用。

内核在执行系统调用时需要访问自己的内存和调用进程的内存。因为正在执行当前线程的处理器被配置为使用当前进程的地址空间映射来映射虚拟地址,所以大多数操作系统将每个进程地址空间的一部分映射到公共内核存储器区域。映射供内核使用的地址空间部分称为内核态空间;可以由用户应用程序使用的其余部分称为用户态空间。

内核态和用户态空间的平衡因操作系统而异,甚至在不同硬件架构上运行的相同操作系统的实例之间也存在差异。这种平衡一般是可配置的,可为用户态程序或内核态程序提供更多空间。压缩内核态区域可能会导致诸如限制可以同时登录的用户数量或可以运行的进程数量等问题;较小的用户态空间则意味着供开发者使用的空间较小。

默认情况下,在32位Windows具有2GB的用户态空间和2GB的内核态空间。某些Windows版本上,通过将/3GB*配置项开关添加到引导配置并使用/LARGEADDRESSAWARE*配置项开关重新链接应用程序,可以将用户态空间配置为3GB,内核空间为1GB。在32位Linux上,默认用户态空间为3GB和内核态空间为1GB。有些Linux发行版提供了一种名为hugemem,支持4GB用户态空间的内核。为实现此目的,内核拥有自己的地址空间,用于进行系统调用。在这种情况下,虽然用户态空间变大了,但系统调用变的更慢,因为操作系统必须在用户态和内核态的地址空间之间复制数据,并在每次进行系统调用时重置进程地址空间映射。图2展示了32位Windows的地址空间布局:

图2. 32位Windows的地址空间布局

图3显示了32位Linux的地址空间布局:
 图3. 32位Linux的地址空间布局

对于Linux 390 31位架构,使用单独的内核地址空间使得对于小于2GB的地址空间并不适合划分独立地址空间;但是390架构可以在工作同时使用多个地址空间而不会影响性能。

进程地址空间必须包含程序所需的所有内容:包括程序本身和它使用的共享库(Windows上的DLL,Linux上的.so文件)。共享库不仅可以占用程序不能存储数据的空间,还能分割地址空间并减少可以作为连续块分配的内存。这在使用3GB用户态空间的Windows x86上运行的程序中很明显。 DLL是由首选的加载地址构建的:当加载DLL时,它被映射到特定位置的地址空间,除非该位置已被占用,此时,它会被重新定位并加载到其他位置。最初设计Windows NT时,用户态空间为2GB,系统库被加载至2GB的边界附近,从而使大部分用户态空间可供用户程序使用。当用户态空间扩展到3GB时,系统共享库仍然加载到2GB附近:位于用户空间的中间。虽然总用户态空间为3GB,但无法分配3GB的内存块,因为中间还隔着共享库。

在Windows上使用/3GB*开关可将内核空间减少到原先设计的一半(1GB)。在某些情况下,可能在耗尽1GB内核态空间时遇到慢I/O或创建新用户会话的问题。虽然/3GB*开关对某些应用程序非常有价值,但使用它的任何环境都应在部署之前进行全面的压力测试。

Native内存泄漏或使用太多的native内存使用会导致不同的问题,具体取决于是否耗尽地址空间或物理内存。耗尽地址空间通常只发生在32位的进程中:因为最大4GB很容易分配。64位进程有成百上千GB的用户态空间,所以很难耗尽。如果你耗尽了Java进程的地址空间,Java运行时就会出现我将在本文后面描述的奇怪症状。在进程地址空间比物理内存更大的系统上运行时,内存泄漏或过度使用native内存会强制操作系统将某些native进程的虚拟地址空间交换至外存。访问已被交换的内存地址比读取驻留(物理内存)地址要慢得多,因为操作系统必须从硬盘驱动器读取数据。为了可以分配足够的内存,可能耗尽所有物理内存和交换分区(页面文件);在Linux上,这会触发内核内存不足(OOM)杀手,它会强行杀死占用大量内存的进程。在Windows上,分配开始失败的方式与地址空间已满时的方式相同。

如果你使用的虚拟内存比物理内存更大,很明显,在进程因为内存耗尽而被杀死之前很久就会出现问题。系统会停止响应:大部分时间都用于在交换空间和物理内存之间来回复制。发生这种情况时,计算机和各个应用程序的性能将变得非常差。当JVM的Java堆被交换出时,GC的性能变得极差,以至于程序好像挂起了。如果在同一台计算机上运行多个Java程序实例时,物理内存必须足以容纳所有Java堆。

Java运行时如何使用native内存

Java运行时是操作系统中的进程,它受到作者在前一节中概述的硬件和操作系统限制的约束。运行时环境提供了由未知代码驱动的功能,所以无法预测运行时环境在每种情况下都需要哪些资源。 在Java环境中,Java应用程序执行的每个操作都可能会影响提供该环境的运行时资源需求。本节介绍Java应用程序使用native内存的方式和以及为啥要这样用。

堆与GC

Java堆是分配对象的内存区域。大多数Java SE实现都有一个逻辑堆,尽管一些专业Java运行时(例如Java实时规范(RTSJ))有多个堆。根据用于管理堆内存的垃圾收集(GC)算法,可以将单个物理堆拆分为逻辑部分。这些部分通常由被Java内存管理器(包括垃圾收集器)管理,native内存的连续区域实现。

在Java命令行中,-Xmx和-Xms选项可以控制堆大小(mx是堆的最大大小,ms是初始大小)。虽然逻辑堆大小可以根据堆上对象数量和GC消耗的时间来控制,但使用的native内存数量是不变的。由于大部分GC算法依赖于连续的内存块,因此堆需要扩展时,无法分配更多native内存,All heap memory must be reserved up front。

Reserving native memory is not the same as allocating it. When native memory is reserved, it is not backed with physical memory or other storage. Although reserving chunks of the address space will not exhaust physical resources, it does prevent that memory from being used for other purposes. A leak caused by reserving memory that is never used is just as serious as leaking allocated memory.

Some garbage collectors minimise the use of physical memory by decommitting (releasing the backing storage for) parts of the heap as the used area of heap shrinks.

More native memory is required to maintain the state of the memory-management system maintaining the Java heap. Data structures must be allocated to track free storage and record progress when collecting garbage. The exact size and nature of these data structures varies with implementation, but many are proportional to the size of the heap.

即时编译(JIT)

JIT编译器在运行时将Java字节码编译为优化的native可执行代码。这极大地提高了Java运行时的运行时速度,并允许Java应用程序以与native代码一样的速度运行。

字节码编译使用native内存(与gcc等静态编译器需要运行内存的方式相同),但JIT的输入(字节码)和输出(可执行代码)也必须存储在native内存中。包含许多JIT编译方法的Java应用程序比较小的应用程序使用更多的native内存。

类和类加载器

Java应用程序由定义对象结构和方法逻辑的类组成。它们还使用Java运行时类库(例如java.lang.String)中的类,并且可以使用第三方库。只要它们被使用,这些类就需要存储在内存中。

如何存储类因实现而异。Sun JDK使用堆中的永久代(PermGen)。Java 5以后的IBM实现为每个类加载器分配native内存块,并将类数据存储在里面。现代Java运行时具有诸如类共享之类的技术,这些技术可能需要将共享内存映射到地址空间。要了解这些分配机制如何影响Java运行时的本机占用空间,可以阅读该实现的官方文档。但这些实现仍然存在共同点。

在最基本的层面上,使用更多类会占用更多内存。这可能意味着您的native内存使用量增加,或者您必须显式调整某个区域的大小:比如永久代或共享类缓存,以便足够存放所有类。请记住,不仅是程序需要足够的内存;框架、应用程序服务器、第三方库和Java运行时都包含有按需加载而且占用内存的类。

Java运行时可以卸载类来回收空间,但只能在严格的条件下进行。不可能卸载单个类。卸载类加载器,取出它们加载的所有类。只有在以下情况下才能卸载类加载器:

  • Java堆没有该类加载器(java.lang.ClassLoader)的实例。
  • Java堆没有由该类加载器加载的java.lang.Class的实例。
  • 该类加载器加载的任何类的对象都不在Java堆上存活(引用)。

注意,Java运行时为所有Java应用程序设置的三个默认类加载器: bootstrap、extension和application均不满足这些标准;因此,任何系统类(如java.lang.String)或者通过application类加载器加载的任何应用程序类都无法在运行时释放。

即使类加载器符合垃圾回收条件,运行时也只会将类加载器作为GC循环的一部分进行收集。某些实现仅在某些GC周期中卸载类加载器

在没有意识到的情况下,也可以在运行时生成类。许多J2EE应用程序使用JavaServer Pages(JSP)技术来生成Web页面。使用JSP为每个执行的.jsp页面生成一个类,因此,会延长加载这些类的类加载器的生命周期:通常是Web应用程序的生命周期。

生成类的另一种常用方法是使用Java反射。反射的工作方式因Java实现而异,但Sun和IBM实现都使用下面的方法。

使用java.lang.reflect API时,Java运行时必须将反射对象(如java.lang.reflect.Field)的方法连接到反射的对象或类。这可以通过使用Java native接口(JNI)访问器来完成,该访问器使用方便但速度很慢,或者在运行时为要反射的对象动态生成所需类。第二种方法使用麻烦但执行速度快,因此非常适合经常使用反射技术的应用程序。

Java运行时在反射类的前几次使用JNI方法,但在多次使用之后,访问器被扩展为字节码访问器,这样需要构建相关类并用新的类加载器加载。大量反射可能会导致创建许多访问器类和类加载器。保持对反射对象的引用会使这些类保持活跃并继续占用空间。因为创建字节码访问器非常慢,所以Java运行时可以缓存这些访问器供以后使用。某些应用程序和框架还会缓存反射对象,因此也会占用native内存。

JNI

JNI允许native代码(使用native编译语言,如C和C++编写的应用程序)调用Java方法,反之亦然。Java运行时本身在很大程度上依赖于JNI代码来实现类库函数,例如文件和网络I/O. JNI应用程序可以通过三种方式增加Java运行时占用的native内存:

  • JNI应用程序的本机代码被编译为加载到进程地址空间的共享库或可执行文件。大型native应用程序只需加载就可占据很大一部分进程地址空间。
  • Native代码必须与Java运行时共享地址空间。
  • 某些JNI函数可以使用native内存作为其正常操作的一部分。GetTypeArrayElements和GetTypeArrayRegion函数可以将Java堆数据复制到native内存缓冲区,以便使用native代码。是否复制取决于运行时实现。(IBM Developer Kit for Java 5.0及更高版本生成native副本。)以这种方式访问Java堆中的大量数据可能会使用相应数量的native堆。

NIO

Java 1.4中添加的新I/O(NIO)相关类引入了基于通道和缓冲区的新I/O方法。除了基于Java堆内存实现的I/O缓冲区之外,NIO还添加了对native内存实现的的DirectByteBuffers(使用java.nio.ByteBuffer.allocateDirect()方法分配)的支持。DirectByteBuffers可以直接传递给操作系统库函数来执行I/O:某些情况下这样做明显速度更快,因为可以避免在Java堆和native堆之间复制数据(即零拷贝,译者注)。

那么,DirectByteBuffer数据究竟在哪里存储?应用程序仍然使用Java堆上的对象来编排I/O操作,但带有数据的缓冲区在native内存中:Java堆对象仅包含对native堆缓冲区的引用。非直接的ByteBuffer将其数据保存在Java堆上的byte数组中。图4显示了直接和非直接ByteBuffer对象之间的区别:
 图4.直接和非直接java.nio.ByteBuffers的内存拓扑


DirectByteBuffer对象自动清理其对应的native缓冲区,但只能作为Java堆GC的一部分执行:因此它们不会自动响应native堆上的压力。GC仅在Java堆变满时才会发生:无法继续分配内存时,或者Java应用程序显式调用,并不推荐显示调用,因为会导致性能问题(译者注,Full GC会导致STW)。

极端情况下,native堆已满并且一个或多个DirectByteBuffers符合GC条件(可以在native堆中释放空间),但由于Java堆还没有满,所以不会GC。

线程

程序中的每个线程都需要内存来存储其堆栈(用于保存局部变量的内存区域以及调用函数时的状态)。每个Java线程都需要运行堆栈空间。根据实现,Java线程可以具有单独的native和Java堆栈。除了堆栈空间之外,每个线程还需要一些native内存用于thread-local存储和内部数据结构。

栈大小因Java实现和架构而异。某些实现可以指定Java线程的栈大小。通常在256KB和756KB之间。

尽管每个线程使用的内存量非常小,但对于具有数百个线程的程序,线程栈的总内存使用量可能很大。当程序中的线程数量比可用处理器数量多很多时,会导致效率降低和内存使用量增加。

如何判断我的native内存是否耗尽?

Java运行时处理Java堆内存耗尽和native堆内存耗尽的方式完全不同,尽管这两种情况都可能出现类似的症状。Java程序在Java堆耗尽时很难运行:因为Java应用程序很难在不分配对象的情况下执行任何操作。一旦Java堆填满,GC性能就会变得非常糟糕,并抛出OutOfMemoryErrors。

相反,一旦Java运行时启动并且程序处于稳定状态,就可以一直工作到native堆完全耗尽。不一定出现奇怪的行为(native堆耗尽时),因为需要分配native内存的操作比需要分配Java堆的操作要少得多。虽然需要分配native内存的操作因JVM实现而异,但一些常用示例包括:启动线程,加载类以及执行某些类型的网络和文件I/O.

Native内存不足时的症状比Java堆内存不足时的症状不尽相同,因为native堆的分配没有单一的控制入口。尽管所有Java堆分配都在Java内存管理系统的控制之下,但任何本机代码 :无论是在JVM内部,Java类库还是应用程序代码,都可以执行本机内存分配,并捕获错误。然后根据设计者的想法,相应代码然后可以处理该错误:它可以通过JNI接口抛出OutOfMemoryError,在控制台打印日志、静默失败并稍后再试、或者做一些其他的事情。

由于native内存耗尽的诸多症状并不典型,所以没有一种简单的办法可以确认native内存耗尽。所以,需要使用来自操作系统和Java运行时的数据来确认问题究竟出在哪里。

native内存耗尽的几个例子

AbstractQueuedSynchronizer

AQS是JDK提供的同步器抽象类,主要用于实现各种用途的锁(信号量,闭锁,可重入读写锁),本文主要记录AQS组成结构和核心能力分析。

结构分析

成员变量
  • head:Node //同步队列头节点
  • tail:Node //同步队列尾节点
  • state:int //当前同步状态
  • unsafe:Unsafe.getUnsafe()
  • stateOffset:long
  • headOffset:long
  • tailOffset:long
  • waitStatusOffset:long
  • nextOffset:long
方法(Protected)
  • getState():int //获取同步状态
  • setState():int //设置同步状态
  • compareAndSetState(int, int):int //使用CAS设置同步状态
  • tryAcquire(int):boolean //尝试获取独占锁,实现时应先查看当前对象是否支持独占锁。
  • tryRelease(int):boolean //尝试释放独占锁
  • tryAcquireShared(int):boolean //尝试获取共享锁
  • tryReleaseShared(int):boolean //尝试释放共享锁
  • isHeldExclusively():boolean //当前线程是否获得了独占锁
方法(Public)
  • acquire(int):void //获取独占锁(不响应中断)
  • acquireInterruptibly(int):void //获取独占锁(响应中断)
  • tryAcquireNanos(int, long):boolean //获取独占锁(不响应中断),若等待超时则失败
  • release(int):boolean //释放独占锁,可以用来实现Lock.unlock()
  • acquireShared(int):void //获取独占锁(不响应中断)
  • acquireSharedInterruptibly(int):void //获取独占锁(响应中断)
  • tryAcquireSharedNanos(int, long):boolean //获取共享锁(不响应中断),若等待超时则失败
  • releaseShared(int):boolean //释放共享锁
  • hasQueuedThreads(int):boolean //同步队列中是否有线程在等待
  • hasContended():boolean //查询是否其他线程也曾竞争该同步器;即是否有线程已经阻塞。
  • getFirstQueuedThread():Thread //获取第一个进入同步队列的线程(等待时间最长的)。
  • isQueued(Thread):boolean //线程是否在同步队列中
  • hasQueuedPredecessors():boolean //判断当前线程是否有前导节点,即判断当前线程是否在同步队列的队首,来返回同步队列是否有比当前线程等待更久的线程。
  • getQueueLength():int //返回同步队列的预估长度
  • getQueuedThreads():Collection //获取同步队列中的线程集合
  • getExclusiveQueuedThreads():Collection //获取同步队列中,等待获取独占锁的线程集合
  • getSharedQueuedThreads():Collection //获取同步队列中,等待获取共享锁的线程集合
  • owns(ConditionObject):boolean //判断当前同步器是否使用了给定condition
  • hasWaiters(ConditionObject):boolean //判断是否有线程在给定condition等待
  • getWaitQueueLength(ConditionObject):int //返回在给定condition上等待的线程估计数量
  • getWaitingThreads(ConditionObject):Collection //返回在给定condition上等待的线程集合。
  • toString():String

JVM在不同的竞争情况下,对synchronized提供了不同的优化方式

  • 锁消除
  • 自旋
  • 自适应自旋

在优化过程中锁主要存在4种状态:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

这四种状态会根据资源竞争情况进行膨胀(升级)。

偏向锁

偏向锁的目的是为了消除无竞争情况下的同步原语,从而进一步提升性能。‘偏’指的是该锁会偏向于首先获得他的线程,如果在执行过程中,该锁未被其他的线程获取,则持有偏向锁的线程就一直不需要同步。

获取偏向锁的过程
  1. 检查Mark World偏向锁是否打开。若为1,则执行步骤2,否则代表不支持偏向锁。
  2. 检查Mark World储存的线程ID是否为当前线程ID,如果是则执行同步块,否则执行步骤3.
  3. 使用CAS操作将Mark World中线程ID改为当前线程ID,若成功则执行同步代码块,否则执行步骤4。
  4. 当拥有该锁的线程到达安全点后,挂起该线程,升级为轻量级锁。
释放偏向锁的过程
  1. 偏向锁采取了竞争才会释放锁的方案,线程并不会主动放弃偏向锁,需要等待其他线程竞争。
  2. 等待全局安全点。
  3. 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否活着,如果处于非活动状态,则将对象头设置为无锁状态,否则设置为被锁定状态。如果锁对象处于无锁状态,则恢复到无锁状态(01),以允许其他线程竞争,如果锁对象处于锁定状态,则挂起持有偏向锁的线程,并将对象头Mark World的锁记录指针改为当前线程的锁记录,锁升级为轻量锁。

轻量级锁

轻量级锁区别于使用mutex方式实现的重量级锁,他使用对象头中的Mark World进行同步。

获取轻量锁的过程
  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中。
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
释放轻量锁的过程
  1. 使用CAS操作将对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来(依据Mark Word中锁记录指针是否还指向本线程的锁记录),如果替换成功,则执行步骤2,否则执行步骤3。
  2. 如果替换成功,整个同步过程就完成了,恢复到无锁的状态(01)。
  3. 如果替换失败,说明有其他线程尝试获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

该过程的完整流程图

image

Access method

版本5.7

system

当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system

const

当根据主键或唯一二级索引列与常数进行等值匹配时,对单表的访问方法就const

ref

当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref

ref_or_null

当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null

range

如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法。

index

当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index

eq_ref

在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref

index_merge

在一般情况下执行一个查询时最多只会用到单个二级索引,特殊情况下也可能在一个查询中使用到多个二级索引,这种使用到多个索引来完成一次查询的执行方法称之为index_merge,具体的合并方式分为以下三种

  • Intersection:交集,某个查询可以使用多个二级索引,将从多个二级索引中查询到的结果取交集。
    • 二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只匹配部分列的情况。
    • 主键列可以是范围匹配。
  • Union:并集,适用于使用不同索引的搜索条件之间使用OR连接起来的情况。
    • 二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹配,不能出现只出现匹配部分列的情况。
    • 主键列可以是范围匹配。
    • 使用Intersection索引合并的搜索条件。
  • Sort-Union:先按照二级索引记录的主键值进行排序,之后按照Union索引合并方式执行的方式称之为Sort-Union索引合并,显然Sort-Union索引合并比单纯的Union索引合并多了一步对二级索引记录的主键值排序的过程。

unique_subquery

类似于两表连接中被驱动表的eq_ref访问方法,unique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery

index_subquery

unique_subquery类似,只不过访问子查询中的表时使用的是普通的索引。

all

全表扫描方式。

fulltext

全文检索时会使用该方式。

参考链接

引子

在使用自建的OSS时,发现使用Safari下载中文名文件,文件名是乱码,如果在响应头中将文件名使用UTF-8进行URLEncode,又会直接拿到编码过后的文件名。研究之后发现其实还是HTTP的曲折发展史造成的问题。

RFC 5987

  • RFC 2068
  • RFC 2616
  • RFC 5987

HTTP 1.1协议历经三代发展,从最初的不支持非ASCII文件名到支持UTF-8,中间阶段不同的UA使用了不同的方式对非ASCII文件名进行了支持。最终由RFC 5987结束了这种混乱的局面。
最佳的解决方案如下:

1
2
//filename需要使用RFC 3986标准声明的“百分号URL编码”
Content-Disposition: attachment;filename="%e2%82%ac%20rates.txt"; filename*=utf-8''%e2%82%ac%20rates.txt

这种方式既兼容使用旧标准(RFC 2616)的IE,同时也兼容使用新标准(RFC 5987)的UA,完美的解决了多语言的问题(唯一的小瑕疵是需要有一个ASCII编码的后缀)。

/etc/resolve.conf文件大家应该很熟悉了,最近被同事问到修改该文件后后总是自动还原的问题,在这里做一个简单的记录。

情景重现

系统类型 版本
Ubuntu Server 18.04
1
2
3
echo 'nameserver 114.114.114.114' > /etc/resolve.conf
cat /etc/resolve.conf
nameserver 114.114.114.114

一段时间后…

1
2
3
4
5
6
7
cat /etc/resolve.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
# 127.0.0.53 is the systemd-resolved stub resolver.
# run "systemd-resolve --status" to see details about the actual nameservers.

nameserver 127.0.0.53

解决方案

通过查看还原后的文件内容,发现两个关键信息

  • systemd-resolve –status
  • nameserver 127.0.0.53

这说明系统将DNS解析服务由systemd的resolve服务托管,resolve.conf文件也由该服务维护。如果想使用自定义DNS服务器,很简单。

1
2
3
systemctl stop systemd-resolve
systemctl disable systemd-resolve
echo 'nameserver 114.114.114.114' > /etc/resolve.conf

后记

看文档,看提示信息。一切皆有迹可循。

Spring中被代理对象内部调用

 众所周知,Spring AOP是基于动态代理实现的,最近被同事问到在被代理对象中直接进行内部调用,拦截器或切面不生效的问题,在这里做一个简单的记录。

情景重现

Spring版本:5.1.6

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class Impl implements Service{

public void a(){
b();
}

public void b(){

}

}

 如上所示,在Impl类外部调用a方法时,针对b方法的拦截器或切面并不能生效。原因是这种情形下,this代表被代理对象,调用并没有通过代理对象执行。

解决方案一

  1. 确保使用CGLIB并将代理对象暴露给用户。

    1
    @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
  2. 获取代理对象,强制转型为被代理对象(这里牵扯到CGlLIB的特点,代理对象其实是被代理对象的子类实例)

1
2
3
4
//继续使用上面的例子
public void a(){
((Impl)AopContext.currentProxy()).b();
}

解决方案二

Bean加载后,向内部注入增强对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class Impl implements Service{

@Autowired //注入上下文
private ApplicationContext context;

private Impl proxy;//表示代理对象,不是目标对象

@PostConstruct //③ 初始化方法
private void setSelf() {
//此种方法不适合于prototype Bean,因为每次getBean返回一个新的Bean
proxy = context.getBean(Impl.class);
}

public void a(){
proxy.b();
}

public void b(){

}

}

解决方案三

方案二需要类自己实现注入增强对象的方法,可以通过BeanPostProcessor在目标对象中注入,这样便提高了代码复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface BeanSelfAware {  
void setSelf(Object proxy);
}

@Service
public class Impl implements Service, BeanSelfAware{
private Impl proxy;//表示代理对象,不是目标对象

private void setSelf() {
//此种方法不适合于prototype Bean,因为每次getBean返回一个新的Bean
proxy = context.getBean(Impl.class);
}

public void a(){
proxy.b();
}

public void b(){

}

}

@Component
public class InjectBeanSelfProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//如果Bean实现了BeanSelfAware标识接口,就将代理对象注入
if(bean instanceof BeanSelfAware) {
//即使是prototype Bean也可以使用此种方式
((BeanSelfAware) bean).setSelf(bean);
}
return bean;
}
}

From 51CTO

引言

JAVA堆内存管理是影响性能主要因素之一。
堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的,如图:
image

  1. JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
  2. 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  3. 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  4. 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:

  • MetaspaceSize :初始化元空间大小,控制发生GC阈值
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

为什么移除永久代?

移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题了!

分代概念

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

  • Minor GC : 清理年轻代
  • Major GC : 清理老年代
  • Full GC : 清理整个堆空间,包括年轻代和永久代
    所有GC都会停止应用所有线程。

为什么分代?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

为什么survivor分为两块相等大小的幸存空间?

主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。

JVM堆内存常用参数

参数 描述
-Xms 堆内存初始大小,单位m、g
-Xmx(MaxHeapSize) 堆内存最大允许大小,一般不要大于物理内存的80%
-XX:PermSize 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了
-XX:MaxPermSize 非堆内存最大允许大小
-XX:NewSize(-Xns) 年轻代内存初始大小
-XX:MaxNewSize(-Xmn) 年轻代内存最大允许大小,也可以缩写
-XX:SurvivorRatio=8 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1
-Xss 堆栈内存大小

垃圾回收算法(GC,Garbage Collection)

红色是标记的非活动对象,绿色是活动对象。

  • 标记-清除(Mark-Sweep)
    GC分为两个阶段,标记和清除。首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
    image

  • 复制(Copy)
    将内存按容量划分为两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。缺点需要两倍的内存空间。
    image

  • 标记-整理(Mark-Compact)
    也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。
    一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。
    image

    垃圾收集器

  • 串行收集器(Serial)

比较老的收集器,单线程。收集时,必须暂停应用的工作线程,直到收集结束。

  • 并行收集器(Parallel)

多条垃圾收集线程并行工作,在多核CPU下效率更高,应用线程仍然处于等待状态。

  • CMS收集器(Concurrent Mark Sweep)
    CMS收集器是缩短暂停应用时间为目标而设计的,是基于标记-清除算法实现,整个过程分为4个步骤,包括:
    • 初始标记(Initial Mark)
    • 并发标记(Concurrent Mark)
    • 重新标记(Remark)
    • 并发清除(Concurrent Sweep)

其中,初始标记、重新标记这两个步骤仍然需要暂停应用线程。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段。
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。

  • G1收集器(Garbage First)
    G1收集器将堆内存划分多个大小相等的独立区域(Region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了再有限时间内获得更高的收集效率。G1收集器工作工程分为4个步骤,包括:
    • 初始标记(Initial Mark)
    • 并发标记(Concurrent Mark)
    • 最终标记(Final Mark)
    • 筛选回收(Live Data Counting and Evacuation)

初始标记与CMS一样,标记一下GC Roots能直接关联到的对象。并发标记从GC Root开始标记存活对象,这个阶段耗时比较长,但也可以与应用线程并发执行。而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。最后在筛选回收阶段对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收。

垃圾收集器参数

参数 描述
-XX:+UseSerialGC 串行收集器
-XX:+UseParallelGC 并行收集器
-XX:+UseParallelGCThreads=8 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与CPU数量相等
-XX:+UseParallelOldGC 指定老年代为并行收集
-XX:+UseConcMarkSweepGC CMS收集器(并发收集器)
-XX:+UseCMSCompactAtFullCollection 开启内存空间压缩和整理,防止过多内存碎片
-XX:CMSFullGCsBeforeCompaction=0 表示多少次Full GC后开始压缩和整理,0表示每次Full GC后立即执行压缩和整理
-XX:CMSInitiatingOccupancyFraction=80% 表示老年代内存空间使用80%时开始执行CMS收集,防止过多的Full GC
-XX:+UseG1GC G1收集器
-XX:MaxTenuringThreshold=0 在年轻代经过几次GC后还存活,就进入老年代,0表示直接进入老年代

为什么会堆内存溢出?

在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。

OOM(Out of Memory)异常常见有以下几个原因:
  1. 老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
  2. 永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
  3. 代码bug,占用内存无法及时回收。

OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。
可以通过添加个参数-XX:+HeapDumpOnOutMemoryError,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

熟悉了JAVA内存管理机制及配置参数,下面是对JAVA应用启动选项调优配置:

1
2
JAVA_OPTS="-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:
ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/var/log/gc.log"
  • 设置堆内存最小和最大值,最大值参考历史利用率设置
  • 设置GC垃圾收集器为G1
  • 启用GC日志,方便后期分析

小结

  • 选择高效的GC算法,可有效减少停止应用线程时间。
  • 频繁Full GC会增加暂停时间和CPU使用率,可以加大老年代空间大小降低Full GC,但会增加回收时间,根据业务适当取舍。

名词解释

双亲委拖:当类加载器A被请求加载某个类B,则A并不会直接去加载B,而是把这个请求委派给父类加载器C,每一层的类加载器都是如此,因此所有的类加载请求都会委派到顶端的启动类加载器;只有当父类加载器在其搜索范围内不能找到所需的类后,把结果反馈给子类加载器,子类加载器才会尝试去直接加载。

JDK中类加载器分类如下:

名称 说明
BootStrap ClassLoader cpp实现,负责加载JAVA_HOME/lib下的类
Extension ClassLoader java实现,负责加载JAVA_HOME/lib/ext下的类
Application ClassLoader java实现,负责加载程序入口目录下的类
User ClassLoader 用户自定义

双亲委拖正是基于以上类加载器的设定,明确了类加载的分工和过程,确保类加载器只加载负责范围内的类,从而不会造成混乱。

对于任意类,都由加载它的类加载器和类本身确定其在JVM中的唯一性。如果同一个类被不同的类加载器加载为不同的实例,则有可能会出现异常(例如行为异常,无法强制转型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

/**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}