初步探索Java虚拟机的奥秘
虽然从事Java开发已经有些年头了,但是对JVM底层的一些知识还是不甚了解。本系列文章初步介绍了“JVM内存模型”、“Java内存结构”、“Java对象模型”、类加载机制等等一些知识。部分内容参考了一些网上其它博主的文章或则官方文档,不过也会经过自己的验证和测试,并重新整理,文章最后会给出主要参考来源。
  • JVM——Java类的加载机制以及生命周期   

    一、什么是类的加载类的加载,就是将.class文件中的二进制数据读取到内存中,其实是存放于运行时的方法区。然后在堆中创建一个java.lang.Class对象(该对象封装了符合方法区定义的数据结构),同时向用户提供访问方法区内的数据结构的接口。JVM允许类加载器对某个类进行预加载。预加载过程发生错误(.class文件缺失或者有错误),类加载器需要在程序首次主动使用该类时报告错误(LinkageError),如果程序一直没有主动使用这个类,则类加载器不会报告这个错误。类加载器的加载方式:本地文件系统、网络.class文件、压缩包中的.class文件、Java源文件动态编译的.class文件。 二、类的生命周期类的生命周期包括加载、验证、准备、解析、初始化五个阶段。其中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的。而解析可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。 加载(Loading):查找对应目录中的类,并加载二进制字节流 1. 通过classpath和类的完整签名来读取对应的二进制字节流。 2. 将二进制字节流所代表的存储结构转化为方法区的数据结构。 3. 在堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区数据结构的入口。此阶段结束之后,JVM外部的二进制字节流文件就按照规范存储在方法区了,而且堆中存在一个可以访问这些方法区中的数据的对象。 -------- 连接-验证(Linking-Verification):确保被加载的类的正确性(符合当前虚拟机版本规范,且不会造成危害) 1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。 3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 4. 符号引用验证:判断引用合法性,以确保解析动作能正确执行。比如引用的类是否存在,引用的属性是否存在,访问权限是否允许等。 连接-准备(Linking-Preparation):为类的静态变量分配内存,并将其初始化为默认值该阶段为类的变量分配内存,并设置变量的初始值,这里的内存都是在方法区分配。这个阶段只负责类的静态变量(static),实例变量会在对象实例化的时候分配在堆中。需要注意的是,静态变量的初始值是数据类型对应的默认值(int是0,Integer是null),而不是显式定义的默认值。例如:javapublic static int data 2; //显式的定义初始化值那么data变量在准备阶段之后,初始值就是int的默认值0,而不是2。只有在初始化阶段,变量才会被赋值为2。在实际的程序中,只有同时被final和static修饰的变量,才会具有ConstantValue属性(且限于基本类型和String,因为常量池中只能引用基本类型和String的字面量)。在准备阶段,虚拟机会根据ConstantValue将这类变量初始化为显式定义的值,而不是在类构造器中初始化。 连接-解析(Linking-Resolution):把类中的符号引用转换为直接引用解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 -------- 初始化(Initialization):这里开始真正有我们的Java程序代码介入为类的静态变量(非final修饰)赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式: 1. 声明时指定初始值 2. 静态代码块为变量指定初始值注意:所有类变量初始化语句和静态代码块都会在编译时,被编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是clinit方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用。如果多线程去加载一个具有静态代码块的类,那么JVM会保证其clinit方法被正确的加锁、同步和执行。也就是说,第一个线程获得clinit的锁之后,如果其静态代码块执行时间很长,会阻塞这类的其它线程。而且,同一个类加载器下,一个类只会初始化一次,因此在执行clinit方法的线程结束之后,其它线程不会再次进入这个类的clinit方法。看下面的例子:javapublic class InitializationClass { public static Integer value 2; static { System.out.println(Thread.currentThread().getName() + " start to run static code"); //static是按顺序执行,因此value2已经赋值结束 System.out.println(Thread.currentThread().getName() + " value " + value); try { //模拟初始化的阻塞 TimeUnit.SECONDS.sleep(4); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " finish runing static code"); }}然后进行测试:javapublic class InitializationTest { public static void main(String[] args) { Runnable run new Runnable() { @Override public void run() { String name Thread.currentThread().getName(); System.out.println(name + " start run"); System.out.println(name + " value in runnable : " + InitializationClass.value); System.out.println(name + " finish run"); } }; new Thread(run).start(); new Thread(run).start(); }}控制台输出:bashThread-0 start runThread-1 start runThread-0 start to run static codeThread-0 value 2Thread-0 finish runing static codeThread-0 value in runnable : 2Thread-1 value in runnable : 2Thread-1 finish runThread-0 finish run从输出结果可以验证:1. 两个线程都正常进入run方法2. 线程Thread-0获得了InitializationClass的clinit方法的锁,并进入初始化3. 在Thread-0初始化完成(Sleep结束)之前,线程Thread-1阻塞在获取InitializationClass.value步骤上,也就是阻塞在clinit方法的竞争上4. Thread-0初始化结束之后,Thread-1并不会再次进入clinit进行初始化初始化步骤: 1. 判断类是否加载和验证,如果没有,则先进行加载和验证。 2. 判断该类的直接父类是否初始化,如果没有,则初始化父类。 3. 如果类中有初始化语句,则依次执行。只有发生类的主动使用,才会触发初始化: 1. 通过new关键字创建实例对象 2. 访问某个类(包括Interface)的静态变量(访问或者赋值) 3. 调用类的静态方法(2和3,如果是通过子类去访问父类的静态方法或者变量,不会触发子类的初始化,只会触发定义目标方法或者变量的父类的初始化) 4. 反射,例如Class.forName("com.wj.oom.Article"),默认会执行初始化 5. 初始化某个子类的时候,其父类会现行初始化 6. JVM启动时,被标明为启动类的类注意:ClassLoad.loadClass是不会触发类的初始化的。详见下面“JVM类加载机制”。 使用(Using)正常的代码逻辑中对类的使用 卸载(Unloading)JVM结束生命周期的几种情况: + System.exit() + 进程结束 + 程序异常导致的终止 + 操作系统异常导致JVM终止注意:JVM提供的三种类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。我在项目之外定义一个测试类,它的目录为/Users/wjyuian/projects/Arithmetics/Arithmetics/bin/,签名是analyze.one.ClassUnloadTest。javapublic class ClassUnloadTest { static { System.out.println("初始化 ClassUnloadTest"); }}然后进行测试:javapublic class TestClassLoadAndUnload { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InterruptedException { String path "/Users/wjyuian/projects/Arithmetics/Arithmetics/bin/"; //这个自定义类加载器,后面例子有 MyClassLoader loader new MyClassLoader(path); //此方法会加载,但不会初始化 Class clazz loader.loadClass("analyze.one.ClassUnloadTest"); System.out.println(clazz + "[" +clazz.hashCode() + "] / " + clazz.getClass() + "[" + clazz.getClass().hashCode() + "], " + clazz.getClassLoader()); //实例化,会触发初始化 System.out.println("实例化一个对象"); Object obj clazz.newInstance(); System.out.println(obj); System.out.println(); //分别实例对象引用置空,将类加载器引用置空,类对象引用置空 obj null; loader null; clazz null; //执行垃圾回收,卸载ClassUnloadTest类 Runtime.getRuntime().gc(); System.out.println(); System.out.println("置空之后,再加载一次ClassUnloadTest,并实例化"); //下面是我的猜测: //换一个类加载器实例,会导致类的重新加载;导致返回的Class不一致; //其实这个类在堆上的Class类型的对象还是同一个 //Class clazz2是 analyze.one.ClassUnloadTest //clazz2.getClass()返回的是ClassUnloadTest加载时在堆上初始化的java.lang.Class对象(Class) loader new MyClassLoader(path); Class clazz2 loader.loadClass("analyze.one.ClassUnloadTest"); System.out.println(clazz2.newInstance()); System.out.println(clazz2 + "[" +clazz2.hashCode() + "] / " + clazz2.getClass() + "[" + clazz2.getClass().hashCode() + "], " + clazz2.getClassLoader()); }}针对上面的猜测,可以参考文章一开始的定义: 类的加载,就是将.class文件中的二进制数据读取到内存中,其实是存放于运行时的方法区。然后在堆中创建一个java.lang.Class对象(该对象封装了符合方法区定义的数据结构),同时向用户提供访问方法区内的数据结构的接口。在执行main函数的时候,设置jvm参数-XX:+TraceClassLoading -XX:+TraceClassUnloading,这样就可以看到加载和卸载信息了:bash 省略了部分打印信息[Loaded java.io.ByteArrayOutputStream from /Library/Java/JavaVirtualMachines/jdk1.7.079.jdk/Contents/Home/jre/lib/rt.jar] 加载ClassUnloadTest类信息[Loaded analyze.one.ClassUnloadTest from JVMDefineClass]class analyze.one.ClassUnloadTest[537068416] / class java.lang.Class[635099371], jvmloading.MyClassLoader@667262b6实例化一个对象初始化 ClassUnloadTestanalyze.one.ClassUnloadTest@42d73fb7 卸载ClassUnloadTest; obj null;loader null;clazz null;Runtime.getRuntime().gc();这四句代码,缺少任意一句,都不会执行Unloading ClassUnloadTest 操作。 因为实例对象存在对Class的引用,Class与loader直接存在双向引用[Unloading class analyze.one.ClassUnloadTest]置空之后,再加载一次ClassUnloadTest,并实例化[Loaded analyze.one.ClassUnloadTest from JVMDefineClass]初始化 ClassUnloadTestanalyze.one.ClassUnloadTest@533790ebclass analyze.one.ClassUnloadTest[1695244027] / class java.lang.Class[635099371], jvmloading.MyClassLoader@2524e205[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.7.079.jdk/Contents/Home/jre/lib/rt.jar] 三、ClassLoader类加载器负责为虚拟机加载.class文件,ClassLoader类本身是一个抽象类,我们可以自己实现自己的类加载器。类加载器根据.class文件的签名信息,去寻找真正的二进文件,并经过加载、验证、准备、解析和初始化等一系列操作。每一个类对象都含有一个引用指向加载它的ClassLoader。javapublic class ClassLoaderTest { public static void main(String[] args) { ClassLoader classLoader ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); System.out.println(classLoader.getParent().getParent()); }}控制台输出:bashsun.misc.Launcher$AppClassLoader@5a4b4b50sun.misc.Launcher$ExtClassLoader@53d9f80null从输出结果可以看出,ClassLoaderTest是由AppClassLoader加载的,而其父加载器是ExtClassLoader,不过ExtClassLoader无法找到父加载器,是因为顶级BootstrapLoader是C语言实现的,这是一个启动类加载器,无法被Java程序直接引用。 JDK默认提供了三个ClassLoader:1. BootstrapLoader负责加载存放在javahome\jre\lib(javahome代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。2. ExtClassLoaderExtensionClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载javahome\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。3. AppClassLoaderApplicationClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 四、JVM类加载机制+ 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入+ 类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类+ 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效类的三种加载方式:1. JVM初始化加载2. Class.forName()方法动态加载3. ClassLoader.loadClass()方法加载javapublic class LoaderTest { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader classLoader LoaderTest.class.getClassLoader(); System.out.println("classLoader : " + classLoader); System.out.println("执行:classLoader.loadClass(\"jvmloading.DemoClass\")"); classLoader.loadClass("jvmloading.DemoClass"); //这里没有打印执行代码块的信息,因为loadClass调用的时候,resolvefalse,并不会去解析Class信息并初始化 System.out.println(); System.out.println("执行:Class.forName(\"jvmloading.DemoClass2\")"); Class.forName("jvmloading.DemoClass2"); System.out.println(); System.out.println("执行:Class.forName(\"jvmloading.DemoClass3\", false, classLoader)"); Class.forName("jvmloading.DemoClass3", false, classLoader); System.out.println(); System.out.println("执行:Class.forName(\"jvmloading.DemoClass3\", true, classLoader)"); Class.forName("jvmloading.DemoClass4", true, classLoader); }}控制台输出:bashclassLoader : sun.misc.Launcher$AppClassLoader@d16e5d6执行:classLoader.loadClass("jvmloading.DemoClass")执行:Class.forName("jvmloading.DemoClass2")初始化静态代码块 class2执行:Class.forName("jvmloading.DemoClass3", false, classLoader)执行:Class.forName("jvmloading.DemoClass3", true, classLoader)初始化静态代码块 class4从输出可以看出来,使用classLoader.loadClass方法加载类的时候,不会执行静态代码块,而Class.forName方法加载类信息,默认是会执行初始化操作,也就是会执行静态代码块,除非传入第二个参数是false。不管哪种类加载方式,只有调用了Class.newInstance()方法,才会执行构造函数。 五、双亲委派模型javaprotected 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) { //解析Class类信息,并将Class对象与其实际类加载器建立连接,如果已连接,则直接返回 resolveClass(c); } return c; }}从上面的方法中,大概可以看出双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,先判断是否已加载,如果已加载,则结束。否则开始加载,它首先把加载请求委托给父加载器去完成,在父类中也是如此的流程,依次向上。因此,每个类的加载请求都会向上传递到顶级加载器上进行处理。如果不存在父加载器,则会寻找启动类加载器进行处理,如果存在父加载器但是加载失败,则才调用自己定义的加载器进行加载。具体的:1. 当AppClassLoader加载一个class时,它先把类加载请求委派给父类加载器ExtClassLoader去处理。2. 当ExtClassLoader加载一个class时,它先把类加载请求委派给BootStrapClassLoader去完成。3. 如果BootStrapClassLoader加载失败(例如在 $JAVAHOME/jre/lib里未查找到该class),则会使用ExtClassLoader.findClass()来尝试加载。4. 如果ExtClassLoader也加载失败,则会使用AppClassLoader.findClass()来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。不管有多少类加载器,所有类的加载总是从顶级父加载器开始尝试加载,然后依次向下。这样的好处就是,同一个类总是由同一个加载器去加载,保证了内存中不会出现签名(package.ClassName.java)完全相同的字节码,从而保证程序的安全稳定运行。 六、自定义类加载器我们要加载在项目目录之外(如果是项目的classpath路径下,则会由AppClassLoader加载)的一个class文件,通常是网络文件或者没有提供jar包的文件。本例中,要加载的文件是本地的,路径为/Users/wjyuian/projects/oneblog/oneblog-data/target/classes/org/oneblog/data/util/StringTools.class。那这里classPath就是/Users/wjyuian/projects/oneblog/oneblog-data/target/classes/,而className就是'org.oneblog.data.util.StringTools'。首先定义一个自己类加载器:javapublic class MyClassLoader extends ClassLoader { //类的根目录,因为findClass中的name只是完整签名,不包含路径,因此这里需要指定 private String classPath; public MyClassLoader(String classPath) { super(); this.classPath classPath; } @Override protected Class findClass(String name) throws ClassNotFoundException { //从文件读取二进制流 byte[] classBytes loadClassData(name); if(classBytes null) { throw new ClassNotFoundException(); } return defineClass(name, classBytes, 0, classBytes.length); } private byte[] loadClassData(String className) { //拼接class文件的全路径,替换签名字符串中的.为/ String filePath classPath + className.replaceAll("\\.", "/") + ".class"; InputStream is null; ByteArrayOutputStream baos null; try { is new FileInputStream(new File(filePath)); baos new ByteArrayOutputStream(); int bufferSize 2048; byte[] buffer new byte[bufferSize]; int len 0; while((len is.read(buffer)) ! -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { //关闭相关资源 if(is ! null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(baos ! null) { try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; }}然后在main方法进行测试:javapublic class SelfClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { String path "/Users/wjyuian/projects/oneblog/oneblog-data/target/classes/"; //设置自定义类加载器的classPath MyClassLoader myClassLoader new MyClassLoader(path); Class clazz myClassLoader.loadClass("org.oneblog.data.util.StringTools"); //查看关联到该类的实际的加载器 System.out.println(clazz.getClassLoader()); //创建一个实例,这里只是测试实例化 Object instance clazz.newInstance(); System.out.println(instance); //获取方法,这里的方法是将字符串进行逗号分割并转为Long的list Method method clazz.getMethod("stringToLongList", String.class); //打印方法签名信息,本方法是static,所以invoke的时候,obj可以传null System.out.println(method); //这里instance可以传null List list (List) method.invoke(instance, "124,5674,772345,243"); //打印结果 System.out.println(list); }}控制台输出:bashjvmloading.MyClassLoader@667262b6org.oneblog.data.util.StringTools@17b68215public static java.util.List org.oneblog.data.util.StringTools.stringToLongList(java.lang.String)[124, 5674, 772345, 243]从输出可以确定,这个类确实由我们自定义的类加载器加载,并且可以正常实例化。通过反射也可以执行里面的静态方法。---参考:[jvm系列(一):java类的加载机制](https://mp.weixin.qq.com/s?bizMzI4NDY5Mjc1Mg&mid2247483934&idx1&sn41c46eceb2add54b7cde9eeb01412a90&chksmebf6da61dc81537721d36aadb5d20613b0449762842f9128753e716ce5fefe2b659d8654c4e8&scene21wechatredirect)

    JVM   类加载   ClassLoader   Java   2019-03-15 浏览(585) 有用(0) 阅读原文>> [原创]
  • J2EE开发技术栈相关整理——基础版   

    AI基础进行中... Zookeeper简介 Apache ZooKeeper is an effort to develop and maintain an open-source server which enables highly reliable distributed coordination. ZooKeeper is a high-performance coordination service for distributed applications. It exposes common services - such as naming, configuration management, synchronization, and group services - in a simple interface so you don't have to write them from scratch. You can use it off-the-shelf to implement consensus, group management, leader election, and presence protocols. And you can build on it for your own, specific needs.Apache ZooKeeper致力于提供一个高可用、高性能、开源的分布式应用协调服务。它为分布式应用提供一致性保障,常用的服务包括:命名系统、配置管理、分布式同步、组服务等。你通过一些简单的接口就可以轻而易举的获得一些功能,例如一致性维护、组管理、leader选举;同样的,你可以基于它来实现自己想要的其它功能。 ZooKeeper是以Fast Paxos算法为基础的,Paxos算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos作了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解。 设计目的+ 易用性 ZooKeeper提供一种简单的层级式命名设计,就像我们PC标准的文件系统一样(文件和目录)。通过共享同一个命名目录,ZooKeeper实现多个分布式处理过程之间可以相互通信、协调。 命名空间由数据目录组成,我们称之为znode。ZooKeeper目录结构和数据保存在内存中,这意味着高吞吐和低延迟。鉴于此,它具有以下几点特性: 1. 高性能:可以应用于生产环境的大规模分布式集群。 2. 高可用:防止单点故障;如果有一台follower发生故障,集群会立即踢掉这台服务器;如果leader发生故障,集群会立即进入选举流程,只要一半以上的服务器正常,则集群就是可用的。 3. 严格有序:客户端连接集群的任何一个服务端,获得消息的顺序都是一致的。+ 可复制 Zookeeper集群也是可以水平扩展的,可复制的。集群中的服务器相互可知,各自的内存中维护了一份集群状态数据,只要多数服务器是可用的,那么集群就是可用的。+ 有序性 ZooKeeper给每个更新动作都标记了一个数字,通过这个有序的数字来表示所有ZooKeeper事务。利用这个序号,我们可以实现更高层次的抽象,例如同步原语。+ 高性能 ZooKeeper可以运行在成千上万台机器上,并且它具有更高的读写性能比,比率大概为10:1 ZK节点+ PERSISTENT-持久化目录节点 客户端与zookeeper断开连接后,该节点依旧存在+ PERSISTENTSEQUENTIAL-持久化顺序编号目录节点 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号+ EPHEMERAL-临时目录节点 客户端与zookeeper断开连接后,该节点被删除+ EPHEMERALSEQUENTIAL-临时顺序编号目录节点 客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号参考:[Fast Pasox](https://blog.csdn.net/chen77716/article/details/7297122)[https://www.cnblogs.com/shenguanpu/p/4048660.html](https://www.cnblogs.com/shenguanpu/p/4048660.html)[http://blog.sina.com.cn/s/blog9d7b61450102vsx1.html](http://blog.sina.com.cn/s/blog9d7b61450102vsx1.html)---- CuratorCurator是Netflix公司开源的一个Zookeeper客户端,与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。目前是Apache的顶级项目。 常用组件+ org.apache.curator/curator-recipes All of the recipes. Note: this artifact has dependencies on client and framework and, so, Maven (or whatever tool you're using) should pull those in automatically. 这个组件提供了许多即插即用的功能模块。它需要依赖于client、framework两个组件,所以,最好通过Maven一起引入它们。+ org.apache.curator/curator-client The Curator Client - replacement for the ZooKeeper class in the ZK distribution. 这是一个客户端工具包,在基于ZK的分布式应用中,用它来代替zk-client会是一个更好的选择。+ org.apache.curator/curator-framework The Curator Framework high level API. This is built on top of the client and should pull it in automatically. Curator框架中高度抽象接口组件,它是Client和recips的上层抽象,所以必须要引入这个包。通过这个组件,我们可以轻松实现对于ZK的基本操作,比如连接管理、重连策略等。+ org.apache.curator/curator-async Asynchronous DSL with O/R modeling, migrations and many other features. 实战 一、创建连接Curator的zk连接实例是通过工厂方法创建的,因此同一个Zookeeper只需要一个连接实例即可:javaprivate static CuratorFramework init(String zookeeperHost) { log(LOGPREV, "init curator client, zkHost {}", new Object[]{ zookeeperHost }); CuratorFramework client null; //失败重试策略:失败后10s重试,最多重试8次 RetryPolicy retryPolicy new ExponentialBackoffRetry(10000, 8); client CuratorFrameworkFactory.builder() //zookeeper连接地址和重试逻辑 .connectString(zookeeperHost).retryPolicy(retryPolicy) //节点空间 .namespace("watchtest").build(); //这里是将start动作,交给请求实例的地方。// client.start(); return client;} 二、状态监听获取连接实例之后,我们还需要对连接状态进行监听,以便在不同状态下进行对应的操作,这个动作需要在start之前:java//添加客户端监听client.getConnectionStateListenable().addListener(new ConnectionStateListener() { //客户端连接zk节点之后,连接状态变动时的处理 @Override public void stateChanged(CuratorFramework client, ConnectionState state) { if(state null) { log(LOGPREV, "can not get connection state, zkHost {}", dubboregistry); return; } switch (state) { case CONNECTED://连接成功 log(LOGPREV, "connect to zookeeper successfully, zkHost {}", dubboregistry); break; case SUSPENDED://连接挂起 break; case RECONNECTED://挂起,丢失,只读的连接,被重新唤起 log(LOGPREV, "connection is reconnected, zkHost {}", dubboregistry); break; case LOST://连接丢失 log(LOGPREV, "connection is lost, zkHost {}", dubboregistry); break; default://连接只读 log(LOGPREV, "connect to zookeeper failed, zkHost {}", dubboregistry); break; } }});//启动连接CuratorFrameworkState state client.getState();//重复启动会报错:Cannot be started more than onceif(state ! null && state ! CuratorFrameworkState.STARTED) { log(LOGPREV, "curator client start to connected zookeeper, zkHost {}", new Object[]{ zookeeperHost }); client.start();} 三、创建节点上面提到,一个ZK连接只需要维护一个连接实例,因此可以采用静态变量CLIENT表示。java/ 节点创建确认 如果节点不存在,则创建 /private static void createIfNotExists(String nodePath) { try { //先判断节点路径是否存在 if (CLIENT ! null && CLIENT.checkExists().forPath(nodePath) null) { //创建节点,如果父目录不存在,会同步创建 String s CLIENT.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL) .forPath(nodePath, "node".getBytes()); log(LOGPREV, "[GgFramework] create zk node, path {}, s {}", nodePath, s); } } catch (Exception e) { e.printStackTrace(); }}同时,可以给这个节点设置数据:javaCLIENT.setData().forPath(path, byte[]...); 四、节点监听Curator提供三种节点监听方式:+ NodeCache A utility that attempts to keep the data from a node locally cached. This class will watch the node, respond to update/create/delete events, pull down the data, etc. You can register a listener that will get notified when changes occur. 这个类会将节点数据缓存到本地,并在节点的创建、删除以及更新操作发生时,更新缓存数据。我们可以在这个节点注册事件监听,以便及时获知上述三个动作的触发。 IMPORTANT - it's not possible to stay transactionally in sync. Users of this class must be prepared for false-positives and false-negatives. Additionally, always use the version number when updating data to avoid overwriting another process' change. 特别注意:这个类无法保证同步事务操作,因此用户需要自行处理“假阳性”(误报)和“假阴性”(漏报)。通常建议在更新数据的时候,通过维护一个数据版本号来达到最终一致性。 官方提供了两个构造函数: java / @param client curztor client @param path 节点完整路径 @param dataIsCompressed true表示进行数据压缩 / public NodeCache(CuratorFramework client, String path, boolean dataIsCompressed) //dataIsCompressed false public NodeCache(CuratorFramework client, String path) 给节点注册监听事件:java final NodeCache nodeCache null;//这里创建一个NodeCache ExecutorService pool null;//创建一个连接池 //启动 nodeCache.start(true); //绑定事件 final IChangedService temp null; //这是一个回调函数 nodeCache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception {//节点信息变更触发事件 GgTable data null; try { //反序列化 data SSDBCoderUtil.decode(nodeCache.getCurrentData().getData()); } catch (Exception e) { log(LOGPREV, "[ERROR] nodeChanged for ggtable, changedService {}", temp.getClass()); } temp.excute(data); } }, pool);+ PathChildrenCache: 同样可以缓存数据到本地,与NodeCache不同的是,它负责监听子节点的事件(创建、删除、更新,包括数据变化)。在使用构造函数进行创建实例的时候,需要注意构造函数的参数说明,例如下面这个: java public PathChildrenCache(CuratorFramework client, String path, boolean cacheData) 同样是三个参数,前两个和NodeCache一样,第三个则表示的是是否缓存数据,而不是压缩数据。 如果子节点发生了创建、删除、更新等动作,则会触发这个实例上注册的监听事件。有一个应用场景就是服务发现,我根据服务方法签名来监听对应接口,其子节点就是提供该服务接口的提供者信息列表。一旦有新的提供者或者现有提供者宕机,能及时发现。 java //下面是自己写的rpc服务发现代码 final PathChildrenCache nodeCache null;//根据父节点创建实例 nodeCache.start(); // 绑定事件 nodeCache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { ChildData data event.getData(); createIfNotExists(data.getPath()); byte[] bs client.getData().forPath(data.getPath()); RpcServiceRegister service SSDBCoderUtil.decode(bs); switch (event.getType()) { case CHILDADDED: // 子节点被添加 RpcLogUtil.log("provider 注册成功 {}", new Object[] {data.getPath()}); break; case CHILDREMOVED: // 子节点被删除 RpcLogUtil.log("provider 宕机 {}", new Object[] {data.getPath()}); break; case CHILDUPDATED: // 子节点数据变化 RpcLogUtil.log("provider 信息变动 {}", new Object[] {data.getPath()}); break; default: break; } if(service ! null) { RpcLogUtil.log("provider 信息 :{}, {} - {}:{}", new Object[] { service.getClassName(), service.getInstanceName(), service.getHost(), service.getPort() }); //设置服务提供者信息 RpcConsumerService.setProviderHostAndPort(service.getHost(), service.getPort()); } } }, Executors.newFixedThreadPool(1)); + TreeCache: 与上面两种节点不同的是,它既可以监听自身,同时也监听子节点。个人也未进行实际使用,这里就略过,有兴趣的可以自行测试。--- mysqlmysql默认的索引结构是B树。 实例介绍假设现在有一张表,结构如下:sqlCREATE TABLE position ( positionid varchar(63) NOT NULL, insidepositionid bigint(20) DEFAULT NULL, positiontitle varchar(63) NOT NULL, minworkyear int(7) DEFAULT NULL, workyeardesc varchar(31) DEFAULT NULL, keywords varchar(255) DEFAULT NULL, degreeid int(3) DEFAULT NULL, degreerequire varchar(31) DEFAULT NULL, description varchar(2047) DEFAULT NULL, jobrequirements varchar(2047) DEFAULT NULL, sourceoperatetime datetime DEFAULT NULL COMMENT '源操作时间', createtime bigint(20) DEFAULT NULL, workaddress varchar(300) DEFAULT NULL COMMENT '工作地点', outpositionname varchar(255) DEFAULT NULL COMMENT '对外职位名称', updatetime datetime DEFAULT NULL COMMENT '更新时间', sourcereleasetime datetime DEFAULT NULL COMMENT '源发布时间', chargeperson varchar(200) DEFAULT NULL COMMENT '招聘负责人', orgid int(11) DEFAULT NULL COMMENT '组织id', PRIMARY KEY (positionid), KEY pidipid (positionid,insidepositionid) USING BTREE, KEY degreepidorg (degreeid,positionid,orgid), KEY pid (positionid)) ENGINEInnoDB DEFAULT CHARSETutf8;请思考,以下查询语句是否可能用到索引,用到了哪些索引,为什么?sql1. EXPLAIN SELECT FROM position WHERE positionid 1 ;2. EXPLAIN SELECT FROM position WHERE positionid '' ;3. EXPLAIN SELECT FROM position WHERE insidepositionid 1 ;4. EXPLAIN SELECT FROM position WHERE degreeid 1 AND insidepositionid 1;5. EXPLAIN SELECT FROM position WHERE orgid 1 AND degreeid 1;6. EXPLAIN SELECT FROM position WHERE orgid 1;7. EXPLAIN SELECT FROM position WHERE degreeid 1 AND orgid 1 AND positionid 2;大部分人(包括我)都会这样回答: 1、2用到主键索引;3用到了索引pidipid;4、5的部分可能用到了索引degreepidorg;6没有用到索引;7用到了索引degreepidorg。实际上,这样的回答应该说是大部分是对的,但是我是不知道为什么的。接下来,我将这些SQL实际进行执行。 分析开始之前,先贴一下EXPLAIN中最重要的两个字段解释: possiblekeys 指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possiblekeys中的某些键实际上不能按生成的表次序使用。如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询 key key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possiblekeys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。1、EXPLAIN SELECT FROM position WHERE positionid 1 ;+ possiblekeys : PRIMARY,pidipid,pid+ key : pid虽然查询条件中用的是数值型常量,而positionid的字段定义是字符串,但依旧会走索引,不过用的是我们自己创建的名为KEY pid (positionid)的索引,而不是想象中的PRIMARY KEY (positionid)。注:第二次去执行的时候,发现key是NULL。2、EXPLAIN SELECT FROM position WHERE positionid '' ;+ possiblekeys : NULL+ key : NULL我比较不解的是这一条,明明查询条件中也是字符串常量,与字段定义一直,为何没有走索引?如果字符串的值是能查到数据的值,则key会显示PRIMARY。3、EXPLAIN SELECT FROM position WHERE insidepositionid 1 ;+ possiblekeys : NULL+ key : NULL与之前我们推测的可以使用KEY pidipid (positionid,insidepositionid)不同,这一条语句并没有可以利用的索引,这与mySQL默认的索引结构(BTREE)的特性(索引的最左匹配特性)有关。4、EXPLAIN SELECT FROM position WHERE degreeid 1 AND insidepositionid 1;+ possiblekeys : degreepidorg+ key : degreepidorg这里的确用到了degreepidorg索引,不过只有degreeid的查询用到了这个。5、EXPLAIN SELECT FROM position WHERE orgid 1 AND degreeid 1;+ possiblekeys : degreepidorg+ key : degreepidorg这里的确用到了degreepidorg索引,不过与我们想象的不同。在查询条件中的两个字段,只有degreeid的查询用到了索引,而另一个字段orgid是无法使用这个索引的,因为在这个索引中,联合索引字段是严格按照顺序的。这与mySQL默认的索引结构(BTREE)的特性有关,也可以与第三点相互印证。6、EXPLAIN SELECT FROM position WHERE orgid 1;+ possiblekeys : NULL+ key : NULL7、EXPLAIN SELECT FROM position WHERE degreeid 1 AND orgid 1 AND positionid 2;+ possiblekeys : PRIMARY,pidipid,degreepidorg,pid+ key : degreepidorgmySQL查询器会做优化,因此查询条件中的三个字段经过顺序调整刚好就用到了索引degreepidorg,而且是完全用到了。8、EXPLAIN SELECT positionid, position.degreeid,position.orgid FROM position WHERE degreeid 1 AND orgid 1 AND positionid 2;+ possiblekeys : PRIMARY,pidipid,degreepidorg,pid+ key : degreepidorg9、EXPLAIN SELECT FROM position WHERE degreeid 1 AND orgid 1 AND positionid 2;+ possiblekeys : PRIMARY,pidipid,degreepidorg,pid+ key : NULL10、EXPLAIN SELECT positionid FROM position WHERE insidepositionid 1;+ possiblekeys : NULL+ key : pidipid从8和9可以发现,如果查询域中有范围查询,即使它是在索引中,也不见得能用到索引;除非返回字段为索引中的字段(此时能用到索引的只有范围查询字段以及联合索引中的前面字段)。 InnoDBInnoDB是MySQL 5.6及以后版本默认的存储引擎,它平衡了高性能和高可用两个方面。 系统级别的两个数据库mysql和INFORMATIONSCHEMA依旧采用MyISAM作为存储引擎,并且不能改为InnoDB。+ DML操作遵循ACID模型,通过提交、回滚以及灾备恢复等事务性动作来保证用户数据的完整性。+ 行级锁和类Oracle的读一致性,提升多用户访问的并发能力和性能。+ 实际数据排列存储在磁盘上,并通过缓存primary keys来优化查询性能。每一个表都有一个主键索引,InnoDB通过这个主键将数据有序的存储在磁盘上,以便在遍历主键的时候最小化I/O开销。+ 在保证数据完整性上,InnoDB还支持外键限制。通过指定另一张表的外键限制,插入、更新和删除操作都会进行验证以确保外键数据不会与另一张表的实际数据发生冲突(外键指定的数据,在真实表中不存在的情况)。 关于SQL语句的子集划分: + DML:Data manipulation language 数据操作语句,比如INSERT、UPDATE、DELETE等。SELECT通常也被认为是DML。DML声明都在事务操作上下文中执行,因此这些都做是可以被提交或者回滚的,而且是原子的。 + DDL:Data definition language 结构定义语句,它们更多的是用于操作数据库本身而不是数据。比如CREATE、ALTER、DROP和TRUNCATE,这些动作会自动提交,因此是不能回滚的。 + DCL:Data control language 权限控制语句,比如GRANT、REVOKE。 关于ACID 数据库系统通常都具备这些属性,或者说事务操作必须要满足这些特性。InnoDB就是遵循这些特性来实现事务操作的。 + A(atomicity)原子性 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 + C(consistency)一致性 一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。 + I(isolation)隔离性 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。 + D(durability) 在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。持久性这里顺便提一下分布式系统中的CAP原则。 CAP原则又称CAP定理,它是NoSQL数据库的基石。指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。 也就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容错性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡,没有NoSQL系统能同时保证这三点 索引类型关于索引类型以及不同引擎支持的索引类型,可以参考[官方文档](https://dev.mysql.com/doc/refman/5.6/en/create-index.html)。 BTREE索引特性联合索引的最左匹配原则这是一条非常重要的原则,mySQL会按照联合索引的顺序,一直从左往右进行索引查询,直到遇到范围查询(、 3 AND d 4,如果联合索引是index1(a,b,c,d),则d的查询时无法用到索引的;如果联合索引是index2(a,b,d,c)就可以完全用到索引。在index2中,前三个字段顺序是任意的。发生不命中索引的情况LIKE、使用函数、OR、类型转换、!、范围查询等都可能发生无法命中索引的情况。 quartz solr mahout 关联规则 Spring myBatis mongoDB freemark、jstl

    Java   Zookeeper   Curator   2019-05-06 浏览(300) 有用(0) 阅读原文>> [原创]
  • JVM——初探内存结构(基于JDK8)   

    Java是一门通用的、支持并发的面向对象的编程语言。它借鉴了C和C++的语法,但是舍弃了一些令它们变得复杂的、不安全的容易使人产生迷惑的特性。而JVM(Java Virtual Machine)则是Java平台的基石。正因为它,Java平台才能使用少量的编译代码就能实现跨硬件、跨操作系统特性,而且让用户免受恶意程序的攻击。虽然是一个虚拟的计算机器,但JVM与真实的物理计算机一样,也有自己的指令集,也会在运行时操作不同的内存区域。通过虚拟机来实现编程语言是很常见的,比如UCSD Pascal语言就是通过P-Code虚拟机实现的。第一个JVM原型是由Sun公司实现的。----- JVM内存结构——运行时数据区(Run-Time Data Areas)JVM规范定义了在程序运行期间的若干个不同的运行时数据区(内存)。有些区域的生命周期与JVM一致,随着JVM的启动而创建,又随着JVM的退出而销毁。有些区域是线程私有的,线程私有的数据区生命周期与线程的生命周期一致。+ 程序计数器(Program Counter Register)+ Java虚拟机栈(Java Virtual Machine Stacks)+ Java堆(Java Heap)+ 方法区(Method Area) + 运行时常量池(Runtime Constant Pool)+ 本地方法栈(Native Method Stacks)![图片](https://oomabc.com/staticsrc/img/201903/22/1553244569400192ca2a5a24840d6948c14e4137ac2d1.jpg)--- 程序计数器(Program Counter Register)JVM的能支持多线程并发,就是通过线程轮流切换并分配CPU时间来实现的。每个JVM线程都有各自独立的程序计数器,只有这样才能在多线程轮流切换之后恢复到正确的执行位置。同一时刻,只有一个线程可以执行当前方法对应的机器指令。如果当前执行的不是native方法,则程序计数器的值就是当前正在执行的JVM指令的内存地址。如果当前在执行的是native方法,则此时的程序计数器的值是undefined。 If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined.这是一个线程私有的内存区域,也是唯一一个在JVM规范中没有规定任何OOM情况的区域---- Java虚拟机栈(Java Virtual Machine Stacks)每个JVM线程都有私有的虚拟机栈内存,它的生命周期与线程一致。我们把虚拟机栈存储的信息单位称为帧(Frame),它可以是方法执行过程中的局部变量、动态链接、方法调用信息和返回等信息。线程方法的调用就是通过将栈帧在虚拟机栈中进行入栈出栈(没有其它别的操作)操作实现的,栈帧的数据可能存储在堆上。虚拟机占栈的内存分配不需要物理上连续的内存块。JVM规范允许虚拟机栈的内存大小可以是固定的,也可以是动态按需分配的。。Java虚拟机栈在以下情况会发生异常:+ 如果是固定大小,则每个虚拟机栈在创建的时候就会分配一个独立的且固定大小的内存。在线程执行过程中,如果计算所需内存超过当时分配的线程栈内存,则会抛出StackOverflowError。+ 如果是动态按需分配,则Java虚拟机栈在需要的时候会进行内存扩张(按需分配更多的内存)。如果尝试内存扩张但是发现没有足够的内存,或者扩张后剩余的内存不足以为新线程初始化一个虚拟机栈,则JVM会抛出内存溢出错误OutOfMemoryError。线程私有,生命周期与线程一致,无需地址连续的内存----- Java堆(Java Heap)JVM堆内存是所有线程共享的,用来存放类的实例对象以及数组对象。在堆上存储的对象会通过一个自动存储管理系统(垃圾收集器,也就是GC)来进行回收,而不需要我们显式的指明某个对象需要回收,这一切都是自动的。堆的总内存上限是固定的,但实际上是按需分配的。在限定范围内,根据当前使用情况来进行内存扩展或者释放。Java堆总内存的上下限,通过JVM参数-Xmx4096m -Xms1024m设置。+ 当JVM在运行过程中,需要的内存大小超过了堆的最大值限制,则会抛出OutOfMemoryError异常。所有线程共享,生命周期与JVM一致,无需地址连续的内存 JVM规范描述的是,所有对象实例都在这里分配内存!但是随着JIT(Just-In-Time)编译器的发展与逃逸分析技术(某些方法内部产生的对象实例可以存储在栈上而不是堆上)的逐渐成熟,栈上分配、标量替换优化技术将导致一些微妙的变化,所有对象都在堆上分配内存也变得不那么绝对了。 为了提高回收效率和性价比,Java堆还可以细分为:新生代、老年代、Eden、From Survivor、To Survivor,线程共享的Java堆还能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。 内存分配机制Java内存分配主要是针对对象进行的,所以主要是指堆上的内存分配。内存分配和回收机制主要是分代分配、分代回收。按照对象存活时间,堆内存可以划分为:+ 新生代(Young Generation) + 伊甸园(Eden) + 存活区1(From Survivor) + 存活区2(To Survivor)+ 老年代(Old Generation)+ 永久代(Permanent Generation) 新生代1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;2. 首次Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor1(此时Survivor2是空白的,两个Survivor总有一个是空白的),然后清空Eden区;3. 下次 Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor2中,然后清空Eden区;4. 将Survivor1中消亡的对象清理掉,将其中可以晋级的对象复制到Old区,将存活的对象也复制到Survivor2区,然后清空Survivor1区;5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。------ 方法区(Method Area)这块区域存储已被虚拟机加载的类信息:+ 非静态的属性+ 非静态的方法的元数据+ 运行时常量池+ 方法和构造函数编译后的代码+ 类加载初始化或者实例对象初始化用到的特殊方法方法区在逻辑上是属于堆的一部分,少数虚拟机厂商不会对这一部分内存进行回收和整理。 对于HotSpot虚拟机对应的开发者来说,这里又叫“永久代”(Permanent Generation)。“永久代”可能被放弃,并搬家至Native Memory来实现方法区的规划。这块区域的内存也是可以固定大小,实际上按需分配或者释放的。JVM参数: -XX:PermSize128M -XX:MaxPermSize256M。+ 当方法区可用内存无法满足内存分配需求时,将抛出OutOfMemoryError异常。所有线程共享,生命周期与JVM一致,无需地址连续的内存----- 运行时常量池(Runtime Constant Pool)+ 它方法区的一部分+ 在类加载后,这里存放了Class文件的版本、字段、方法、接口等描述信息和常量池(Class文件常量池)表信息(编译期生成的各种字面量和符号引用)+ 存储Class文件加载后,数据结构的直接引用+ 动态性:不要求常量只能在编译期产生,并非是只有预先置入Class文件常量池的内容才能进入方法的运行时常量池,运行期也能产生新的常量,例如:String类的intern方法。----- 本地方法栈(Native Method Stacks)本地方法栈发挥的作用与虚拟机栈非常相似,这是一个传统意义上的栈,官方称它为C 栈,它是用来给native(用其它语言编写的方法,而不是java)方法提供服务的。用其它语言(比如C语言)来实现JVM指令集的解释器可以操作这个本地方法栈。JVM本身是无法管理native方法栈的。正因为它与虚拟机栈的作用非常相似,所以对应的异常条件以及信息都是一样的:+ 如果是固定大小,则每个本地方法栈在创建的时候就会分配一个独立的且固定大小的内存。在线程执行过程中,如果计算所需内存超过当时分配的栈内存,则会抛出StackOverflowError。+ 如果是动态按需分配,则本地方法栈在需要的时候会进行内存扩张(按需分配更多的内存)。如果尝试内存扩张但是发现没有足够的内存,或者扩张后剩余的内存不足以为新线程初始化一个本地方法栈,则JVM会抛出内存溢出异常OutOfMemoryError。 所有本地方法接口都会使用本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈(虚拟机栈)。然而当他调用的是本地方法时,虚拟机会保持Java栈不变 ,而简单与本地方法建立动态连接并直接调用该方法。可以理解为,Java虚拟机利用本地方法来动态扩展自己 。借助于本地方法栈,JVM可以在Java程序运行的任何时间点,如同调用自己的方法一样去调用本地方法。----- 直接内存(Direct Memory)+ 不是虚拟机运行时的数据区的一部分,也不是JVM规范中定义的内存区域+ 频繁的被使用,也能抛出OOM异常 JDK4加入的NIO(New Input/Output)类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,通过一个存储在堆内的DirectByteBuffer对象作为这块内存的引用,进行操作。避免在Java堆和Native堆来回复制数据。----- 帧(Frames)JVM在虚拟机栈中存储的信息就是帧(栈帧),那到底什么是帧呢?+ 存储数据与临时运算结果:动态链接、方法返回值、异常信息的包装。+ 方法调用的时候会创建一个对应的帧,方法调用完成后帧也随之被销毁。这里的完成指的是正常执行完成或者抛出异常。+ 有线程创建,并在虚拟机栈上分配内存。+ 每个帧有各自的局部变量表(以字长为单位的数组)、操作数栈(operand stack)、对应类对应方法的运行时常量池指针。+ Debug时产生的调试信息也会存储在帧中。+ 局部变量表和操作数栈的内存大小,是在编译期根据方法代码所需的帧的大小来确定的。而帧的数据结构所占内存大小只能有JVM实现来决定,而实际内存分配发生在方法调用的时候。任何一个线程,在方法执行期间只有一个帧被激活,叫做当前帧;这个在执行的方法叫做当前方法;包含这个方法的类叫做当前类;此时操作的局部变量表与操作数栈都是在当前帧上的。当当前帧对应的方法调用了另一个方法或者调用完成,当前帧不再是当前帧,会有新的帧成为当前帧。+ 方法A调用方法B,此时正在执行B方法,则B对应的帧就是当前帧,B就是当前方法+ B调用方法C,那么C创建的帧就是新的当前帧+ 方法C执行结束并返回,那么C的帧不再是当前帧;而B的帧再次激活变为当前帧线程私有,生命周期与方法一致参考文章[《The Java® Virtual Machine Specification —— Java SE 8 Edition》](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html)

    JVM   Java   内存结构   JDK8   2019-03-28 浏览(342) 有用(0) 阅读原文>> [原创]
  • JVM——Java内存模型   

    前言大部分Java程序员(包括我自己)大部分时间接触的都还是单线程代码,虽然JVM本身是支持多线程并发执行的。在JVM中,多个线程在执行代码(操作变量或对象)的时候,都是通过操作共享的主内存来实现的。多线程并发可能通过多个硬件处理器实现,也可能是在单处理器上通过切换CPU时钟实现,或者是在多个处理器上切换CPU时钟实现。对于程序员来说,只能通过创建一个Thread类的实例对象来创建一个线程;每个线程都会存在一个与之对于的实例对象。通过实例对象的start()方法来启动对应的线程:java//每个线程都是通过创建Thread的实例对象来定义的Thread firstThread new Thread();//只有调用的线程的start方法,才能启动线程(不管是这里的主动调用或者其他组件的隐式调用)firstThread.start();如果没有对多线程操作进行合理的同步(synchronized、volatile等)控制,就会出现一些匪夷所思的问题。本章会介绍Java多线程编码相关的语义,比如共享内存中多线程对变量进行更新之后的可见性。许多硬件体系结构中也存在类似内存模型的概念,而我们本章讨论的是Java语言中的内存模型,称为Java programming language memory model(JMM),下文简称内存模型。---- 同步(Synchronization)Java为多线程通信提供了不同级别的同步机制,最常用的就是同步原语(synchronized)了,它是通过监视器(准确的说是monitors,下文都称为监视器)实现的。每个Java对象(包括常见的类实例对象和Class对象)都有一个对应的监视器,同一个时刻只能有一个线程进获得某一个对象的监视器锁。此时,其它在尝试获得该监视器锁的线程都会堵塞,直到该锁被释放。特别注意的是,这种方式实现的锁是非公平锁:同一个线程可以多次重复获得同一个监视器锁,每次锁的竞争都与各个线程之前尝试获得锁的次数无关(线程A尝试获得锁100次与线程B尝试获得锁1次,对于某次竞争来说都是一样的)。 synchronized锁对象+ 代码执行到锁的位置,会尝试获取对象监视器锁,此时代码一直阻塞直到成功获得锁+ 获得锁之后,执行该代码块+ 代码块执行结束(正常结束或者抛出异常)之后,自动释放该监视器锁 synchronized锁方法+ 代码执行到锁的位置,会尝试获取对象监视器锁,此时代码一直阻塞直到成功获得锁+ 获得锁之后,执行该代码块+ 如果是对象方法,则锁的是该对象实例(调用该方法的实例)+ 如果是静态方法,则锁的是定义该方法的Class对象+ 代码块执行结束(正常结束或者抛出异常)之后,自动释放该监视器锁Java编译器不会检测或者阻止死锁代码,所以在多线对多个对象加锁的场景,我们需要自己来避免死锁,或者通过更高层次的不会死锁的锁原语来实现。Java中还提供了其它同步机制,比如volatile、java.util.concurrent包。---- 等待队列(Wait Set)和唤醒上面说过每个对象都有一个监视器,同时还有一个等待队列(准确的翻译应该是等待池,因为这里没有先进先出的约定,注意与锁池概念的差异),队列中放的就是等待获得监视器锁的线程。一个对象刚创建的时候,它的等待队列是空的,而线程进入和移出等待队列都是原子的。实例对象的wait、notify、notifyAll等方法可以对等待队列进行操作,实际上等待队的操作还受到线程终止状态以及各个类对于线程中断处理方式的影响。线程的等待和唤醒会对应线程中对象实例的方法的休眠(阻塞)和触发。 等待通过调用对象的wait、wait(long millisecs)、wait(long millisecs, int nanosecs)方法,可以是线程进入等待状态。wait(0, 0)、wait(0)的效果与wait()一致。当前线程t中,有个对象m正在执行wait方法,n代表线程t在对象m的监视器上获得锁的次数(未释放):+ 如果n是0,表示当前线程t并未获得对象m的监视器锁,则会抛出IllegalMonitorStateException异常信息。线程只有在获得监视器锁的情况下才能执行wait、notify、notifyAll等方法。+ 如果是调用wait(long millisecs)、wait(long millisecs, int nanosecs)方法,当 millisecs 比如这个例子,如果this.done不是一个volatile修饰的布尔变量:javawhile(!this.done){ Thread.sleep(1000);} 编译器只会从主存中读取一次this.done变量的值,然后会将该值缓存在线程栈内存中。所以,即使有另一个线程将this.done的值修改为true,这个循环也不会结束。--- 内存模型Java内存模型描述的是,对于给定的一个程序和执行路径,它能预测该执行路径是否符合规范。Java内存模型在工作的时候,会检查执行路径中每一次读操作,以确保在明确的规则下,读到的值都是最后一次写的值。内存模型描述了一个程序运行的规范。不同的虚拟机可以对同一段程序产生不同的执行路径,只要最终执行结果是内存模型可预测的。这样一来,虚拟机就可以有很大的自由度去优化程序代码,比如重排执行顺序、删除不必要的同步锁。Java允许编译器或者微处理器对代码进行优化,当这些优化行为发生在没有进行合理同步化的代码之上时,会产生一些令人难以捉摸的结果。比如下面这个例子: r1和r2分别是线程的局部变量,A和B是共享变量,初始时A B 0; 线程1执行两步操作:1、r2 A;2、B 1; 线程2执行两步操作:3、r1 B;4、A 2;正常思维一看,如果是1先执行,则最终r10或者r11,但一定会出现r20(此时线程1无法看到线程2中A 2这一步的结果);如果3先执行,则最终一定出现r10,而r22或者r20(此时线程2无法看到线程1中B 1的执行结果)。无论如何不可能出现r11和r22的情况。在不影响单线程执行的情况下,编译器允许对线程中的部分执行语句进行重新排序。比如,将线程1中的1和2步骤换一下顺序: 线程1执行两步操作:1、B 1;2、r2 A; 线程2执行两步操作:3、r1 B;4、A 2;如果此时的执行步骤是 1 3 4 2,则会出现r11和r22的结果。对一些程序员来说,这种执行结果似乎是不合理的。然而,我们需要注意的是,这杨的代码本身就没有进行合理的同步化:+ 在一个线程中进行写操作+ 另一个线程对同一个变量进行读操作+ 而且,读和写的操作没有进行同步化处理在Java内存模型架构体系中,把有能力对代码进行重排序的东西统一称为编译器。JVM中的JIT(Just-In-Time)就是一个可以将代码或者执行器进行重排序的编译器。 JIT 是 just in time 的缩写, 也就是即时编译编译器(动态编译)。使用即时编译器技术,能够提升Java 程序的执行速度。 javac将程序源代码编译,转换成java字节码,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。为了提高执行速度,引入了JIT技术。 在运行时JIT会把翻译过的机器码保存起来,以备下次使用,从理论上来说,采用该JIT技术后的执行速度可以接近编译技术。与之对应的还有AOT(Ahead Of Time),这是静态编译技术。目的就是将源码编译为本地可执行代码,大多数编译工作在执行前完成。而JIT则是将源码编译为字节码,然后解释为机器指令,然后进行编译。大多数编译动作在执行期间进行。关于JIT和AOT相关对比以及介绍,会在后续文章中进行。 变量共享Java内存模型描述的重点就是多线程之间的内存共享问题,这些内存我们称之为共享内存或者堆内存。所有实例对象的属性、类的静态字段以及数据元素都是存储在堆上的,也就是说都是可以被多线程共享的。包括方法参数以及方法内变量都称为局部变量,它们是无法被多线程共享的,也就不在Java内存模型的描述之内。如果同一时刻对同一个变量有两个操作(至少有一个写操作),就会发生冲突。 执行动作当一个线程动作可以影响到另一个线程,或者被另一个线程检测到的时候,这个动作就称为线程间动作:+ 变量读取(普通参数,不是volatile修饰的)+ 变量写入+ 被同步修饰的动作: + volatile变量的读取 + volatile变量的写入 + 竞争并获得监视器锁 + 释放监视器锁 + 线程的第一个和最后一个动作 + 启动线程或者检测线程中断状态的动作+ 外部执行动作:在执行线程之外可以观察到的且执行结果依赖于外部环境的动作。(比如Integer数字的递增循环,上限依赖于其最大值定义)+ 线程的自旋动作:在一个无限循环中,没有操作内存、没有同步变量也没有别的外部执行动作。Java内存模型只关心线程间动作,所以本章也无需去考虑线程内动作了(比如方法内对于局部变量的操作)。。。。。。。待续

    内存模型   JVM   2019-03-29 浏览(301) 有用(0) 阅读原文>> [原创]
  • Java对象结构以及Java锁   

    java对象结构 HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头(Header)对象头又可以分为两个部分:MarkWord和Klass+ Markword第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开指针启压缩)中分别为32bit和64bit,官方称它为“MarkWord”。+ Klass Pointer对象头的另外一部分是Klass类型指针,即对象指向它的类元数据的指针(所以也可以称为元数据指针),虚拟机通过这个指针来确定这个对象是哪个类的实例。所占空间大小为64bit,即8字节(开启指针压缩,大小为4字节)。+ 数组长度(只有数组对象有)如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。这是一个int类型的数值,所占空间为4字节。 实例数据(Instance Data)这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是8个字节(开启指针压缩,reference是4字节)。 静态属性的所占空间不计算在对象内存的大小中,因为它们存放在方法区。 对齐填充(Padding)第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot的虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充(Padding)来补全。 以上数据如无特殊说明,则是针对64位的虚拟机,且未开启指针压缩。 对象实例化举例在我们执行A a new A();时,JVM会进行哪些操作呢?1. 如果这个类没有被加载过,则会首先进行类加载,并在方法区创建一个java.lang.Class的实例对象(称为instanceKlass),储存在方法区。参考文章[《JVM——Java类的加载机制以及生命周期》](https://oomabc.com/articledetail?atclidc29481964c3247f0846fa842af4a23a2)2. 如果类被加载过,JVM会创建一个instanceOopDesc对象,来表示A类的实例对象,然后进行Markword填充,将其Klass Pointer指向方法区的instanceKlass对象,并填充实例对象的数据。3. 元数据——instanceKlass对象会存在元空间(方法区),而对象实例——instanceOopDesc会存在Java堆。Java虚拟机栈中会存有这个对象实例的引用。--- 锁锁,顾名思义就是控制访问,即谁谁谁可以访问某个东西,谁谁谁不能访问。在Java中,锁的概念是伴随着多线程并发而产生的。而Java在处理多线程的时候,是通过操作共享主内存来实现的。因此,在多个线程操作主存上同一个共享对象的时候,就会产生脏数据的问题。所以,我们需要提供一种机制来实现多线程执行和访问,这就是锁。如果某个对象同时只能被一个线程访问,则需要对该对象加一个同步锁,并在访问结束的时候释放它。最常用的方式就是同步原语(synchronized)了。关于Java内存模型和同步原语以及锁的相关流程的介绍,可以参考文章——[《JVM——Java内存模型》](https://oomabc.com/articledetail?atclida11df0b8af78483b8b9bde9a600bfd5a)。 锁的代价加了同步锁会造成并发线程的阻塞,其代价是很高昂的。 java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。synchronized会导致争用不到锁的线程进入阻塞状态,频繁的进行内核态和用户态切换,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。 乐观锁乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。--- 悲观锁悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。--- 自旋锁(spinlock)我们很难优化重量级锁引起的内核态和用户态切换而造成的性能损失,因此引入自旋的概念来减少线程阻塞造成状态频繁切换的开销。当一个线程在竞争锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(此时通过循环来达到阻塞的目的,但并不会引起状态切换),然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。1. 当前线程竞争锁失败时,打算阻塞自己2. 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)等待3. 在自旋的同时,不断的重新竞争锁3. 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己 缺点+ 单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner(锁的实际拥有者)就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。+ 自旋锁在自旋的时候要占用CPU。如果是计算密集型任务,自旋锁通常得不偿失,减少锁的使用是更好的选择。+ 如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。 优点+ 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,系统开销少,执行速度快。+ 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能) 自适应自旋锁看到这个概念,我的第一反应是这样的: 根据上一次自旋锁获得锁所自旋的时长,来决定下次的自旋时长,目的是在可接受的时间范围内,尽可能让每次自旋都有好结果。后来想了想,并不仅仅是这样。更重要的目的是,根据之前自旋是否获得锁以及所耗费时长,来判断下一次自旋在一定时间内能否获得锁,从而决定下一次竞争锁失败的时候,是否还需要自旋、自旋多久。如果前面几次自旋的时间都很长,那么系统会决定在下次竞争锁的时候可能会减少自旋时间或者省略自旋而直接进入阻塞状态。 自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。--- 轻量级锁自旋锁的目标是减少线程切换从而降低系统开销的成本。如果锁竞争激烈,那么过多的自旋锁会极大浪费CPU时间,所以通过重量级锁让竞争失败的线程阻塞是一种不错的选择;单如果实际上完全没有这么多的锁竞争的需求,那么申请重量级锁也是极大的浪费。轻量级锁的目标就是,减少无实际锁竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。 Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。从上面的定义也可以看出来,轻量级锁真正的使用场景是实际上不存在锁竞争(多个线程交替获得锁,无实际锁竞争);如果锁竞争的情况不激烈,我们还是可以使用自旋锁优化、自旋失败后再膨胀为重量级锁。--- 偏向锁在没有实际竞争的情况下,还能够针对部分场景继续优化。比如不仅仅没有实际竞争,而且使用锁的线程永远只有一个,那么维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS(Compare AND Set),但偏向锁只有初始化时需要一次CAS。“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何其它线程再来竞争锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。--- Mark Word 详解上面介绍对象头的时候,提到MarkWord存储了哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。下面对这一部分进行详细的说明。![图片](https://oomabc.com/staticsrc/img/201907/23/15638532363725f6f622a53c74c7f8e4346db1ae0cbec.jpg)+ lock:2位的锁状态标记。+ biasedlock:1位的偏向锁标记。为1时表示对象启用了偏向锁,为0时表示没有偏向锁。 关于偏向锁: [Biased Locking in HotSpot](https://blogs.oracle.com/dave/biased-locking-in-hotspot) Java中常见的CAS(Compare-And-Swap)操作会尝试获得Java对象的监视器锁,从而导致运行中的CPU出现大量的阻塞情况,这是非常消耗资源的。但是另一个方面,JVM中的大部分对象在整个生命周期中只会被一个线程所竞争,所以JVM将通过允许对象偏向某一个线程来提升性能。一旦某个线程获得这个偏向锁,那么它将不再需要通过代价极高的原子指令来进行偏向锁对象的锁定和释放操作。 很明显,某个对象在生命周期的任何时刻都可能被某个线程获得偏向锁(将这个线程叫做持有偏向锁的线程)。如果此时有另外一个线程尝试获得这个对象的偏向锁,那么就发生了竞争。通常,我们需要先将原始线程(当前持有该对象偏向锁的线程)获得的偏向锁标记清空(就是清空对象头中MarkWord的偏向锁标记)。但是在释放偏向锁的同时,线程可以再次获得偏向锁或者干脆直接获得通常意义的锁来锁定对象的剩余生命周期)。所以在撤销偏向锁标记的时候,最关键的事就是协调好竞争者和被竞争者(持有偏向锁的线程)的关系。我们必须保证,被竞争者在释放偏向锁的时候不会再次获得偏向锁。 更重要的一点是,我们需要证明放弃原子指令带来的的性能提升超过消除偏向锁时付出的代价,从而证明偏向锁是有价值的。+ age:4位的Java对象年龄标记。在[《JVM——初探内存结构(基于JDK8)》](https://oomabc.com/articledetail?atclid68ac7d02f5284124939d9bd97b902cfe)一文中介绍: 对象创建时分配的内存空间在Eden区,当Eden区满了之后会进行Minor GC,将依旧存活的对象复制到Survivor 1区,然后情况Eden;下次Eden区满的时候,将依旧存活的对象复制到Survivor 2,然后将Survivor 1中存活的对象也复制到Survivor 2。而Survivor中可以晋升到老年代的对象复制到Old区。 对象在Survivor区复制一次,age值加1。当对象的age值达到阈值时,会晋升到老年代。由于age只有4位,因此最大值是15,所以对象从Survivor区晋升到老年代的阈值默认就是15。+ identityhashcode:31位的对象唯一标识hashCode。通过调用System.identityHashCode(obj)方法计算,并将结果写到对象头中。当对象加锁(偏小、轻量级、重量级),Mark Word 的字节没有足够空间保存该值,则该值会移动到Monitor(对象监视器,[《JVM——Java内存模型》](https://oomabc.com/articledetail?atclida11df0b8af78483b8b9bde9a600bfd5a)一文中有提到这一概念)中。+ thread:偏向锁状态下,持有该偏向锁的线程ID。+ epoch:偏向锁时间戳。+ ptrtolockrecord:轻量级锁状态下,执行栈帧中锁记录的指针。ptr pointer。+ ptrtoheavyweightmonitor:重量级锁状态下,指向对象监视器Monitor指针。 锁升级上一节中说明了不同类型的锁的开销代价,JVM为了提高效率,会根据具体需求来进行锁的升级。1. 对象刚创建的时候,还没有线程来竞争,因此MarkWord处于“正常(无锁)”状态,lock位是01,biasedlock位是0。2. 当有一个线程来竞争锁的时候,首先使用偏向锁,直白点说就是这个对象偏好这个线程。此时,这个线程要执行此锁相关的代码无需进行任何锁校验和上下文切换,因此效率非常高。MarkWord会记录被偏好的线程的ID。lock位是01,biasedlock位是1。3. 当有两个线程来竞争这个锁对象的时候,不再是偏向锁了,此时偏向锁不公平。因此,偏向锁会升级为轻量级锁,两个线程公平竞争(之前哪个线程获得偏向锁,不影响竞争轻量级锁的结果)。哪个线程获得对象锁,MarkWord就会记录该线程的栈帧中的锁的指针。lock位是00。4. 当竞争这个锁对象的线程越来越多,导致了更多的状态切换和阻塞等待,消耗了越来越多的资源,此时JVM会将锁升级为重量级锁。此时的MarkWord会指向一个监视器对象。可以参考[《JVM——Java内存模型》](https://oomabc.com/articledetail?atclida11df0b8af78483b8b9bde9a600bfd5a)中关于监视器如何控制线程并发、锁竞争的细节。---参考:[Java对象结构与锁实现原理及MarkWord详解](https://blog.csdn.net/scdncp/article/details/86491792)

    Java   对象结构      MarkWord   2019-07-23 浏览(205) 有用(0) 阅读原文>> [原创]
  • blogTest
    分享文章
     
    使用APP的"扫一扫"功能,扫描左边的二维码,即可将网页分享给别人。
    你也可以扫描右边本博客的小程序二维码,实时关注最新文章。