类的访问修饰符
访问控制符 | 同一类中 | 同一包中 | 同一子类中 | 其他 |
---|---|---|---|---|
private | 是 | 否 | 否 | 否 |
default | 是 | 是 | 否 | 否 |
protected | 是 | 是 | 是 | 否 |
public | 是 | 是 | 是 | 是 |
Error与Exception的区别
i++与++i
基本数据类型
线程安全的数据类型
Java中的深拷贝和浅拷贝
而深拷贝是指在拷贝对象时,同时会对引用指向的对象进行拷贝。
区别就在于是否对 对象中的引用变量所指向的对象进行拷贝。
1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)
2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)
final关键字
finalize关键字
(1).对象不一定会被回收。
(2).垃圾回收不是析构函数。
(3).垃圾回收只与内存有关。
(4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。
finally关键字
在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
String:
StringBuffer:字符创变量
StringBuilder:字符创变量
Arrays.copyof()与System.arraycopy()区别
Collection 和 Collections的区别
Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。
Collection是个java.util下的接口,它是各种集合结构的父接口。
List, Set, Map是否继承自Collection接口? List,Set是 Map不是
HashMap:
HashTable:
TreeMap:
LinkedHashMap
ArrayList
LinkedList
Comparator和Comparable在排序中的应用
1、一个类实现了Camparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。2、Comparator可以看成一种算法的实现,将算法和数据分离3、Comparator也可以在下面两种环境下使用:3.1、类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身3.2、可以使用多种排序标准,比如升序、降序等什么是集合迭代器快速失败行为
反射
Java序列化与反序列化
transient关键字
静态代理
优点
缺点
横向扩展:代理更多的类纵向扩展:增强更多的方法
动态代理
5.1、找到被代理的类
5.1.1、从缓存中获取代理类,如果没有从 ProxyClassFactory.apply 方法中获取5.1.2、通过反射加载接口5.1.3、生成一个 atomicLong 的数值5.1.4、生成代理类名 com.sun.proxy.$Proxy + atomicLong的数值5.1.5、通过ProxyGenerator.generateProxyClass 方法生成 class文件的二进制数组5.1.6、通过 defineClass0 生成Class
5.2、通过类文件找到构造函数5.3、通过反反射生成 类的实例 newInstance()
优点:
缺点:
cglib代理
NIO
2、缓冲区Buffer
2.1、 在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
2.1.1、position:指定了下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。
2.1.2、limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。2.1.3、capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。
2.2、缓冲区分片
可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。
2.3、只读缓冲区
只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。
2.4、直接缓冲区
直接缓冲区是为加快I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或者从一个中间缓冲区中拷贝数据。
2.5、内存映射文件I/O
内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是通过使文件中的数据出现为 内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中
3、通道Channel
4、Selector
4.1、NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各种I/O事件地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件4.2、使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤:
4.2.1. 向Selector对象注册感兴趣的事件4.2.2. 从Selector中获取感兴趣的事件4.2.3. 根据不同的事件进行相应的处理
4.3、Selector是NIO中实现I/O多路复用的关键类。Selector实现了通过一个线程管理多个Channel,从而管理多个网络连接的目的。Channel代表这一个网络连接通道,我们可以将Channel注册到Selector中以实现Selector对其的管理。一个Channel可以注册到多个不同的Selector中。当Channel注册到Selector后会返回一个SelectionKey对象,该SelectionKey对象则代表这这个Channel和它注册的Selector间的关系。并且SelectionKey中维护着两个很重要的属性:interestOps、readyOps
4.3.1、interestOps是我们希望Selector监听Channel的哪些事件。我们将我们感兴趣的事件设置到该字段,这样在selection操作时,4.3.2、当发现该Channel有我们所感兴趣的事件发生时,就会将我们感兴趣的事件再设置到readyOps中,这样我们就能得知是哪些事件发生了以做相应处理。
4.4、Selector中维护3个特别重要的SelectionKey集合,分别是
4.4.1、keys:所有注册到Selector的Channel所表示的SelectionKey都会存在于该集合中。keys元素的添加会在Channel注册到Selector时发生。4.4.2、selectedKeys:该集合中的每个SelectionKey都是其对应的Channel在上一次操作selection期间被检查到至少有一种SelectionKey中所感兴趣的操作已经准备好被处理。该集合是keys的一个子集。4.4.3、cancelledKeys:执行了取消操作的SelectionKey会被放入到该集合中。该集合是keys的一个子集。
JVM
1、JVM全称是Java Virtual Machine(java虚拟机)。
它之所以被称之为是“虚拟”的,就是因为它仅仅是由一个规范来定义的抽象计算机。JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。
2、JRE/JDK/JVM是什么关系
2.1、JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
2.2、JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。2.3、JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台
3、JVM的生命周期
3.1、当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。3.2、JVM实例对应了一个独立运行的java程序,它是进程级别。
3.2.1、启动:启动一个Java程序时,一个JVM实例就产生了,任何一个拥有publicstatic void main(String[] args)函数的class都可以作为JVM实例运行的起点3.2.2、运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程3.2.3、消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
4、JVM运行原理
4.1、JVM装入环境。JVM提供的方式是操作系统的动态连接文件,查找 JVM.dll4.2、装载JVM.dll通过第一步已经找到了JVM的路径,Java通过LoadJavaVM来装入JVM.dll文件。装入工作很简单,就是调用Windows API函数: LoadLibrary装载JVM.dll动态连接库.然后把JVM.dll中的导出函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和GetDefaultJavaVMInitArgs函数指针变量上。JVM.dll的装载工作宣告完成。4.3、初始化JVM挂接到JNIENV(JNI调用接口)实例,获得本地调用接口,这样就可以在Java中调用JVM的函数了。调用InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。4.4、运行Java程序Java程序有两种方式一种是jar包,一种是class。运行jar(Java -jarXXX.jar)的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用Java类Java.util.jar.JarFileJNIEnv中方法getManifest()并从返回的Manifest对象中取getAttributes("Main-Class")的值即jar包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。main函数直接调用Java.c中LoadClass方法装载该类。如果是执行class方法。main函数直接调用Java.c中LoadClass方法装载该类。然后main函数调用JNIEnv实例的GetStaticMethodID方法查找装载的class主类中“publicstatic void main(String[] args)”方法,并判断该方法是否为public方法,然后调用JNIEnv实例的CallStaticVoidMethod方法调用该Java类的main方法。
5、JVM体系结构
5.1、Class loader子系统:根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到 Runtime data area(运行时数据区域)中的method area(方法区域)。5.2、Execution engine子系统:执行classes中的指令。方法的字节码是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。执行引擎执行字节码时,首先取得一个操作码,如果操作码有操作数,取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在线程完成前将一直持续。任何JVM实现的核心是Execution engine,换句话说:Sun 的JDK 和IBM的JDK好坏主要取决于他们各自实现的Execution engine的好坏。5.3、Native interface组件 :与native libraries交互,是其它编程语言交互的接口。Java里声明为native的方法多数在jdk/src/<platform>/native里可以找到。其中<platform>可以是share,也就是平台中立的代码;也可以是某个具体平台。这个native目录里的结构跟Java源码结构一样是按包名来组织的。不过需要提醒的是,这些native方法不是“JVM”的,是“类库”的,不在JVM里面。
5.4、Runtime data area 组件:Java虚拟机定义了若干种程序运行时使用到的运行时数据区,有一些是随虚拟机的启动而创建,随虚拟机的退出而销毁,如堆、方法区。第二种则是与线程一一对应,随线程的开始和结束而创建和销毁,如Java栈,PC寄存器。
6、ClassLoader
6.1、Java程序并不是一个原生的可执行文件,而是由许多独立的类文件组成,每一个文件对应一个Java类。此外,这些类文件并非立即全部装入内存的,而是根据程序需要装入内存。ClassLoader专门负责类文件装入到内存。6.2、数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类加载器由 Class.getClassLoader() 返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。6.3、Bootstrap ClassLoader:BootStrap 是最顶层的类加载器,它是由C++编写并且已经内嵌到JVM中了,主要用来读取Java的核心类库JRE/lib/rt.jar。
6.4、Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包6.5、App ClassLoader(System ClassLoader):负责记载classpath中指定的jar包及目录中class6.6、Custom ClassLoader:属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader6.7、JVM有四种类型的类加载器,
即java是如何区分一个类该由哪个类加载器来完成呢?在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类”。具体流程如下:
6.7.1、"A类加载器"加载类时,先判断该类是否已经加载过了;6.7.2、如果还未被加载,则首先委托其"A类加载器"的"父类加载器"去加载该类,这是一个向上不断搜索的过程,当A类所有的"父类加载器"(包括bootstrap classloader)都没有加载该类,则回到发起者"A类加载器"去加载;6.7.3、如果还加载不了,则抛出ClassNotFoundException。
6.8、Class.forName()与ClassLoader.loadClass()
这两方法都可以通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象,区别如下:
6.8.1、初始化
Class.forName()会对类初始化,而loadClass()只会装载或链接。ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的。可以利用上述的差异。比如,要加载一个静态初始化开销很大的类,就可以选择提前加载该类(以确保它在classpath下),但不进行初始化,直到第一次使用该类的域或方法时才进行初始化
6.8.2、类加载器可能不同
Class.forName(String)方法(只有一个参数),使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器。当然,它也有个重载的方法,可以指定加载器。相应的,ClassLoader.loadClass()方法是一个实例方法(非静态方法)调用时需要自己指定类加载器,那么这个类加载器就可能是也可能不是加载调用代码的类加载器(调用代码类加载器通getClassLoader0()获得)
6.9、类的加载过程
Java类加载机制与Tomcat类加载器架构
类的加载要经过三步:装载(Load),链接(Link),初始化(Initializ)。其中链接又可分为校验(Verify),准备(Prepare),解析(Resolve)三步。
6.9.1、加载
ClassLoader就是用来装载的。通过指定的className,找到二进制码,生成Class实例,放到JVM中。在加载阶段,虚拟机需要完成以下三件事(虚拟机规范对这三件事的要求并不具体,因此虚拟机实现与具体应用的灵活度相当大):
6.9.1.1、通过一个类的全限定名获取定义这个类的二进制流可以从jar包、ear包、war包中获取,可以从网络中获取(Applet),可以运行时生成(动态代理),可以通过其它文件生成(Jsp)等。6.9.1.2、将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。6.9.1.3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
6.9.2、链接
链接就是把load进来的class合并到JVM的运行时状态中。可以把它分成三个主要阶段:
6.9.2.1、校验
对二进制字节码的格式进行校验,以确保格式正确、行为正确。这一阶段主要是为了确保Class文件的字节流中包含的信息复合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要验证过程包括:6.9.2.1.1、文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。6.9.2.1.2、元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。6.9.2.1.3、字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。6.9.2.1.4、符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生6.9.2.2、准备
准备类中定义的字段、方法和实现接口所必需的数据结构。比如会为类中的静态变量赋默认值(int等:0, reference:null, char:'\u0000')。准备阶段正式为类变量分配内存并设置初始值。这里的初始值并不是初始化的值,而是数据类型的默认零值。这里提到的类变量是被static修饰的变量,而不是实例变量。关于准备阶段为类变量设置零值的唯一例外就是当这个类变量同时也被final修饰,那么在编译时,就会直接为这个常量赋上目标值。6.9.2.3、解析
装入类所引用的其他所有类,虚拟机将常量池中的符号引用替换为直接引用。可以用许多方式引用类:超类、接口、字段、方法签名、方法中使用的本地变量。
6.9.3、初始化
在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段,则是根据程序员通过程序的主观计划区初始化类变量和其他资源。类初始化前,它的直接父类一定要先初始化(递归),但它实现的接口不需要先被初始化。类似的,接口在初始化前,父接口不需要先初始化。
有且只有4种情况必须立即对类进行初始化:
6.9.3.1、遇到new(使用new关键字实例化对象)、getstatic(获取一个类的静态字段,final修饰符修饰的静态字段除外)、putstatic(设置一个类的静态字段,final修饰符修饰的静态字段除外)和invokestatic(调用一个类的静态方法)这4条字节码指令时,如果类还没有初始化,则必须首先对其初始化
6.9.3.2、使用java.lang.reflect包中的方法对类进行反射调用时,如果类还没有初始化,则必须首先对其初始化
6.9.3.3、当初始化一个类时,如果其父类还没有初始化,则必须首先初始化其父类6.9.3.4、当虚拟机启动时,需要指定一个主类(main方法所在的类),虚拟机会首选初始化这个主类除了上面这4种方式,所有引用类的方式都不会触发初始化,称为被动引用。如:通过子类引用父类的静态字段,不会导致子类初始化;通过数组定义来引用类,
不会触发此类的初始化;引用类的静态常量不会触发定义常量的类的初始化,因为常量在编译阶段已经被放到常量池中了。
6.10 扩展
7、JVM内存模型
7.1、程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
7.2、虚拟机栈
线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
7.2.1、局部变量表
7.2.1.1、局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。7.2.1.2、局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
7.2.1.3、虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
7.2.1.4、 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段
7.2.2、操作数栈
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作——压栈和出栈—来访问的。
7.2.3、动态连接
虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
7.2.4、返回地址
7.2.5、异常
7.3、本地方法栈:
本地方法栈则是为虚拟机使用到的Native 方法服务
7.4、堆
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
7.4.1、如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;
7.4.1.1、新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
7.4.1.2、老年代:用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况: 1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。 2、大的数组对象,且数组中无引用外部对象。老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
7.5、方法区
7.5.1、方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。
7.5.2、简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载(后的结果就是由.class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储如下信息:类型信息、字段信息、方法信息、其他信息7.5.3、JVM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer,和floating point常量)和对类型,域和方法的符号引用。池中的数据项象数组项一样,是通过索引访问的。7.5.4、方法区主要有以下几个特点:
7.5.4.1、方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。
7.5.4.2、方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。
7.5.4.3、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
8、JVM垃圾回收机制
8.1、在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC。
8.2、引用计数是最简单直接的一种方式,这种方式在每一个对象中增加一个引用的计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。
优点:简单,直接,不需要暂停整个应用
缺点:1.需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作;2.不能处理循环引用的问题
8.3、根搜索算法
8.3.1、通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。
在java语言中,可作为GCRoot的对象包括以下几种对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。b.方法区中的类静态属性引用的对象。c.方法区中的常量引用的对象。d.本地方法栈中JNI本地方法的引用对象。
判断无用的类:
(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。(2).加载该类的类加载器已经被回收。(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。
8.4、四种引用
GC在收集一个对象的时候会判断是否有引用指向对象,在JAVA中的引用主要有四种:
8.4.1、强引用:
是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
8.4.2、软引用:
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。8.4.3、弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。8.4.4、虚引用
”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用于检测对象是否已经从内存中删除,跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
8.5、策略:JVM中的垃圾收集策略
8.5.1、标记-清除算法
标记清除收集器停止所有的工作,从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。优点:1 解决循环引用的问题2 不需要编译器的配合,从而就不执行额外的指令缺点:1. 每个活跃的对象都要进行扫描,收集暂停的时间比较长。2.标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
8.5.2、复制算法
复制收集器将内存分为两块一样大小空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的空间满的时候,GC就会将活跃的对象复制到未使用的空间中去,原来不活跃的空间就变为了活跃的空间
优点:1 只扫描可以到达的对象,不需要扫描所有的对象,从而减少了应用暂停的时间缺点:1.需要额外的空间消耗,某一个时刻,总是有一块内存处于未使用状态2.复制对象需要一定的开销复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当空间存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。
8.5.3、标记-整理算法
标记整理收集器汲取了标记清除和复制收集器的优点,它分两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的的对象复制到堆得底部该算法极大的减少了内存碎片,并且不需要像复制算法一样需要两倍的空间。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
8.5.4、分代回收算法
垃圾分代回收算法(GenerationalCollecting)基于对对象生命周期分析后得出的垃圾回收算法。
内存主要被分为三块,新生代、旧生代、持久代。三代的特点不同,造就了他们所用的GC算法不同,新生代适合那些生命周期较短,频繁创建及销毁的对象,旧生代适合生命周期相对较长的对象,持久代在Sun HotSpot中就是指方法区(有些JVM中根本就没有持久代这中说法)
8.5.4.1、Young(年轻代、新生代 复制算法和标记-清除垃圾收集算法):JVM specification中的 Heap(堆)的一部份年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制旧生代。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。
新生代使用复制算法和标记-清除垃圾收集算法,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from(Survivor 0)和Survivor to(Survivor1)三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。
如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。
Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。
使用java虚拟机-Xmn参数可以指定新生代内存大小。
8.5.4.2、Tenured(年老代、旧生代 用标记-整理垃圾回收算法):JVMspecification中的 Heap(堆)的一部份年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。
Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。
java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。
8.5.4.3、Perm(持久代、永久代 标记-整理算法进行垃圾回收): JVM specification中的 Method area 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。java虚拟机内存中的方法区在SunHotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。
永久代也使用标记-整理算法进行垃圾回收,java虚拟机参数-XX:PermSize和-XX:MaxPermSize可以设置永久代的初始大小和最大容量
8.5.5、垃圾回收过程
1、首先当启动J2EE应用服务器时,JVM随之启动,并将JDK的类和接口,应用服务器运行时需要的类和接口以及J2EE应用的类和接口定义文件也及编译后的Class文件或JAR包中的Class文件装载到JVM的永久存储区。
2、在Eden区中创建JVM,应用服务器运行时必须的JAVA对象,创建J2EE应用启动时必须创建的JAVA对象;J2EE应用启动完毕,可对外提供服务。
3、JVM在Eden区根据用户的每次请求创建相应的JAVA对象,当Eden区的空间不足以用来创建新JAVA对象的时候,JVM的垃圾回收器执行对Eden区的垃圾回收工作,销毁那些不再被其他对象引用的JAVA对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他对象所引用的JAVA对象移动到Survivor0区。
4、如果Survivor0区有足够空间存放则直接放到Survivor0区;如果Survivor0区没有足够空间存放,则JVM的垃圾回收器执行对Survivor0区的垃圾回收工作,销毁那些不再被其他对象引用的JAVA对象,并将那些被其他对象所引用的JAVA对象移动到Survivor1区。
5、如果Survivor1区有足够空间存放则直接放到Survivor0区;如果Survivor0区没有足够空间存放,则JVM的垃圾回收器执行对Survivor1区的垃圾回收工作,销毁那些不再被其他对象引用的JAVA对象,并将那些被其他对象所引用的JAVA对象移动到老年代区。
6、如果老年代区有足够空间存放则直接放到养老区;如果养老区没有足够空间存放,则JVM的垃圾回收器执行对老年代区的垃圾回收工作,销毁那些不再被其他对象引用的JAVA对象,并保留那些被其他对象所引用的JAVA对象。如果到最后老年代区,Survivor1区,Survivor0区和Eden区都没有空间的话,则JVM会报告“JVM堆空间溢出(java.lang.OutOfMemoryError: Java heap space)”,也即是在堆空间没有空间来创建对象。这就是JVM的内存分区管理,相比不分区来说;一般情况下,垃圾回收的速度要快很多;因为在没有必要的时候不用扫描整片内存而节省了大量时间。
8.5.6、对象的空间分配和晋升
(1)对象优先在Eden上分配(2)大对象直接进入老年代虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。因为新生代采用的是标记-复制策略,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。(3)长期存活的对象将进入老年代对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。
8.5.7、触发:何时开始GC
Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。而Full GC(老年代回收,一般伴随一次MinorGC)则有几种触发条件:(1)老年代空间不足(2)PermSpace(永久代)空间不足(3)统计得到的MinorGC晋升到老年代的平均大小大于老年代的剩余空间这里注意一点:PermSpace并不等同于方法区,只不过是HotspotJVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区
8.5.8.1、新生代串行收集器
java内存模型(Java Memory Model,JMM)
1.1、原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
1.2、可见性volatile
对于可见性,Java提供了volatile关键字来保证可见性。 volatile关键字
1.2.1、 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。1.2.2、而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。1.2.3、通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
1.3、有序性
1.3.1、在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
1.3.2、在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
1.3.3、Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
happens-before原则(先行发生原则)
jdk/bin工具
extcheck.exe
1、主要用于检测指定的jar文件与Java SDK安装的任何扩展之间是否存在版本冲突。2、extcheck工具通过将指定jar文件中清单文件(manifest)的Specification-title和Specification-version头信息与扩展目录中所有的jar文件对应的头信息进行比较,从而检测是否存在版本冲突。3、JDK(或JRE)默认的扩展安装目录为jre/lib/ext。extcheck工具通过使用与方法java.lang.Package.isCompatibleWith相同的方式来比较版本号。如果没有检测出冲突问题,则返回代码为0。jar.exe
1、jar文件管理工具,主要用于打包压缩、解压jar文件。2、JAR(Java Archive),意即Java归档文件。JAR 文件实际上就是一个或多个Java类字节码文件(.class)的打包压缩文件,采用常见的ZIP压缩算法(这意味着我们也可以直接使用支持ZIP算法的文件管理工具处理JAR文件)。JAR 文件主要用于压缩和发布Java字节码文件,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。3、在 JAR 中包含特殊的文件,如 manifests和部署描述符,用于指示工具如何处理特定的 JAR。4、jar命令行的用法为:4.1、jar {ctxui}[vfm0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files ...4.2、{ctxui}表示必需的参数选项,你必须使用其中的一个或多个参数选项(如果允许同时使用的话)。[4.3、[vfm0Me]表示可选的参数选项,你可以根据需要自行决定是否使用其中的参数选项。4.4、如果使用参数f,就需要指定jar-file(JAR文件);使用参数m,就需要指定manifest-file(清单文件);使用参数e,就需要指定entry-point(入口点)。这些参数的指定顺序也应该和文件/入口点的指定顺序保持一致。
方法一:jar cvf jar文件名称 一个或多个class文件方法二:打包当前目录下的所有文件为 jar cvf jar文件名称 .
7.1、可运行的jar文件,实际上是一个可以直接被JVM运行的JAR格式的Java应用程序。众所周知,Java应用程序的启动入口就是某个类中的static main方法。因此,我们需要通过jar文件中的清单文件META-INF/MANIFEST.MF来指定main入口类的完全名称
7.2、方法一
7.2.1、在打包jar文件时指定入口点(也就是main入口类)
#将projects/softown目录下的所有文件打包为softown.jar,并且设定入口点为test.Hello(类名)jar cvfe softown.jar test.Hello -C projects/softown .7.3、方法二7.3.1、我们也可以手动创建或更改MANIFEST.MF文件,在其中添加如下一行:
Main-Class:test.Hello
7.3.2、然后将该文件以清单文件的形式添加到jar文件中:
#将projects/softown目录下的所有文件打包为softown.jar,并使用指定的MANIFEST.MF作为清单文件jar cvfm softown.jar MANIFEST.MF -C projects/softown .7.3.3、运行jar文件
java -jar jar文件名
#将会列出softown.jar文件中的所有文件列表jar tf softown.jar
#将会解压softown.jar文件到当前工作目录jar xf softown.jar
MANIFEST.MF文件
1、JAR文件的用途非常广泛,其用途包括电子签名、版本控制、包封装等。那么,是谁什么给了它如此神通广大的能力?答案就是JAR的清单文件MANIFEST.MF。2、MANIFEST.MF文件是一个特殊文件,位于JAR文件内部的META-INF目录,其中保存了与JAR文件中封装的文件有关的信息(一般称为"元数据")。3、MANIFEST.MF文件就是由一对或多对"属性名:属性值"构成的(一对占据一行)。4、Sealed:false 定义JAR文件是否密封,值为"true"或"false"。什么叫做密封?密封 JAR 文件中的一个包意味着在这个包中定义的所有类都必须在同一个 JAR 文件中找到。jarsigner
1、jarsigner,jar文件的签名和验证工具,主要用于为jar文件生成签名,并且验证已签名的jar文件的签名信息javaw.exe
Java运行工具,用于运行.class字节码文件或.jar文件,但不会显示控制台输出信息,适用于运行图形化程序。
java.exe
java 包名.类名
java -jar jar文件路径
java和javaw工具的用法一直,不过它们也有一些不同之处。与java相比,javaw工具将屏蔽Java程序的控制台信息输出,如果你不希望出现命令提示符窗口,你可以使用javaw来运行该Java程序。
javac.exe
1、Java源代码编译工具2、-encoding参数命令,用于指定Java源文件所使用的字符编码,例如"UTF-8"、"GB2312"等,这样可以避免Java源文件的中文乱码问题3、-source和-target参数-source和-target参数命令,分别用于指定编译前的源文件的兼容版本和编译后的字节码文件的最低兼容版本。简而言之,-source参数用于指定使用什么版本的编译器来编译源文件;-target参数用于指定编译出来的字节码文件最低支持在什么版本的Java虚拟机上运行。
javadoc.exe
1、javadoc是一个根据Java源代码中的文档注释生成HTML格式的API文档的工具。2、javadoc [-d 文档存放目录] -author -version cn/softown/Hello.java cn/softown/World.javajavah.exe
1、javah,是一个C头文件和存根文件生成器。我们可以使用javah从一个Java类生成C头文件和C源代码文件。这些文件提供了Java和C代码进行交互的纽带。(主要用于JNI开发领域)。javap.exe
1、javap,是JDK自带的反汇编工具,用于将Java字节码文件反汇编为Java源代码。2、实际上等同于javap -package Personjavap Person
javap -c Person
java-rmi.exe
javaws.exe
1、javaws,是Java Web Start的简写。Java Web Start 是一个Java软件,主要用于管理Java应用程序,例如在线下载、安装、运行、更新等。jcmd.exe
1、jcmd,用于向正在运行的JVM发送诊断信息请求。2、jcmd的命令行用法有如下几种:jcmd [ options ]jcmd [ pid | main-class ] PerfCounter.printjcmd [ pid | main-class ] command [ arguments ]jcmd [ pid | main-class ] -f filejcmd 21036 \"123\" PerfCounter.print
2.1、pid:接收诊断命令请求的进程ID。该进程必须是一个Java进程。你可以使用jps或jcmd来获取运行于当前计算机上的Java进程列表。2.2、main-class:接收诊断命令请求的进程的main类。匹配进程时,main类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的Java进程共享同一个main类,诊断命令请求将会发送到所有的这些进程中。
2.3、command [arguments]:接收诊断命令请求的进程的main类。匹配进程时,main类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的Java进程共享同一个main类,诊断命令请求将会发送到所有的这些进程中。注意: 如果任何参数含有空格,你必须使用英文的单引号或双引号将其包围起来。 此外,你必须使用转义字符来转移参数中的单引号或双引号,以阻止操作系统shell处理这些引用标记。当然,你也可以在参数两侧加上单引号,然后在参数内使用双引号(或者,在参数两侧加上双引号,在参数中使用单引号)。2.4、Perfcounter.print:打印目标Java进程上可用的性能计数器。性能计数器的列表可能会随着Java进程的不同而产生变化。2.5、-f file:从文件file中读取命令,然后在目标Java进程上调用这些命令。在file中,每个命令必须写在单独的一行。以"#"开头的行会被忽略。当所有行的命令被调用完毕后,或者读取到含有stop关键字的命令,将会终止对file的处理。
3.1、使用不带参数或参数-l,jcmd 将打印运行的Java进程的列表信息,其中包括进程ID、main类和它们的命令行参数。3.2、如果在命令行中指定进程ID,jcmd 将向该进程ID的Java进程发送至诊断命令请求。3.3、如果在命令行中指定main类,jcmd 将向main类包含该子字符串的所有Java进程发送诊断命令请求。3.4、如果使用PerfCounter.print 参数,jcmd 打印目标Java进程上可用的性能计数器。3.5、如果使用-f 选项参数,jcmd 将向目标Java进程发送file中存储的诊断命令。
JConsole.exe
1、JConsole是一个遵循JMX(Java Management Extensions)规范的图形用户界面的监测工具,主要用于监控并提供运行于Java平台的应用程序的性能和资源占用信息。2、远程监控使用????????????jdb.exe
1、jdb,意即Java Debugger,主要用来帮助我们查找并修复Java程序中的bug。jhat.exe
1、jhat(Java Heap Analysis Tool),是JDK自带的Java堆内存分析工具。2、jhat命令解析一个java heap dump文件并启动一个web服务器。jhat使得你可以使用自己喜欢的web浏览器浏览heap dump。jhat支持预先设计的查询语言(例如'show all instances of a known class "Foo"')以及OQL(对象查询语言) -- 一种用于查询heap dump的、类似于SQL的查询语言。你可以通过jhat显示的OQL帮助页面查看关于OQL的帮助信息。在使用默认端口的情况下,OQL的帮助信息位于http://localhost:7000/oqlhelp/3、有以下几种方式可以生成一个Java heap dump:3.1、使用jmap -dump选项获取一个运行时的heap dump。3.2、使用jconsole选项通过运行时的HotSpotDiagnosticMXBean获取一个heap dump。3.3、通过指定-XX:+HeapDumpOnOutOfMemoryErrorVM选项,在抛出OutOfMemoryError错误时,将会生成一个heap dump。3.4、使用hprof。
2.1、heap-dump-file:指定用于浏览的Java二进制heap dump文件。对于一个包含多个heap dump的dump文件,你可以在文件名称后面追加"#<number>"来指定文件中的某个dump
jmap.exe
1、jmap是JDK自带的工具软件,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。2、 jmap -dump:format=b,file=文件名 进程IDjinfo.exe
1、jinfo(Java Configuration Information),主要用于查看指定Java进程(或核心文件、远程调试服务器)的Java配置信息。2、jinfo 进程IDjmc.exe
jps.exe
1、jps(JVM Process Status Tool),主要用于显示Java进程的状态信息。jstack.exe
1、jstack,是一个堆栈跟踪工具,主要用于打印指定Java进程、核心文件或远程调试服务器的Java线程的堆栈跟踪信息。jvisualvm.exe
1、jvisualvm,即Java VisualVM,是一个图形化界面的Java虚拟机监控工具。jvisualvm主要提供在Java虚拟机上运行的Java应用程序的详细信息。2、使用jvisualvm,你可以详细查看虚拟机中每个Java应用程序,甚至每个线程、每个类、每个实例的相关信息,包括堆栈、字段、类型、方法值,以及启动时间、方法调用次数等。native2ascii.exe
1、本地编码到ASCII编码的转换器(Native-to-ASCII Converter),用于"任意受支持的字符编码"和与之对应的"ASCII编码和(或)Unicode转义"之间的相互转换。pack200.exe
1、JAR文件打包压缩工具,它可以利用Java类特有的结构,对普通JAR文件进行高效压缩,以便于能够更快地进行网络传输unpack200.exe
1、JAR文件解压工具,将一个由pack200打包的文件解压提取为JAR文件。多线程
1、继承Thread类
1.1、在java.lang包中定义, 继承Thread类必须重写run()方法
2、实现Runnable接口
2.1、通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。
3、线程的状态
3.1、新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();3.2、就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;3.3、运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;3.4、阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
3.4.1、等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;3.4.2、同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;3.4.3、其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
3.5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。
4、sleep和wait的区别:
4.1、sleep是Thread类的方法,wait是Object类中定义的方法.4.2、Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep不会让线程释放锁.4.3、Thread.sleep和Object.wait都会暂停当前的线程. 操作系统会将执行时间分配给其它线程. 区别是, 调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.
5、上下文切换
对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
6、线程的常用方法
6.1、currentThread:可以返回代码段正在被哪个线程调用的信息。
6.2、sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
6.2.1、sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。6.2.2、sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。6.2.3、当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
6.3、yield()
6.3.1、yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程6.3.2、yield方法不会释放锁6.3.3、yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级或优先级更高的线程有获取CPU执行时间的机会。6.3.4、用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
7、对象方法
7.1、start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。7.2、run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。7.3、getId()的作用是取得线程的唯一标识7.4、isAlive()的功能是判断当前线程是否处于活动状态
7.4.1、活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
7.5、join()的作用是等待线程对象销毁7.6、getPriority和setPriority用来获取和设置线程优先级。
7.6.1、线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。7.6.2、继承性:比如A线程启动B线程,则B线程的优先级与A是一样的。7.6.3、规则性:高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。7.6.4、随机性:优先级较高的线程不一定每一次都先执行完
7.7、setDaemon和isDaemon用来设置线程是否成为守护线程和判断线程是否是守护线程。
7.7.1、Daemon的作用是为其他线程的运行提供服务7.7.2、守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
8、停止线程
8.1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止8.2、使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。8.3、使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
9、wait()- notify()-notifyAll()线程通信
9.1、wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。9.2、notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。9.3、notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
10、Synchronized
synrhronized使用广泛。其应用层的语义是可以把任何一个非null对象作为"锁",当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁;当synchronized作用于某一个对象实例时,锁住的便是对应的代码块
通过 javap -c class路径 反编译classmonitorenter和monitorexit指令来实现 同步代码块monitorenter
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
ACC_SYNCHRONIZED
方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
简单的加锁机制
机制:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出syncronized块时,计数器减1,当计数器为0时,锁被释放(这就保证了锁是可重入的,不会发生死锁的情况)。偏向锁、轻量锁、重量锁
偏向锁,简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。但是偏向锁也有一个问题,就是当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态。JAVA虚拟机锁机制的升级流程
每一个线程在准备获取共享资源时:
第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord,
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
CAS和ABA问题
volatile关键字
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是 用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
volatile关键字禁止指令重排序有两层意思:
2.1、当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;2.2、在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
4.1、“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
4.2、lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
4.2.1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
4.2.2、它会强制将对缓存的修改操作立即写入主存;4.2.3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
并发编程中的三个概念
3.1、什么是指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
Java 并发工具包
1、阻塞队列 BlockingQueue
1.1、BlockingQueue 接口表示一个线程放入和提取实例的队列
1.2、无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个 NullPointerException。1.3、可以访问到 BlockingQueue 中的所有元素,而不仅仅是开始和结束的元素。比如说,你将一个对象放入队列之中以等待处理,但你的应用想要将其取消掉。那么你可以调用诸如 remove(o) 方法来将队列之中的特定对象进行移除。但是这么干效率并不高(译者注:基于队列的数据结构,获取除开始或结束位置的其他对象的效率不会太高),因此你尽量不要用这一类的方法,除非你确实不得不那么做
1.4、BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同
1.4.1、抛异常(add/remove/element):如果试图的操作无法立即执行,抛一个异常。1.4.2、特定值(offer/poll/peek):如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。1.4.3、阻塞(put/take):如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。1.4.4、超时(offer/poll):如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
2、数组阻塞队列 ArrayBlockingQueue
2.1、ArrayBlockingQueue 类实现了 BlockingQueue 接口。2.2、ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。2.3、ArrayBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。
3、延迟队列 DelayQueue
3.1、DelayQueue 实现了 BlockingQueue 接口。3.2、DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口3.3、DelayQueue 将会在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take被调用的时候被释放掉。
3.4、Delayed 接口也继承了 java.lang.Comparable 接口,这也就意味着 Delayed 对象之间可以进行对比。这个可能在对 DelayQueue 队列中的元素进行排序时有用,因此它们可以根据过期时间进行有序释放。
4、链阻塞队列 LinkedBlockingQueue
4.1、LinkedBlockingQueue 类实现了 BlockingQueue 接口。4.2、LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。4.3、LinkedBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个
5、具有优先级的阻塞队列 PriorityBlockingQueue
5.1、PriorityBlockingQueue 类实现了 BlockingQueue 接口。5.2、PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。5.3、所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。注意 PriorityBlockingQueue 对于具有相等优先级(compare() == 0)的元素并不强制任何特定行为。 同时注意,如果你从一个 PriorityBlockingQueue 获得一个Iterator 的话,该 Iterator 并不能保证它对元素的遍历是以优先级为序的。
6、同步队列 SynchronousQueue
6.1、SynchronousQueue 类实现了 BlockingQueue 接口。6.2、SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素
7、阻塞双端队列 BlockingDeque
7.1、BlockingDeque 接口表示一个线程安放入和提取实例的双端队列
7.1.1、BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列7.1.2、BlockingDeque 接口继承自 BlockingQueue 接口。这就意味着你可以像使用一个 BlockingQueue 那样使用 BlockingDeque。
7.2、链阻塞双端队列 LinkedBlockingDeque
7.2.1、LinkedBlockingDeque 类实现了 BlockingDeque 接口。7.2.2、LinkedBlockingDeque 是一个双端队列,在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。
8、并发 Map(映射) ConcurrentMap
8.1、ConcurrentMap 接口表示了一个能够对别人的访问(插入和提取)进行并发处理的 java.util.Map,除了从其父接口 java.util.Map 继承来的方法之外还有一些额外的原子性方法。8.2、ConcurrentHashMap
8.2.1、ConcurrentHashMap 和 java.util.HashTable 类很相似,但 ConcurrentHashMap 能够提供比 HashTable 更好的并发性能。在你从中读取对象的时候ConcurrentHashMap 并不会把整个 Map 锁住。此外,在你向其中写入对象的时候,ConcurrentHashMap 也不会锁住整个 Map。它的内部只是把 Map 中正在被写入的部分进行锁定
8.2.2、另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程的同时使用
9、并发导航映射 ConcurrentNavigableMap
9.1、ConcurrentNavigableMap 是一个支持并发访问的 java.util.NavigableMap,它还能让它的子 map 具备并发访问的能力。所谓的 "子 map" 指的是诸如 headMap(),subMap(),tailMap() 之类的方法返回的 map---- 9.2、headMap(T toKey) 方法返回一个包含了小于给定 toKey 的 key 的子 map。---- 9.3、tailMap(T fromKey) 方法返回一个包含了不小于给定 fromKey 的 key 的子 map。---- 9.4、subMap() 方法返回原始 map 中,键介于 from(包含) 和 to (不包含) 之间的子 map。
10、 闭锁 CountDownLatch
10.1、CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。10.2、CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这一数量就减一。通过调用 await() 方法,线程可以阻塞等待这一数量到达零。
--11、栅栏 CyclicBarrier
11.1、CyclicBarrier 类是一种同步机制,它能够对处理一些算法的线程实现同步。换句话讲,它就是一个所有线程必须等待的一个栅栏,直到所有线程都到达这里,然后所有线程才可以继续做其他事情。11.2、在创建一个 CyclicBarrier 的时候你需要定义有多少线程在被释放之前等待栅栏
CyclicBarrier barrier = new CyclicBarrier(2);
11.3、线程等待一个 CyclicBarrier
barrier.await();
当然,你也可以为等待线程设定一个超时时间。等待超过了超时时间之后,即便还没有达成 N 个线程等待 CyclicBarrier 的条件,该线程也会被释放出来。以下是定义超时时间示例:barrier.await(10, TimeUnit.SECONDS);满足以下任何条件都可以让等待 CyclicBarrier 的线程释放:最后一个线程也到达 CyclicBarrier(调用 await())当前线程被其他线程打断(其他线程调用了这个线程的 interrupt() 方法)其他等待栅栏的线程被打断其他等待栅栏的线程因超时而被释放外部线程调用了栅栏的 CyclicBarrier.reset() 方法
11.4、barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
12、交换机 Exchanger
12.1、Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。12.2、交换对象的动作由 Exchanger 的两个 exchange() 方法的其中一个完成
13、信号量 Semaphore
13.1、Semaphore 类是一个计数信号量。这就意味着它具备两个主要方法:acquire()、release()13.2、计数信号量由一个指定数量的 "许可" 初始化。每调用一次 acquire(),一个许可会被调用线程取走。每调用一次 release(),一个许可会被返还给信号量。因此,在没有任何 release() 调用时,最多有 N 个线程能够通过 acquire() 方法,N 是该信号量初始化时的许可的指定数量。这些许可只是一个简单的计数器13.3、信号量主要有两种用途:
保护一个重要(代码)部分防止一次超过 N 个线程进入。
在两个线程之间发送信号。
13.4、公平
没有办法保证线程能够公平地可从信号量中获得许可。也就是说,无法担保掉第一个调用 acquire() 的线程会是第一个获得一个许可的线程。如果第一个线程在等待一个许可时发生阻塞,而第二个线程前来索要一个许可的时候刚好有一个许可被释放出来,那么它就可能会在第一个线程之前获得许可。如果你想要强制公平,Semaphore 类有一个具有一个布尔类型的参数的构造子,通过这个参数以告知 Semaphore 是否要强制公平。强制公平会影响到并发性能,所以除非你确实需要它否则不要启用它
--14、执行器服务 ExecutorService
14.1、ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的ExecutorService 实现就是一个线程池实现
14.2、ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
ThreadPoolExecutorScheduledThreadPoolExecutor
14.3、execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。
14.4、submit(Runnable)submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕14.5、submit(Callable)submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。14.6、invokeAny()invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 - 只能表明其中一个已执行结束。14.7、invokeAll()invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个Callable 的执行结果。14.8、使用完 ExecutorService 之后你应该将其关闭,以使其中的线程不再运行。比如,如果你的应用是通过一个 main() 方法启动的,之后 main 方法退出了你的应用,如果你的应用有一个活动的 ExexutorService 它将还会保持运行。
ExecutorService 里的活动线程阻止了 JVM 的关闭。
要终止 ExecutorService 里的线程你需要调用 ExecutorService 的 shutdown() 方法。ExecutorService 并不会立即关闭,但它将不再接受新的任务,而且一旦所有线程都完成了当前任务的时候,ExecutorService 将会关闭。在 shutdown() 被调用之前所有提交给 ExecutorService 的任务都被执行。如果你想要立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。可能它们被停止了,也可能已经执行结束。
--15、线程池执行者 ThreadPoolExecutor
15.1、ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用其内部池中的线程执行给定任务(Callable 或者 Runnable)。15.2、hreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
corePoolSizemaximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于 corePoolSize,一个新的线程将被创建,即使池中可能尚有空闲线程。如果内部任务队列已满,而且有至少 corePoolSize 正在运行,但是运行线程的数量低于 maximumPoolSize,一个新的线程将被创建去执行该任务。线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:
corePoolSize: 线程池维护线程的最少数量ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
maximumPoolSize:线程池维护线程的最大数量
keepAliveTime: 线程池维护线程所允许的空闲时间
unit: 线程池维护线程所允许的空闲时间的单位
workQueue: 线程池所使用的缓冲队列
handler: 线程池对拒绝任务的处理策略
一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。
当一个任务通过execute(Runnable)方法欲添加到线程池时:
如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
也就是:处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue
handler有四个选择:
ThreadPoolExecutor.AbortPolicy() :抛出java.util.concurrent.RejectedExecutionException异常
ThreadPoolExecutor.CallerRunsPolicy() :重试添加当前的任务,他会自动重复调用execute()方法
ThreadPoolExecutor.DiscardOldestPolicy() :抛弃旧的任务
ThreadPoolExecutor.DiscardPolicy() :抛弃当前的任务
--16、定时执行者服务 ScheduledExecutorService
16.1、ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给ScheduledExecutorService 的那个线程执行
17、使用 ForkJoinPool 进行分叉和合并
17.1、ForkJoinPool 在 Java 7 中被引入。ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。ForkJoinPool 也在java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool17.2、提交任务到 ForkJoinPool就像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 "行动"),另一种是有返回值的(一个"任务")。这两种类型分别由 RecursiveAction 和 RecursiveTask 表示。17.3、RecursiveAction 是一种没有任何返回值的任务。17.4、RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果
18、锁 Lock
18.1、java.util.concurrent.locks.Lock 是一个类似于 synchronized 块的线程同步机制。但是 Lock 比 synchronized 块更加灵活、精细。18.2、首先创建了一个 Lock 对象。之后调用了它的 lock() 方法。这时候这个 lock 实例就被锁住啦。任何其他再过来调用 lock() 方法的线程将会被阻塞住,直到锁定 lock 实例的线程调用了 unlock() 方法。最后 unlock() 被调用了,lock 对象解锁了,其他线程可以对它进行锁定了18.3、Lock 和 synchronized 代码块的主要不同点
18.3.1、synchronized 代码块不能够保证进入访问等待的线程的先后顺序。18.3.2、你不能够传递任何参数给一个 synchronized 代码块的入口。因此,对于 synchronized 代码块的访问等待设置超时时间是不可能的事情。18.3.3、synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。
18.4、Lock 的方法
18.4.1、lock() 将 Lock 实例锁定。如果该 Lock 实例已被锁定,调用 lock() 方法的线程将会阻塞,直到 Lock 实例解锁。18.4.2、lockInterruptibly() 方法将会被调用线程锁定,除非该线程被打断。此外,如果一个线程在通过这个方法来锁定 Lock 对象时进入阻塞等待,而它被打断了的话,该线程将会退出这个方法调用。18.4.3、tryLock() 方法试图立即锁定 Lock 实例。如果锁定成功,它将返回 true,如果 Lock 实例已被锁定该方法返回 false。这一方法永不阻塞。
18.4.4、tryLock(long timeout, TimeUnit timeUnit) 的工作类似于 tryLock() 方法,除了它在放弃锁定 Lock 之前等待一个给定的超时时间之外。18.4.5、unlock() 方法对 Lock 实例解锁。一个 Lock 实现将只允许锁定了该对象的线程来调用此方法。其他(没有锁定该 Lock 对象的线程)线程对 unlock()方法的调用将会抛一个未检查异常(RuntimeException)。
--19、 读写锁 ReadWriteLock
19.1、java.util.concurrent.locks.ReadWriteLock 读写锁是一种先进的线程锁机制。它能够允许多个线程在同一时间对某特定资源进行读取,但同一时间内只能有一个线程对其进行写入。19.2、ReadWriteLock 锁规则19.2.1、读锁:如果没有任何写操作线程锁定 ReadWriteLock,并且没有任何写操作线程要求一个写锁(但还没有获得该锁)。因此,可以有多个读操作线程对该锁进行锁定。19.2.2、写锁:如果没有任何读操作或者写操作。因此,在写操作的时候,只能有一个线程对该锁进行锁定。
20、原子性布尔 AtomicBoolean
20.1、AtomicBoolean 类为我们提供了一个可以用原子方式进行读和写的布尔值,它还拥有一些先进的原子性操作,比如 compareAndSet()。AtomicBoolean 类位于java.util.concurrent.atomic 包,完整类名是为 java.util.concurrent.atomic.AtomicBoolean。
21、原子性整型 AtomicInteger
22、原子性长整型 AtomicLong
23、原子性引用型 AtomicReference
atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
锁
@CallerSensitive
Spring 事务
1、事物传播行
1.1、REQUIRED:如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)1.2、NOT_SUPPORTED:容器不为这个方法开启事务1.3、REQUIRES_NEW:不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务1.4、MANDATORY:必须在一个已有的事务中执行,否则抛出异常1.5、NEVER:必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)1.6、SUPPORTS:如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
2、事务隔离级别
2.1、READ_UNCOMMITTED:读取未提交数据(会出现脏读, 不可重复读) 基本不使用2.2、READ_COMMITTED:读取已提交数据(会出现不可重复读和幻读)2.3、REPEATABLE_READ:可重复读(会出现幻读)2.4、SERIALIZABLE:串行化
3、MYSQL默认级别:
默认为REPEATABLE_READ级别
4、SQLSERVER默认级别:
5、脏读 :
6、不可重复读 :
7、幻读 :
@Transactional事务几点注意
A. 一个功能是否要事务,必须纳入设计、编码考虑。不能仅仅完成了基本功能就ok。B. 如果加了事务,必须做好开发环境测试(测试环境也尽量触发异常、测试回滚),确保事务生效。
C. 以下列了事务使用过程的注意事项,请大家留意。
1. 不要在接口上声明@Transactional ,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。2.不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。3.使用了@Transactional的方法,对同一个类里面的方法调用, @Transactional无效。比如有一个类Test,它的一个方法A,A再调用Test本类的方法B(不管B是否public还是private),但A没有声明注解事务,而B有。则外部调用A之后,B的事务是不会起作用的。(经常在这里出错)4.使用了@Transactional的方法,只能是public,@Transactional注解的方法都是被外部其他类调用才有效,故只能是public。道理和上面的有关联。故在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但事务无效。5.经过在ICORE-CLAIM中测试,效果如下:
A.抛出受查异常XXXException,事务会回滚。B.抛出运行时异常NullPointerException,事务会回滚。C.Quartz中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)D.异步任务中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)E.在action中加上@Transactional,不会回滚。切记不要在action中加上事务。F.在service中加上@Transactional,如果是action直接调该方法,会回滚,如果是间接调,不会回滚。(即上文3提到的)G.在service中的private加上@Transactional,事务不会回滚。
mySql存储引擎
1、MyISAM
不管是何种MyISAM表,目前它都不支持事务,行级锁和外键约束的功能
1.1、静态MyISAM
如果数据表中的各数据列的长度都是预先固定好的,服务器将自动选择这种表类型。因为数据表中每一条记录所占用的空间都是一样的,所以这种表存取和更新的效率非常高。当数据受损时,恢复工作也比较容易做。
1.2、动态MyISAM
如果数据表中出现varchar、xxxtext或xxxBLOB字段时,服务器将自动选择这种表类型。相对于静态MyISAM,这种表存储空间比较小,但由于每条记录的长度不一,所以多次修改数据后,数据表中的数据就可能离散的存储在内存中,进而导致执行效率下降。同时,内存中也可能会出现很多碎片。因此,这种类型的表要经常用optimize table 命令或优化工具来进行碎片整理
1.3、压缩MyISAM
以上说到的两种类型的表都可以用myisamchk工具压缩。这种类型的表进一步减小了占用的存储,但是这种表压缩之后不能再被修改。另外,因为是压缩数据,所以这种表在读取的时候要先时行解压缩
2、MyISAM Merge引擎
这种类型是MyISAM类型的一种变种。合并表是将几个相同的MyISAM表合并为一个虚表。常应用于日志和数据仓库。
3、InnoDB
InnoDB表类型可以看作是对MyISAM的进一步更新产品,它提供了事务、行级锁机制和外键约束的功能。
4、memory(heap)
这种类型的数据表只存在于内存中。它使用散列索引,所以数据的存取速度非常快。因为是存在于内存中,所以这种类型常应用于临时表中。
5、archive
这种类型只支持select 和 insert语句,而且不支持索引。常应用于日志记录和聚合分析方面。
单例模式
线程安全单例
mysql
mySql存储引擎
联合索引
聚合索引
二分查找法
int[] srcArray = new int[] {1, 3, 5, 7, 8, 9};
int des = 9;
int low = 0;
int high = srcArray.length-1;
while(low <= high)
{
int middle = (low + high)/2;
if(des == srcArray[middle]) {
System.out.println(middle+" 结果");
break;
}else if(des <srcArray[middle]) {
high = middle - 1;
}else {
low = middle + 1;
}
}
mySql存储引擎
二叉树
数据结构
顺序存储: (顺序存储一般只用于完全二叉树)
非完全二叉树使用顺序存储时需要空出很多内存
二叉树遍历的概念
1、前序遍历
先输出当前结点的数据,再依次遍历输出左结点和右结点
最终结果: A B D G H C E I F
2、中序遍历
先遍历输出左结点,再输出当前结点的数据,再遍历输出右结点
最终结果: GDH B A E I C F
3、后序遍历
先遍历输出左结点,再遍历输出右结点,最后输出当前结点的数据
最终结果: G H D B I E F C A
平衡二叉树
在某些极端情况下,二叉查找树会退化为一个线性链表,查找效率的时间复杂度从O(log2n)降到了O(n)。为了规避这个缺点,引入了平衡二叉树。可以理解为平衡二叉树是基于二叉查找树的优化。
平衡二叉树在二叉查找树的基础之上增加了一个新的特性:每一个节点的左子树与右子树高度相差最多为1,这个高度的限制就可以避免出现树结构退化为线性链表的情况。
既然是要求每一个节点的左子树与右子树高度相差最多为1,那么就可以对节点对象引入一个属性值进行判断,这个属性值也就是平衡因子,其值为节点的左子树深度 - 右子树深度。在构建一棵平衡二叉树时,每当插入一个新的节点,都需要检查插入后是否破坏了树的平衡,如果失衡,那么则需要通过旋转去改变树的结构,使其再次平衡。
红黑树
一种二叉查找树,但在每个节点增加一个存储位表示结点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此,红黑树是一中弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,插入最多两次旋转,删除最多三次旋转,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。