ClassLoader(二)- 加载过程

本文源代码在Github。本文仅为个人笔记,不应作为权威参考。原文在前一篇文章初步了解ClassLoader里提到了委托模型(又称双亲委派模型),解释了ClassLoader hierarchy(层级)处理类加载的过程。那么class文件是如何变成Class对象的呢?Class的加载过程Class加载分为这几步:创建和加载(Creation and Loading)链接(Linking)验证(Verification)准备(Preparation)解析(Resolution),此步骤可选初始化(Initialization)注: 前面说了数组类是虚拟机直接创建的,以上过程不适用于数组类。创建和加载(Creation and Loading)何时会触发一个类的加载?Java Language Specification - 12.1.1. Load the Class Test:The initial attempt to execute the method main of class Test discovers that the class Test is not loaded - that is, that the Java Virtual Machine does not currently contain a binary representation for this class. The Java Virtual Machine then uses a class loader to attempt to find such a binary representation.也就是说,当要用到一个类,JVM发现还没有包含这个类的二进制形式(字节)时,就会使用ClassLoader尝试查找这个类的二进制形式。我们知道ClassLoader委托模型,也就是说实际触发加载的ClassLoader和真正加载的ClassLoader可能不是同一个,JVM将它们称之为initiating loader和defining loader(Java Virtual Machine Specification - 5.3. Creation and Loading):A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.When one class loader delegates to another class loader, the loader that initiates the loading is not necessarily the same loader that completes the loading and defines the class. If L creates C, either by defining it directly or by delegation, we say that L initiates loading of C or, equivalently, that L is an initiating loader of C.那么当A类使用B类的时候,B类使用的是哪个ClassLoader呢?Java Virtual Machine Specification - 5.3. Creation and Loading:The Java Virtual Machine uses one of three procedures to create class or interface C denoted by N:If N denotes a nonarray class or an interface, one of the two following methods is used to load and thereby create C:If D was defined by the bootstrap class loader, then the bootstrap class loader initiates loading of C (§5.3.1).If D was defined by a user-defined class loader, then that same user-defined class loader initiates loading of C (§5.3.2).Otherwise N denotes an array class. An array class is created directly by the Java Virtual Machine (§5.3.3), not by a class loader. However, the defining class loader of D is used in the process of creating array class C.注:上文的C和D都是类,N则是C的名字。也就说如果D用到C,且C还没有被加载,且C不是数组,那么:如果D的defining loader是bootstrap class loader,那么C的initiating loader就是bootstrap class loader。如果D的defining loader是自定义的class loader X,那么C的initiating loader就是X。再总结一下就是:如果D用到C,且C还没有被加载,且C不是数组,那么C的initiating loader就是D的defining loader。用下面的代码观察一下:// 把这个项目打包然后放到/tmp目录下public class CreationAndLoading { public static void main(String[] args) throws Exception { // ucl1的parent是bootstrap class loader URLClassLoader ucl1 = new NamedURLClassLoader(“user-defined 1”, new URL[] { new URL(“file:///tmp/classloader.jar”) }, null); // ucl1是ucl2的parent URLClassLoader ucl2 = new NamedURLClassLoader(“user-defined 2”, new URL[0], ucl1); Class<?> fooClass2 = ucl2.loadClass(“me.chanjar.javarelearn.classloader.Foo”); fooClass2.newInstance(); }}public class Foo { public Foo() { System.out.println(“Foo’s classLoader: " + Foo.class.getClassLoader()); System.out.println(“Bar’s classLoader: " + Bar.class.getClassLoader()); }}public class NamedURLClassLoader extends URLClassLoader { private String name; public NamedURLClassLoader(String name, URL[] urls, ClassLoader parent) { super(urls, parent); this.name = name; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println(“ClassLoader: " + this.name + " findClass(” + name + “)”); return super.findClass(name); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { System.out.println(“ClassLoader: " + this.name + " loadClass(” + name + “)”); return super.loadClass(name); } @Override public String toString() { return name; }}运行结果是:ClassLoader: user-defined 2 loadClass(me.chanjar.javarelearn.classloader.Foo)ClassLoader: user-defined 1 findClass(me.chanjar.javarelearn.classloader.Foo)ClassLoader: user-defined 1 loadClass(java.lang.Object)ClassLoader: user-defined 1 loadClass(java.lang.System)ClassLoader: user-defined 1 loadClass(java.lang.StringBuilder)ClassLoader: user-defined 1 loadClass(java.lang.Class)ClassLoader: user-defined 1 loadClass(java.io.PrintStream)Foo’s classLoader: user-defined 1ClassLoader: user-defined 1 loadClass(me.chanjar.javarelearn.classloader.Bar)ClassLoader: user-defined 1 findClass(me.chanjar.javarelearn.classloader.Bar)Bar’s classLoader: user-defined 1可以注意到Foo的initiating loader是user-defined 2,但是defining loader是user-defined 1。而Bar的initiating loader与defining loader则直接是user-defined 1,绕过了user-defined 2。观察结果符合预期。链接验证(Verification)验证类的二进制形式在结构上是否正确。准备(Preparation)为类创建静态字段,并且为这些静态字段初始化默认值。解析(Resolution)JVM在运行时会为每个类维护一个run-time constant pool,run-time constant pool构建自类的二进制形式里的constant_pool表。run-time constant pool里的所有引用一开始都是符号引用(symbolic reference)(见Java Virutal Machine Specification - 5.1. The Run-Time Constant Pool)。符号引用就是并非真正引用(即引用内存地址),只是指向了一个名字而已(就是字符串)。解析阶段做的事情就是将符号引用转变成实际引用)。Java Virutal Machine Specification - 5.4. Linking:This specification allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, provided that all of the following properties are maintained:A class or interface is completely loaded before it is linked.A class or interface is completely verified and prepared before it is initialized.也就是说仅要求:一个类在被链接之前得是完全加载的。一个类在被初始化之前得是被完全验证和准备的。所以对于解析的时机JVM Spec没有作出太多规定,只说了以下JVM指令在执行之前需要解析符号引用:anewarray, checkcast_, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, _putfield 和 putstatic 。看不懂没关系,大致意思就是,用到字段、用到方法、用到静态方法、new类等时候需要解析符号引用。初始化如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命为 <clinit>(class init)。JVM 规范枚举了下述类的初始化时机是:当虚拟机启动时,初始化用户指定的主类;new 某个类的时候调用某类的静态方法时访问某类的静态字段时子类初始化会触发父类初始化用反射API对某个类进行调用时一个接口定义了default方法(原文是non-abstract、non-static方法),某个实现了这个接口的类被初始化,那么这个接口也会被初始化初次调用 MethodHandle 实例时注意:这里没有提到new 数组的情况,所以new 数组的时候不会初始化类。同时类的初始化过程是线程安全的,下面是一个利用上述时机4和线程安全特性做的延迟加载的Singleton的例子:public class Singleton { private Singleton() {} private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return LazyHolder.INSTANCE; }}这种做法被称为Initialization-on-demand holder idiom。类加载常见异常ClassNotFoundExceptionJava Virutal Machine Specification - 5.3.1. Loading Using the Bootstrap Class Loader:If no purported representation of C is found, loading throws an instance of ClassNotFoundException.Java Virutal Machine Specification - 5.3.2. Loading Using a User-defined Class Loader:When the loadClass method of the class loader L is invoked with the name N of a class or interface C to be loaded, L must perform one of the following two operations in order to load C:The class loader L can create an array of bytes representing C as the bytes of a ClassFile structure (§4.1); it then must invoke the method defineClass of class ClassLoader. Invoking defineClass causes the Java Virtual Machine to derive a class or interface denoted by N using L from the array of bytes using the algorithm found in §5.3.5.The class loader L can delegate the loading of C to some other class loader L’. This is accomplished by passing the argument N directly or indirectly to an invocation of a method on L’ (typically the loadClass method). The result of the invocation is C.In either (1) or (2), if the class loader L is unable to load a class or interface denoted by N for any reason, it must throw an instance of ClassNotFoundException.所以,ClassNotFoundException发生在【加载阶段】:如果用的是bootstrap class loader,则当找不到其该类的二进制形式时抛出ClassNotFoundException如果用的是用户自定义class loader,不管是自己创建二进制(这里包括从文件读取或者内存中创建),还是代理给其他class loader,只要出现无法加载的情况,都要抛出ClassNotFoundExceptionNoClassDefFoundErrorJava Virtual Machine Specification - 5.3. Creation and LoadingIf the Java Virtual Machine ever attempts to load a class C during verification (§5.4.1) or resolution (§5.4.3) (but not initialization (§5.5)), and the class loader that is used to initiate loading of C throws an instance of ClassNotFoundException, then the Java Virtual Machine must throw an instance of NoClassDefFoundError whose cause is the instance of ClassNotFoundException.(A subtlety here is that recursive class loading to load superclasses is performed as part of resolution (§5.3.5, step 3). Therefore, a ClassNotFoundException that results from a class loader failing to load a superclass must be wrapped in a NoClassDefFoundError.)Java Virtual Machine Specification - 5.3.5. Deriving a Class from a class File RepresentationOtherwise, if the purported representation does not actually represent a class named N, loading throws an instance of NoClassDefFoundError or an instance of one of its subclasses.Java Virtual Machine Specification - 5.5. InitializationIf the Class object for C is in an erroneous state, then initialization is not possible. Release LC and throw a NoClassDefFoundError.所以,NoClassDefFoundError发生在:【加载阶段】,因其他类的【验证】or【解析】触发对C类的【加载】,此时发生了ClassNotFoundException,那么就要抛出NoClassDefFoundError,cause 是ClassNotFoundException。【加载阶段】,在【解析】superclass的过程中发生的ClassNotFoundException也必须包在NoClassDefFoundError里。【加载阶段】,发现找到的二进制里的类名和要找的类名不一致时,抛出NoClassDefFoundError【初始化阶段】,如果C类的Class对象处于错误状态,那么抛出NoClassDefFoundError追踪类的加载可以在JVM启动时添加-verbose:class来打印类加载过程。参考资料Java Language Specification - Chapter 12. ExecutionJava Virtual Machine Specification - Chapter 5. Loading, Linking, and Initializing极客时间 - 深入拆解Java虚拟机 - 03 Java虚拟机是如何加载Java类的?(专栏文章,需付费购买)CS-Note 类加载机制深入理解JVM(八)——类加载的时机深入理解JVM(九)——类加载的过程 ...

February 20, 2019 · 5 min · jiezi

ClassLoader(一)- 介绍

本文源代码在Github。本文仅为个人笔记,不应作为权威参考。原文什么是ClassLoaderjavadoc ClassLoader:A class loader is an object that is responsible for loading classes. …Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class.A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.简单来说:ClassLoader是一个负责加载Class的对象。给ClassLoader一个类名(需符合Java语言规范),那么它就应该尝试定位,或者生成包含该类定义的数据。一个典型的定位策略是把类名转换成class文件名,然后从文件系统里读取这个class文件。三种ClassLoader实现讲到bootstrap class loader就不得不说三种常见的ClassLoader实现。执行下面代码会看到三种类型的ClassLoader实现:import com.sun.javafx.util.Logging;import java.util.ArrayList;public class PrintClassLoader { public static void main(String[] args) { System.out.println(“Classloader for ArrayList: " + ArrayList.class.getClassLoader()); System.out.println(“Classloader for Logging: " + Logging.class.getClassLoader()); System.out.println(“Classloader for this class: " + PrintClassLoader.class.getClassLoader()); }}结果如下:Classloader for ArrayList: nullClassloader for Logging: sun.misc.Launcher$ExtClassLoader@5e2de80cClassloader for this class: sun.misc.Launcher$AppClassLoader@18b4aac2Bootstrap class loader。bootstrap class loader是native code写的。它是所有ClassLoader的祖先,它是顶级ClassLoader。它负责加载JDK的内部类型,一般来说就是位于$JAVA_HOME/jre/lib下的核心库和rt.jar。Extension class loader。即Extension class loader,负责加载Java核心类的扩展,加载$JAVA_HOME/lib/ext目录和System Property java.ext.dirs所指定目录下的类(见Java Extension Mechanism Architecture)。System class loader,又称Application class loader。它的parent class loader是extension class loader(可以从sun.misc.Launcher的构造函数里看到),负责加载CLASSPATH环境变量、-classpath/-cp启动参数指定路径下的类。类的ClassLoader每个Class对象引用了当初加载自己的ClassLoader(javadoc ClassLoader):Every Class object contains a reference to the ClassLoader that defined it.其实Class对象的getClassLoader()方法就能够得到这个ClassLoader,并且说了如果该方法返回空,则说明此Class对象是被bootstrap class loader加载的,见getClassLoader() javadoc:Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.数组类的ClassLoaderClass objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.简单来说说了三点:数组也是类,但是它的Class对象不是由ClassLoader创建的,而是由Java runtime根据需要自动创建的。数组的getClassLoader()的结果同其元素类型的ClassLoader如果元素是基础类型,则数组类没有ClassLoader下面是一段实验代码:import com.sun.javafx.util.Logging;public class PrintArrayClassLoader { public static void main(String[] args) { System.out.println(“ClassLoader for int[]: " + new int[0].getClass().getClassLoader()); System.out.println(“ClassLoader for string[]: " + new String[0].getClass().getClassLoader()); System.out.println(“ClassLoader for Logging[]: " + new Logging[0].getClass().getClassLoader()); System.out.println(“ClassLoader for this class[]: " + new PrintArrayClassLoader[0].getClass().getClassLoader()); }}得到的结果如下,符合上面的说法:ClassLoader for int[]: nullClassLoader for string[]: nullClassLoader for Logging[]: sun.misc.Launcher$ExtClassLoader@5e2de80cClassLoader for this class[]: sun.misc.Launcher$AppClassLoader@18b4aac2那如果是二维数组会怎样呢?下面是实验代码:import com.sun.javafx.util.Logging;public class PrintArrayArrayClassLoader { public static void main(String[] args) { System.out.println(“ClassLoader for int[][]: " + new int[0][].getClass().getClassLoader()); System.out.println(“ClassLoader for string[][]: " + new String[0][].getClass().getClassLoader()); System.out.println(“ClassLoader for Logging[][]: " + new Logging[0][].getClass().getClassLoader()); System.out.println(“ClassLoader for this class[][]: " + new PrintArrayClassLoader[0][].getClass().getClassLoader()); System.out.println(“ClassLoader for this Object[][] of this class[]: " + new Object[][]{new PrintArrayArrayClassLoader[0]}.getClass().getClassLoader()); }}结果是:ClassLoader for int[][]: nullClassLoader for string[][]: nullClassLoader for Logging[][]: sun.misc.Launcher$ExtClassLoader@5e2de80cClassLoader for this class[][]: sun.misc.Launcher$AppClassLoader@18b4aac2ClassLoader for this Object[][] of this class[]: null注意第四行的结果,我们构建了一个Object[][],里面放的是PrintArrayArrayClassLoader[],但结果依然是null。所以:二维数组的ClassLoader和其定义的类型(元素类型)的ClassLoader相同。与其实际内部存放的类型无关。ClassLoader类的ClassLoaderClassLoader本身也是类,那么是谁加载它们的呢?实际上ClassLoader类的ClassLoader就是bootstrap class loader。下面是实验代码:import com.sun.javafx.util.Logging;public class PrintClassLoaderClassLoader { public static void main(String[] args) { // Launcher$ExtClassLoader System.out.println(“ClassLoader for Logging’s ClassLoader: " + Logging.class.getClassLoader().getClass().getClassLoader()); // Launcher$AppClassLoader System.out.println(“ClassLoader for this class’s ClassLoader: " + PrintClassLoaderClassLoader.class.getClassLoader().getClass().getClassLoader()); // 自定义ClassLoader System.out.println(“ClassLoader for custom ClassLoader: " + DummyClassLoader.class.getClassLoader().getClass().getClassLoader()); } public static class DummyClassLoader extends ClassLoader { }}结果是:ClassLoader for Logging’s ClassLoader: nullClassLoader for this class’s ClassLoader: nullClassLoader for custom ClassLoader: nullClassLoader解决了什么问题简单来说ClassLoader就是解决类加载问题的,当然这是一句废话。JDK里的ClassLoader是一个抽象类,这样做的目的是能够让应用开发者定制自己的ClassLoader实现(比如添加解密/加密)、动态插入字节码等,我认为这才是ClassLoader存在的最大意义。ClassLoader的工作原理还是看javadoc的说法:The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.简单来说:ClassLoader使用委托模型(国内普遍称之为双亲委派模型)查找Class或Resource。每个 ClassLoader 实例都有一个parent ClassLoader。当要查找Class或者Resource的时候,递归委托给parent,如果parent找不到,才会自己找。举例说明:如果ClassLoader层级关系是这样A->B->C,如果被查找Class只能被A找到,那么过程是A-delegate->B-delegate->C(not found)->B(not found)->A(found)。JVM有一个内置的顶级ClassLoader,叫做bootstrap class loader,它没有parent,它是老祖宗。ContextClassLoaderClassLoader的委托模型存在这么一个问题:子ClassLoader能够看见父ClassLoader所加载的类,而父ClassLoader看不到子ClassLoader所加载的类。这个问题出现在Java提供的SPI上,简单举例说明:Java核心库提供了SPI A尝试提供了自己的实现 BSPI A尝试查找实现B,结果找不到这是因为B一般都是在Classpath中的,它是被System class loader加载的,而SPI A是在核心库里的,它是被bootstrap class loader加载的,而bootstrap class loader是顶级ClassLoader,它不能向下委托给System class loader,所以SPI A是找不到实现B的。这个时候可以通过java.lang.Thread#getContextClassLoader()和java.lang.Thread#setContextClassLoader来让SPI A加载到B。为何SPI A不直接使用System class loader来加载呢?我想这是因为如果写死了System class loader那就缺少灵活性的关系吧。Class的唯一性如果一个类被一个ClassLoader加载两次,那么两次的结果应该是一致的,并且这个加载过程是线程安全的,见ClassLoader.java源码: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) { // … 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. // … c = findClass(name); // … } } // … return c; }}如果一个类被两个不同的ClassLoader加载会怎样呢?看下面代码:// 把这个项目打包然后放到/tmp目录下public class ClassUniqueness { public static void main(String[] args) throws Exception { Class<?> fooClass1 = Class.forName(“me.chanjar.javarelearn.classloader.ClassUniqueness”); System.out.println(“1st ClassUniqueness’s ClassLoader: " + fooClass1.getClassLoader()); // 故意将parent class loader设置为null,否则就是SystemClassLoader(即ApplicationClassLoader) URLClassLoader ucl = new URLClassLoader(new URL[] { new URL(“file:///tmp/classloader.jar”) }, null); Class<?> fooClass2 = ucl.loadClass(“me.chanjar.javarelearn.classloader.ClassUniqueness”); System.out.println(“2nd ClassUniqueness’s ClassLoader: " + fooClass2.getClassLoader()); System.out.println(“Two ClassUniqueness class equals? " + fooClass1.equals(fooClass2)); }}运行结果是:1st ClassUniqueness’s ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac22nd ClassUniqueness’s ClassLoader: java.net.URLClassLoader@66d3c617Two ClassUniqueness class equals? false```观察到两点:虽然是同一个类,但是加载它们的ClassLoader不同。虽然是同一个类,但是它们并不相等。由此可以得出结论:一个Class的唯一性不仅仅是其全限定名(Fully-qualified-name),而是由【加载其的ClassLoader + 其全限定名】联合保证唯一。这种机制对于解决诸如类冲突问题非常有用,类冲突问题就是在运行时存在同一个类的两个不同版本,同时代码里又都需要使用这两个不同版本的类。解决这个问题的思路就是使用不同的ClassLoader加载这两个版本的类。事实上OSGi或者Web容器就是这样做的(它们不是严格遵照委托模型,而是先自己找,找不到了再委托给parent ClassLoader)。参考文档JDK Javadoc - ClassLoaderJDK Javadoc - ClassJava虚拟机是如何加载Java类的?(极客时间专栏,需付费购买)Class Loaders in Java深入探讨Java类加载器Java Language Specification - Chapter 12. ExecutionJava Virtual Machine Specification - Chapter 5. Loading, Linking, and Initializing ...

February 20, 2019 · 4 min · jiezi

一次快速排序引发的jvm调优

闲来无事,顺便写一个快排的代码。结果却引发了java.OutOfMemoryError:Java heap space。首先谈谈快速排序,这是一种在统计上很快的排序,他的核心思想是,在一个数组中随便取一个数作为基准(通常取最后一个),然后把整个数组划分,把比基准小或等于的数放在基准之前,把大于基准的数放在基准之后。然后再分别对基准之前的数组和基准之后的数组进行快速排序。java 代码:void quicksort(int[] nums,int begin,int end) { if(end <= begin)return; int p = begin-1; for(int i = begin;i <= end;i++) if(nums[i] <= nums[end]) { p++; int temp = nums[i]; nums[i] = nums[p]; nums[p] = temp; } quicksort(nums,begin,p-1); quicksort(nums,p+1,end); }这里唯一难理解的就是这个p,这里的思想是,p及其左侧都是小于等于基准的数,而p的右侧都是大于基准的数。因此,遍历这个数组的时候,若数大于基准,则访问下一个,否则把p+1,然后交换p和这个位置上的数,这样就能成功划分。因为它是快速排序,所以我想小数据量并不能体现它的快速。因此,我使用了一个很大的数组,并且使用随机数填充它。Random random = new Random();int[] nums = new int[ (102410241024) ];for(int i = 0;i < nums.length;i++) nums[i] = random.nextInt(500000);这个数组的大小是4个GB,因为一个int是4B,而10241024是1M,而1024M是1G。正当我要运行程序并且统计其运行时间时,悲剧发生了!!!Exception in thread “main” java.lang.OutOfMemoryError: Java heap space这是一个堆内存溢出错误。于是我立马想到一个解决方法,那就是扩大堆内存!数组大小只有4GB,我现在给JVM进程5000MB大小的堆内存,也就是大约4.8GB的内存,程序一定是没问题的了。于是我添加了参数-Xms5000m。这个参数的意义是,最小堆内存为5000MB。然鹅,结果是Exception in thread “main” java.lang.OutOfMemoryError: Java heap spacewhy???要说明这个必须先知道java内存结构。理解了java内存结构之后,才能明白。堆内存又分为,新生代(Young)和老年代(Old),而数组这种大对象一般是直接分配在老年代中。虽然我设置了5000MB的堆内存,但是这5000MB的内存并不都是给老年代的,老年代和新生代内存的默认比例是2,也就是说5000MB里只有2/3是老年代的,也就是3333MB≈3.25GB,这个大小仍然小于数组的4GB,因此现在我设置老-新比例为9,也就是老年代拥有5000MB0.9=4500MB≈4.39GB,此时已经大于数组所需要的内存大小!添加完参数-Xms5000m -XX:NewRatio=9后,我再次运行程序,运行成功!!!经过漫长的等待,输出了排序的时间为1443.11s,也就是大约24分钟!!!最后,java 核心代码:public static void main(String[] args) { Random random = new Random(); int[] nums = new int[ (102410241024) ]; for(int i = 0;i < nums.length;i++) nums[i] = random.nextInt(500000); long being = System.currentTimeMillis(); quicksort(nums,0,nums.length-1); long end = System.currentTimeMillis(); System.out.println(((double) (end-being))/1000+“s”); } static void quicksort(int[] nums,int begin,int end) { if(end <= begin)return; int p = begin-1; for(int i = begin;i <= end;i++) if(nums[i] <= nums[end]) { p++; int temp = nums[i]; nums[i] = nums[p]; nums[p] = temp; } quicksort(nums,begin,p-1); quicksort(nums,p+1,end); } ...

February 18, 2019 · 1 min · jiezi

Java代码如何运行在Java虚拟机中

我们都知道要运行Java代码就必须要有JRE,也就是Java运行时环境,JRE中包含了Java程序的必需组件,包括Java虚拟机以及Java核心类库,然而运行C++代码则不需要额外的运行时环境,只需要把代码编译成CPU能识别的指令即可,也就是机器码.那为什么Java不直接像C++那样而需要在虚拟机中运行呢?他在虚拟机中又是如何运行的?接着往下看.Java为什么要在虚拟机中运行刚才我们谈到C++是直接把代码编译成机器码的,但因为各个平台的架构不一样,CPU能处理的指令集也不一样,所以如果要在另一个平台上运行C++代码,就必须用该平台对应的C++代码编译器重新编译一遍才可以.Java一开始就意识到需要跨平台运行,所以Java设计了虚拟机,先将Java代码编译成字节码(class文件),这是虚拟机能够识别的指令,再由虚拟机内部将字节码翻译成机器码,所以我们只需要有Java字节码,就可以在不同平台的虚拟机中运行,这也就是我们一直说的"一次编译,到处运行".Java虚拟机如何运行Java字节码我们JDK所用的虚拟机名为HotSpot虚拟机,他会将所有class文件加载进来,加载后的Java类会被放置在方法区,后面运行时会执行其中的代码.Java虚拟机会在内存中划分出几块,包括程序计数器,本地方法栈,Java虚拟机栈,堆以及方法区.不过光是Java字节码还是无法运行,Java虚拟机还需要将字节码翻译成机器码,HotSpot有2种形式:第一种是解释执行,即将字节码逐条翻译成机器码并运行;第二种是即时编译(JIT),他会将一个方法内的所有字节码编译成机器码再执行.前者的优势无需等待编译,但逐条解释的代价就是运行速度会比后者慢,HotSpot默认采用混合模式,它会先解释执行字节码,然后对于反复执行的热点代码会去进行即时编译.即时编译是监理在复合二八定律的基础上,即百分之20的代码占据百分之80的计算资源.对于不常用的代码我们无需消耗时间在编译成机器码上,采用解释执行就可以,而对于热点代码我们可以将其编译成机器码以提升运行速度.HotSpot内置了几个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,以便在编译时间和生成代码的执行效率之间做取舍,C1编译时间更快,C2编译质量更高.

February 17, 2019 · 1 min · jiezi

JVM(四)垃圾回收的实现算法和执行细节

上一篇我们讲了垃圾标记的一些实现细节和经典算法,而本文将系统的讲解一下垃圾回收的经典算法,和Hotspot虚拟机执行垃圾回收的一些实现细节,比如安全点和安全区域等。因为各个平台的虚拟机操作内存的方法各不相同,且牵扯大量的程序实现细节,所以本文不会过多的讨论算法的具体实现,只会介绍几种算法思想及发展过程。垃圾回收算法1、标记-清除算法标记-清除算法是最基础的算法,像它的名字一样算法分为“标记”和“清除”两个阶段,首先需要标记出所需要回收的对象,标记完成后统一收集被标记的对象。优点: 实现简单。缺点: 产生不连续的内存碎片;“标记”和“清除”的执行效率都不高。标记-清除算法执行过程图:(本文图片来自《深入理解Java虚拟机》)2、复制算法复制算法就是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。优点: 执行效率高。缺点: 空间利用率低, 因为复制算法每次只能使用一半的内存。3、标记-整理算法也称标记-压缩算法,标记-整理算法采用和标记清除算法一样的对象“标记”,但后续不会对可回收对象进行清理,而是将存活的对象往一端空闲空间移动,然后清理边界以外的内存空间。优点: 解决了内存碎片问题,比复制算法空间利用率高。缺点: 因为有局部对象移动,相对效率不高。标记-整理算法执行过程图:4、分代收集算法目前商用虚拟机都采用的是分代收集的算法,这种算法按照对象存活周期把内存分为几块,一般Java中分为新生代和老年代。把存活率低的对象分到新生代使用复制算法提高垃圾回收的性能,老年代则存放存活率搞的对象,使用标记-清除和标记-整理的算法,提高内存空间使用率。新生代和老生代的具体介绍和参数配置,后续的文章会详细讲解。垃圾回收执行细节本节将详细的介绍一下HotSpot虚拟机在执行垃圾回收时的一些细节,目的是让读者更好的理解Java虚拟机。HotSpot虚拟机: 它是Sun JDK和OpenJDK自定的虚拟机,也是目前使用最广泛的虚拟机。垃圾回收流程: Java虚拟机在内存回收之前,为了保证内存的一致性,必须先暂停程序的执行,也就是传说中的Stop The World(简称STW),在使用可达性分析算法枚举GC Roots,标记出死亡对象,再进行垃圾回收。垃圾回收遇到的问题: 那既然是要暂停程序的运行,就一定要保证停止的时间足够短,并且可控,不然带来的灾难将是毁灭性的。解决方案: 显然HotSpot在设计的时候也考虑到了这个问题,所以在JIT编译的时候就会使用OopMap数据结构来记录栈和寄存器上的引用,这样虚拟机就直接知道了那些地方存放着对象的引用,如下图,为我编译String.hashCode()方法的部分本地代码:可以看出,使用OopMap数据结构存储了普通对象的指针引用。查看汇编的方法,启动命令窗体执行:java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly YouTestClass命令可能会报错: Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled报错解决方法:使用编译好的hsdis.dll放到:jre安装目录binserver目录下即可,hsdis.dll地址地址:https://pan.baidu.com/s/1-D6u…安全点(Safepoint)在OopMap的协助下,HotSpot可以快速的完成GC Roots枚举,但导致OopMap内容变化的指令很多,而且如果给每个对象生成对应的OopMap,会造成大量额外的空间,这会导致GC成本很高,所以HotSpot只会在“特定的位置”生成对应的OopMap,这些位置就成为“安全点”。HotSpot也并不是任何时刻都会停顿下来进行GC,只会在程序都到底安全点之后才会GC,所以安全点的设置不能太少,让GC等待时间太长,也不能太多增大运行时的成本。安全点的两种线程中断方式抢断式中断:不需要线程的执行代码去主动配合,当发生GC时,先强制中断所有线程,然后如果发现某些线程未处于安全点,恢复程序运行,直到进入安全点为止。主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起。目前所有商用虚拟机全部采用主动式中断。安全区域(Saferegion)安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然HotSpot不可能一直等待被阻塞或休眠的线程正常唤醒执行;此时就引入了安全区的概念。安全区(Saferegion):安全区域是指在一段区域内,对象引用关系等不会发生变化,在此区域内任意位置开始GC都是安全的;线程运行时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,HotSpot发起GC时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了GC Roots枚举(或这个GC过程),如果完成了就继续执行,否则将继续等待直到收到可以安全离开的Safe Region的信号为止。参考《深入理解Java虚拟机》《垃圾回收的算法与实现》最后关注公众号,发送“gc”关键字,领取《垃圾回收的算法与实现》学习资料。

January 25, 2019 · 1 min · jiezi

Java程序员:不识Jvm真面目,只缘身在增删查改中

前言JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。这里就给大家讲一下JVM。技术大咖带你垂直打击JVM什么是运行时数据区? 我们一起来分享。了解JVM底层原理,让你的代码撸得飞起。搞定内存溢出,涨薪升职。涨见识,字节码执行过程分析。直击真相,原理和代码全都有。测试、效果演示及总结。JVM是什么?JDK: java development kit (Java开发工具包) 编译、反编译、调试等。JRE: java runtime enviroment (Java运行环境)JVM: java Virtual Mechinal (Java虚拟机) 一次编写,到处运行!学jvm的目就是:提升代码质量、解决项目问题。面试!面试!还是面试!JVM是怎么玩的类加载器:Class字节码文件加载到内存执行引擎:解析字节码指令,得到执行结果运行时数据区JVM运行时数据区线程私有程序计数器虚拟机栈本地方法栈线程共享堆列表项目方法区BAT的JVM面试题JVM什么情况下会发生栈内存溢出?JVM中一次完整的GC流程是怎样的?GC——垃圾回收完整意味着有多种情况程序计数器指向当前线程正在执行的字节码指令的地址(行号)栈是什么?栈(Stack)入口和出口只有一个入栈出栈FILO先进后出虚拟机栈虚拟机栈创建一个线程就为线程分配一个虚拟机栈,它又会包含多个栈帧,因为每运行一个方法就创建一个栈帧。运行时才有数据栈帧运行一个线程中的一个方法1.局部变量表2.操作数栈3.动态连接4.返回地址深入理解虚拟机栈演示一段代码的方法的执行过程代码:public int calc(){int a=100;int b=200;int c=300;return(a+b)*c;}虚拟机栈的异常StackOverFlowError异常原因:执行的虚拟机栈深度大于虚拟机栈允许的最大深度(方法的递归调用)。解决办法:增加默认栈的容量。栈容量 -Xss 默认1MOutOfMemeoryError异常原因:多线程环境下虚拟机在扩展栈时无法申请到足够的内存空间。解决办法:减少默认栈的容量来换取更多的线程支持。JVM中线程共有的内存区域Java堆Java堆是被所有线程共享的一块内存区域所有的对象实例以及数组要在堆上分配元数据区老版本名称:方法区(永久代)类信息、常量、编译后的代码信息直接内存以上源于一个视频讲解的概述总结,后续将分享后半部分的内容:可达性分析算法——GC RootsJVM中的堆新生代为什么分三个区?新生代对象的分配和回收老年代对象的分配和回收JVM中一次完整的GC流程是怎样的?如果有兴趣想了解视频具体内容的可以关注我,加入我的合作群(805685193)即可获取原视频。还有一些Java架构视频讲解,需要获取Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术视频教程资料,架构思维导图,和BATJ面试题及答案的,都是免费分享的。关注我,加入我的合作群(805685193)即可获取视频。

January 12, 2019 · 1 min · jiezi

jvm类加载的过程

一个类从加载到虚拟机到使用结束从虚拟机卸载包括了加载、验证、准备、解析、初始化、使用、卸载,即为一个类的生命周期下面来看一下类加载的过程,即加载、验证、准备、解析、初始化5个阶段都做了什么事:阶段1:加载加载阶段虚拟机主要3件事:通过类的全名获取其二进制字节流;将字节流代表的静态结构转化为方法区识别的运行时数据结构;在内存中实例化这个类的java.lang.Class对象(不一定在堆内存中的,HotSpot就将Class对象放在了方法区里),程序访问这个类在方法区中的类型数据时会通过这个类去访问; 以上三点虚拟机并不要求如何实现,只是一个规范,比如第一步,通过类全名获取其二进制流,动态代理技术是在运行时获取、JSP应用是根据jsp文件获取并生成对应的Class以及从ZIP包中获取(JAR、EAR、WA同理)等阶段2:验证验证阶段大体上会完成4个阶段的验证(文件格式验证、元数据验证、字节码验证、符号引用验证),以保证虚拟机中类的规范和安全。文件格式验证,校验字节流是否复合Class文件的格式:验证文件是否以魔数0xCAFEBABE(十六进制class文件中的前4个字节)开头;主、次版本号(十六进制class文件中的第5、第6个字节)能否被当前版本的虚拟机处理;常量池中是否有不被支持的类型;指向常量的索引中是否指向了不存在的常量;Class文件中各个部分以及文件本身是否有被删除或附加的其他信息;……元数据类型,校验语义是否符合Java语言规范的要求:验证类是否有父类(除了java.lang.Object);验证父类是否继承了不可被继承的类;如果不是抽象类,那么要判断是否实现了父类或接口的所要求实现的所有方法;……字节码验证,校验类的方法体,确定语义是否符合逻辑:保证操作数栈中的数据类型与指令序列一致;保证跳转指令不会跳到方法体外的字节码指令上;保证方法体中的类型转换有效;阶段3:准备准备阶段是为类变量分配内存并设置类变量初始值的阶段这里所说的初始值并不是指代码赋的值,而是数据类型的默认值,如public static int value = 123; 在准备阶段过后,value会被置为0,而不是123。同时要注意,public static final int value = 123; 这种使用final修饰的变量,在准备阶段就会被赋值为123,而不是初始值。阶段4:解析解析阶段会将常量池内的符号引用转换为直接引用,关于符号引用和直接引用的解释如下: 符号引用:以一组符号来描述所引用的目,比如定义了在类IntF中定义了intValue = 123,接着让Test.foo中的a变量指向Intf.intValue: public class Test{ public void foo(){ int a = Intf.intValue; } } class Intf{ public static int intValue = 123; }编译代码之后我们用javap -verbose Test来查看class文件中的内容: Constant pool: #1 = Methodref #4.#12 // java/lang/Object."<init>":()V #2 = Fieldref #13.#14 // Intf.intValue:I #3 = Class #15 // Test #4 = Class #16 // java/lang/Object // 省略部分代码… public void foo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=1 0: getstatic #2 // Field Intf.intValue:I 3: istore_1 4: return LineNumberTable: line 3: 0 line 4: 可以看到常量池第2项是一个符号引用,指向了Intf.intValue直接引用:就是我们常说的指针或者句柄,直接引用的目标一定会在虚拟机内存中存在。阶段5:初始化初始化阶段是类加载的最后一个阶段,主要执行类的<clinit>方法(不同与<init>方法,<init>方法是在显式调用constructor时执行,而<clinit>方法在初始化阶段就会执行),<clinit>()方法会执行赋值操作和执行静态语句快中的内容,换句话说,如果代码中没有静态语句块和赋值操作,那么就可以没有<clinit>()方法。这个阶段虚拟机会保证父类的<clinit>()方法会在子类的<clinit>()方法前执行,而且在多线程环境中,虚拟机会保证<clinit>()方法的同步。参考文献:《深入理解Java虚拟机》 - 周志明 ...

January 10, 2019 · 1 min · jiezi

JVM详解2.垃圾收集与内存分配

博客地址:https://spiderlucas.github.io备用地址:http://spiderlucas.coding.meJava与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进来,墙里面的人却想出来。2.1 对象是否需要回收2.1.1 引用计数法算法原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1,任何时刻计数器都为0的对象就是不可能再被使用的。 优点:实现原理简单,而且判定效率很高。缺点:很难解决对象之间相互循环引用的问题。2.1.2 可达性分析算法原理:通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。Java中的GC Roots对象虚拟机栈(栈桢中的本地变量表)中的引用的对象本地方法栈中JNI(一般说的Native方法)的引用的对象方法区中的类静态属性引用的对象方法区中的常量引用的对象2.1.3 什么是引用无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。JDK 1.2 之前在JDK1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。缺点:一个对象在这种定义下只有被引用或者没有被引用两种状态,我们希望能描述这样一类对象——当内存空间还足够时,则能保留在内存之中;如果内存在GC之后还是非常紧张,则可以抛弃这些对象(如缓存)。JDK 1.2 之后在 JDK 1.2 之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次逐渐减弱。强引用:就是指在程序代码之中普遍存在,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后提供了SoftReference类来实现软引用。弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。虚引用(幽灵引用、幻影引用):是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。更多资料:深入探讨 java.lang.ref 包、慎用java.lang.ref.SoftReference实现缓存2.1.4 finalize()两次标记过程即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。如果这个对象有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环,将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记。如果对象要在Finalize()中成功拯救自己——只要重新与引用链上的任何的一个对象建立关联即可,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。使用finalize()自我救赎public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println(“yes, I am still alive”); } protected void finalize() throws Throwable { super.finalize(); System.out.println(“finalize method executed!”); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscapeGC(); // 对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为finalize方法优先级很低,所有暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println(“no ,I am dead QAQ!”); } // 以上代码与上面的完全相同,但这次自救却失败了!!! SAVE_HOOK = null; System.gc(); //因为finalize方法优先级很低,所有暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println(“no ,I am dead QAQ!”); } }}总结System.gc()底层调用的是Runtime.getRuntime().gc();,该方法的Java doc里边写的是调用此方法suggestsJVM进行GC,即无法保证对垃圾收集器的调用。finalize()方法至多由GC执行一次,用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize()的行为。虽然可以在finalize()方法完成很多操作如关闭外部资源,但更好的方式应该是try-finally。finalize()运行代价高昂,不确定大,无法保证各个对象的调用顺序。最好的方法就是忘掉有这个方法!2.1.5 回收方法区Java虚拟机规范不要求虚拟机在方法区实现垃圾收集;方法区的GC性价比一般比较低。方法区的GC主要是回收两部分内容:废弃常量和无用的类。废弃常量判断常量是否废弃跟对象是一样。常量池中的其他类、接口、方法、字段的符号引用也是如此。无用的类(必须同时满足以下三个条件)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收;该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。类是否回收满足上述3个条件的类只是被判定为可以被虚拟机回收,而不是和对象一样,不使用了基于就必然会回收。是否对类进行回收,还需要对虚拟机进行相应的参数设置。在HotSpot中,虚拟机提供-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出。2.2 垃圾收集算法2.2.1 标记-清除算法定义:标记-清除(Mark-Sweep)算法分为标记和清除两个阶段,首先标记出需要回收的对象,标记完成之后统一清除对象。缺点:效率问题,标记和清除过程效率不高;标记清除之后会产生大量不连续的内存碎片。2.2.2 复制算法定义:复制(Copying)算法它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉。优点:这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。缺点:内存缩小为原来的一半。使用情况:现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机:默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。2.2.3 标记-整理算法定义:标记-整理算法的标记过程与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是对所有存活的对象都向一端移动,然后清理掉边界以外的内存。优点:解决了复制算法在对象存活率较高情况下需要大量复制导致的效率问题,而且不会缩小内存。2.2.4 分代收集算法定义:根据对象存活周期的不同将内存分为几块,一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适用的算法。新生代:每次收集都会有大批对象死去,只有少量存活,采用复制算法。老年代:对象存活率较高、没有额外空间对它进行分配担保,采用标记-清除或标记-整理算法。2.3 HotSpot算法实现2.3.1 枚举根节点可达性分析的效率问题:可作为GC Roots的节点主要在全局性的引用(常量或类的静态属性)与执行上下文(如栈帧的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果逐个检查引用必然会消耗很多时间。GC停顿:可达性分析在分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,这就是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情成为“Stop The World”)的一个重要原因,即使在号称(几乎)不会发生停顿的CMS收集器中,枚举跟结点也是必须要暂停的。准确是GC:主流JVM都使用的是准确式GC,即JVM知道内存中某位置的数据类型什么,所以当执行系统停下来的时候,不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机可以有办法知道哪些地方存放着对象的引用。HotSpot的OOPMap:在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来;在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC在扫描的时候就可以直接获得这些信息。2.3.2 安全点为什么需要安全点:有了OOPMap,HotSpot可以快而准的完成GC Roots的查找,但如果为每一行代码的指令都生成OOPMap,这样将占用大量的空间。所以HotSpot并没有这么做!安全点:HotSpot只在特定的位置记录了OOPMap,这些位置称为安全点(Safe Point),即程序不能在任意地方都可以停下来进行GC,只有到达安全点时才能暂停进行GC。安全点的选择安全点的选定基本上是以“是否具有让程序长时间执行的特征”进行选定的,既不能选择太少以致于让GC等待太久,与不能太频繁以致于增大系统负荷。具体的安全点有:循环的末尾方法返回前调用方法的call之后抛出异常的位置GC时让所有线程停下来抢先式中断:不需要线程的执行代码主动配合,在GC时先把所有线程中断,然后如果有线程没有运行到安全点,则恢复线程让他们运行到安全点。几乎没有JVM采用这种方式。主动式中断:当GC需要中断线程的时候,不直接对线程操作而是设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。2.3.3 安全区域安全点的不足:安全点机制保证了程序执行时,在较短的时间就会遇到可以进入GC的安全点,但如果程序处于不执行状态(如Sleep状态或者Blocked状态),这时候线程无法相应JVM的中断请求,无法运行到安全点去中断挂起,JVM也不会等待线程重新被分配CPU时间。安全区域:安全区域(Safe Region)是指在一段代码片段之中,引用关系不会发生变化,这个区域的任何地方GC都是安全的。可以把安全区域看成是扩展了的安全点。安全区域工作原理在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。在线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开安全区域的信号为止。3.4 垃圾收集器这里讨论的收集器基于JDK 7 Update14的HotSpot虚拟机,这个版本中正式提供了商用的G1收集器。下图展示了HotSpot虚拟机的垃圾收集器,如果两个收集器存在连线,说明可以搭配使用。3.4.1 Serial简介:最基本、最悠久、单线程缺点:只会使用一条线程完成GC工作,而且在工作时必须暂停其他所有工作线程。优点:简单而高效(与其他收集器的单线程比),是JVM运行在Client模式下的默认新生代收集器。使用方式-XX:+UseSerialGC,设置之后默认使用Serial(年轻代)+Serial Old(老年代) 组合进行GC。3.4.2 ParNew简介:Serial的多线程版本,其余行为包括Serial的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial完全一样,默认开启的收集线程数与CPU数量相同。优点:多线程收集、能与CMS配合工作(这也是它是许多Server模式下虚拟机中首选的原因)缺点:单线程效率不及Serial。使用方式设置-XX:+UseConcMarkSweepGC的默认收集器设置-XX:+UseConcMarkSweepGC强制指定设置-XX:ParallelGCThreads参数来限制垃圾收集的线程数。3.4.3 Parallel Scavenge简介:新生代收集器、采用复制算法、并行多线程收集器、关注的目标是达到一个可控制的吞吐量而非尽可能的缩短GC时用户线程的停顿时间。吞吐量:CPU用于运行用户代码的时间和CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。停顿时间越短适合与用户交互的程序,良好的相应速度能提升用户体验;而高吞吐量可以高效利用CPU时间,适合后台运算。使用方式-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,是一个大于0的毫秒数-XX:GCTimeRatio:直接设置吞吐量大小,是一个大于0且小于100的整数,默认值是99,就是允许最大1%即(1/(1+99))的垃圾收集时间。-XX:+UseAdaptiveSizePolicy:如果设置此参数,就不需要手工设定新生代的大小、Eden于Survivor区的比例、晋升老年代对象年龄等细节参数来,虚拟机会动态调整。3.4.4 Serial Old收集器简介:Serial的老年代版本、单线程、使用标记整理算法用途:主要是为Client模式下的虚拟机使用;在Server模式下有两大用途,一是在JDK 5及之前版本中配合Parallel Scavenge收集器一起使用,而是作为CMS的后备预案,在并发收集发生Concurrent Mode Failure时使用。3.4.5 Parallel Old收集器简介:Parallel Scavenge的老年代版本、多线程、标记整理算法、JDK 6中才出现用途:直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,可以使用Parallel Scavenge和Parallel Old的组合。3.4.6 CMS简介:CMS(Concurrent Mark Sweep)以最短回收停顿时间为目标、适合B/S系统的服务端、基于标记清除算法优点:并发收集、低停顿工作流程初始标记——需要Stop The World,仅仅标记一下GC Roots能直接关联对象,速度很快并发标记——进行GC Roots Tracing重新标记——需要Stop The World,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,速度很快并发清除缺点对CPU资源非常敏感,在并发阶段它虽然不会导致用户线程停顿,但是会因为占用一部分线程(CPU资源)导致程序变慢CMS无法处理“浮动垃圾”——浮动垃圾是在并发清理阶段用户线程产生的新的垃圾,所以可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS在垃圾收集阶段用户线程还需要执行,所以不能像其他收集器那样等老年代几乎填满了再进行收集,所以需要预留一部分空间给用户线程。CMS运行期间如果预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败,此时虚拟机将会临时启用Serial Old收集器来进行老年代的垃圾收集,导致长时间停顿。由于CMS基于标记清除算法,所以会导致内存碎片。3.4.7 G1收集器原理堆内存划分:G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。收集策略:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由),有计划地避免在整个Java堆中进行全区域的垃圾收集。Region不可能是孤立的:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保障准确性?这个问题其实并非在G1中才有,只是在G1中更加突出了而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象也面临过相同的问题,如果回收新生代时也不得不同时扫描老年代的话,Minor GC的效率可能下降不少。使用Remembered Set来避免全堆扫描:在G1收集器中Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。优点并行与并发:G1能充分使用多CPU、多核来缩短Stop The World的停顿,部分其他收集器需要停顿Java线程执行的GC动作,G1仍然可以通过并发的方式让Java线程继续运行。分代收集:保留了分代收集的概念,而且不需要其他收集器配合能独立管理整个堆。空间整合:G1从整体看来是基于“标记-整理”算法实现的,从局部(两个Region之间)是基于复制算法实现的,不会产生空间碎片。可预测的停顿:G1能让使用者明确制定在长度为M毫秒内,消耗在GC上的时间不得超过N毫秒,这几乎是实时Java(RTJS)的垃圾收集器的特征了。运作流程如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:初始标记(Initial Marking)——标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记(Concurrent Marking)——从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。最终标记(Final Marking)——为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。筛选回收(Live Data Counting and Evacuation)——首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。3.4.8 GC参数总结参数描述UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收UseParNewGC打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收UseConcMarkSweepGC打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用UseParallelGC虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收UseParallelOldGC打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收SurvivorRatio新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1PretenureSizeThreshold直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配MaxTenuringThreshold晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代UseAdaptiveSizePolicy动态调整 Java 堆中各个区域的大小以及进入老年代的年龄HandlePromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况ParallelGCThreads设置并行GC时进行内存回收的线程数GCTimeRatioGC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效MaxGCPauseMillis设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效CMSInitiatingOccupancyFraction设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效UseCMSCompactAtFullCollection设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效CMSFullGCsBeforeCompaction设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效3.5 理解GC日志每一种收集器的日志形式都是由它们自身的实现所决定的,换言之每个收集器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如以下两段典型的GC日志:33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]前面的数字(33.125、100.667):代表GC发生的时间,即从JVM启动以来经过的秒数[GC或[FullGC:代表这次GC的停顿类型,如果有“Full”说明这次GC是发生了Stop-The-World的。新生代也会出现“[Full GC”,这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。[GC (System.gc())或[Full GC (System.gc()):说明是调用System.gc()方法所触发的收集。[DefNew、[Tenured、[Perm等:表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的——上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”;如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”;如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”;老年代和永久代同理,名称也是由收集器决定的。内部方括号中的3324K->152K(11904K):GC前该内存区域已使用容量 -> GC后该内存区域已使用容量(该内存区域总容量)。外部方括号中的3324K->152K(11904K):表示GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)。0.0025925secs:该内存区域GC所占用的时间,单位是秒。[Times:user=0.01 sys=0.00,real=0.02 secs]:user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。详细参见:Linux用户态程序计时方式详解3.6 内存分配与回收策略对象的内存分配总的来说,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配);对象主要分配在新生代的Eden区上;如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配;少数情况下也可能会直接分配在老年代中。分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。3.6.1 Minor和Full GC新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。3.6.2 对象优先在Eden分配大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。/** * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 /public class Allocation { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; // Minor GC }}[GC (Allocation Failure) [DefNew: 7482K->380K(9216K), 0.0061982 secs] 7482K->6524K(19456K), 0.0062260 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]Heap def new generation total 9216K, used 4641K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000) from space 1024K, 37% used [0x00000007bf500000, 0x00000007bf55f318, 0x00000007bf600000) to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) tenured generation total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000) Metaspace used 2968K, capacity 4496K, committed 4864K, reserved 1056768K class space used 327K, capacity 388K, committed 512K, reserved 1048576K-Xms20M、-Xmx20M、-Xmn10M、-XX:SurvivorRatio=8四个参数保证了整个Java堆大小为20M,新生代10M(eden space 8192K、from space 1024K、to space 1024K)、老年代10M。在给allocation4分配空间的时候会发生一次Minor GC,这次GC发生的原因是给allocation4分配所需的4MB内存时,发现Eden区已经被占用了6MB,剩余空间不足以分配 4MB,因此发生Minor GC。[GC (Allocation Failure) :表示因为向Eden给新对象申请空间,但是Eden剩余的合适空间不够所需的大小导致的Minor GC。GC期间虚拟机又发现已有的3个2MB对象无法全部放入Survivor空间(Survivor只有1MB),所以只好通过分配担保机制提前转移到老年代。这次GC结束后,4MB的allocation4对象被顺利分配到Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(allocation1,2,3占用)。3.6.3 大对象直接进入老年代什么是大对象:大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(byte[]数组就是典型的大对象)。大对象的影响:大对象对虚拟机的内存分配来说就是一个坏消息(更加坏的情况就是遇到一群朝生夕死的短命 对象,写程序时应该避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置大对象。设置大对象的参数:可以通过-XX:PretenureSizeThreshold参数设置使得大于这个设置值的对象直接在老年代分配,避免在Eden区及两个Survivor区之间发生大量的内存拷贝。/* * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728(3M) /public class PretenureSizeThreshold { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation = new byte[4 * _1MB]; }}Heap def new generation total 9216K, used 1502K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 18% used [0x00000007bec00000, 0x00000007bed778d8, 0x00000007bf400000) from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000) tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000) Metaspace used 2931K, capacity 4496K, committed 4864K, reserved 1056768K class space used 321K, capacity 388K, committed 512K, reserved 1048576K我们可以看到Eden空间几乎没有被利用,而老年代10MB空间被使用40%,也就是4MB的allocation对象被直接分配到老年代中,这是因为PretenureSizeThreshold被设置为3MB,因此超过3MB的对象都会直接在老年代中进行分配。PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。3.6.4 长期存活对对象将进入老年代对象年龄:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。设置对象晋升年龄:通过参数-XX:MaxTenuringThreshold来设置。/* * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 /public class MaxTenuringThreshold { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; // Eden空间不足GC,allocation1进入Survivor allocation3 = null; allocation3 = new byte[4 * _1MB]; // Eden空间不足第二次GC }}[GC (Allocation Failure) [DefNew: 5690K->624K(9216K), 0.0052742 secs] 5690K->4720K(19456K), 0.0053049 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] [GC (Allocation Failure) [DefNew: 4720K->0K(9216K), 0.0009947 secs] 8816K->4709K(19456K), 0.0010106 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000) from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000) tenured generation total 10240K, used 4709K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 45% used [0x00000007bf600000, 0x00000007bfa99570, 0x00000007bfa99600, 0x00000007c0000000) Metaspace used 2953K, capacity 4496K, committed 4864K, reserved 1056768K class space used 327K, capacity 388K, committed 512K, reserved 1048576K此方法中allocation1对象需要256KB的内存空间,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后会非常干净地变成0KB。而 MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时候新生代仍然有410KB的空间被占用。3.6.5 动态对象年龄判定为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在 Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。/* * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 */public class Main { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; // 第一次GC allocation4 = null; allocation4 = new byte[4 * _1MB]; // 第二次GC }}[GC (Allocation Failure) [DefNew: 5946K->880K(9216K), 0.0045988 secs] 5946K->4976K(19456K), 0.0046307 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew: 5058K->0K(9216K), 0.0012867 secs] 9154K->4965K(19456K), 0.0013125 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4315K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf036ce8, 0x00000007bf400000) from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf4000e0, 0x00000007bf500000) to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000) tenured generation total 10240K, used 4965K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) the space 10240K, 48% used [0x00000007bf600000, 0x00000007bfad9500, 0x00000007bfad9600, 0x00000007c0000000) Metaspace used 2957K, capacity 4496K, committed 4864K, reserved 1056768K class space used 327K, capacity 388K, committed 512K, reserved 1048576K发现运行结果中Survivor占用仍然为0%,而老年代比预期增加了,也就是说allocation1,allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来达到了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。 我们只要注释一个对象的new操作,就会发现另外一个不会晋升到老年代了。3.6.5 空间分配担保Minor GC流程:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小:如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时将进行一次Full GC。空间分配担保:出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间。一共有多少对象会活下去,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验,与老年代的剩余空间进行对比,决定是否进行Full GC来让老年代腾出更多空间。担保失败的解决办法:取平均值进行比较其实仍然是一种动态概率的手段,如果某次Minor GC存活后的对象突增以致于远远高于平均值时,依然会导致担保失败(Handle Promotion Failure)。如果出现HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。JDK 6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。 ...

December 19, 2018 · 5 min · jiezi

JVM详解3.JDK监控和故障处理工具

博客地址:https://spiderlucas.github.io备用地址:http://spiderlucas.coding.me3.1 JDK命令行工具这些工具大多数是tools.jar类库的一层薄的包装,它们的主要功能代码是在tools类库中实现的。还有一些甚至就是由Shell脚本直接生成的。名称作用jpsJVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程jstatJVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据jinfoConfiguration Info for Java,显示虚拟机配置信息 jmapMemory Map for Java,生成虚拟机的内存转储快照(heapdump文件)jhatJVM Heap Dump Browser,用于分析heapmap文件,它会建立一个http/html服务器让用户可以在浏览器上查看分析结果jstackStack Trace for Java,显示虚拟机的线程快照3.1.1 jps:虚拟机进程状况工具作用可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称(main()函数所在的类)以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier)。对于本地虚拟机进程来说,LVMI与操作系统的进程ID(Process Identifier,PID)是一致的。jps可以通过RMI协议开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。命令格式jsp [options] [hostid]主要选项属性 | 作用-p | 只输出LVMID,省略主类的名称-m | 输出虚拟机进程启动时传递给主类main()函数的参数-l | 输出主类的全名,如果进程执行的是jar包,输出jar路径-v | 输出虚拟机进程启动时jvm参数3.1.2 jstat:虚拟机统计信息监视工具作用jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾回收、JIT编译等运行数据,在没有GUI图形界面,只是提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。命令格式jstat [option vmid [interval [s|ms] [count]]]对于命令格式中的VMID和LVMID,如果是本地虚拟机进程,VMID和LVMID是一致的如果是远程虚拟机,那VMID的格式应当是:[protocol:] [//] lvmid[@hostname[:port]/servername]参数interval和count分别表示查询的间隔和次数,如果省略这两个参数,说明只查询一次。主要选项选项作用-class监视装载类、卸载类、总空间以及类装载所耗费的时间-gc监视java堆状况,包括eden区、两个survivor区、老年代、永久代等的容量、已用空间、GC时间合计信息-gccapacity监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到最大、最小空间-gcutil监视内容与-gc基本相同,但输出主要关注已使用控件占总空间的百分比-gccause与-gcutil功能一样,但是会额外输出导致上一次gc产生的原因-gcnew监视新生代GC情况-gcnewcapacity监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间-gcold监视老年代GC情况-gcoldcapacity监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间-gcpermcapacity输出永久代使用到的最大、最小空间-compiler输出JIT编译过的方法、耗时等信息-printcompilation输出已经被JIT编译过的方法示例S0:Survivor1区当前使用比例S1:Survivor2区当前使用比例E:Eden区使用比例 9.36%O:老年代使用比例M:元数据区使用比例CCS:压缩使用比例YGC:年轻代垃圾回收次数 7次FGC:老年代垃圾回收次数 6次FGCT:老年代垃圾回收消耗时间 0.119秒GCT:垃圾回收消耗总时间 0.145秒3.1.3 jinfo:Java配置信息工具作用jinfo的作用是实时的查看和调整虚拟机各项参数。命令格式jinfo [option] pid主要选项选项作用-flag <name>to print the value of the named VM flag-flag [+/-]<name>to enable or disable the named VM flag-flag <name>=<value>to set the named VM flag to the given value-flagsto print VM flags-syspropsto print Java system properties<no option>to print both of the above-h / -helpto print this help message3.1.4 jmap:Java内存映像工具作用jmap命令用于生成堆转储快照。查询finalize执行队列、java堆和永久代的详细信息。如空间使用率、当前用的是哪种收集器等。命令格式jmap [option] vmid主要选项选项作用-dump生成java堆转储快照。格式为: -dump:[live,]format=b,file=<filename>,其中live子参数说明是否只dump出存活的对象-finalizerinfo显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台下有效-heap显示java堆详细信息,如使用哪种收集器、参数配置、分代情况等,在Linux/Solaris平台下有效-jisto显示堆中对象统计信息,包含类、实例对象、合集容量-permstat以ClassLoader为统计口径显示永久代内存状态。只在Linux/Solaris平台下有效-F当虚拟机进程对-dump选项没有相应时。可使用这个选项强制生成dump快照。只在Linux/Solaris平台下有效3.1.5 jhat:虚拟机堆转储快照分析工具作用提供jhat与jmap搭配使用,来分析dump生成的堆快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。一般不会去直接使用jhat命令来分析demp文件:原因一是一般不会在部署应用程序的服务器上直接分析dump文件,一般会尽量将dump文件拷贝到其他机器上进行分析,因为分析工作是一个耗时且消耗硬件资源的过程,既然都要在其他机器上进行,就没必要受到命令行工具的限制了;另外一个原因是jhat的分析功能相对来说很简陋,VisualVM以及专门分析dump文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能实现比jhat更强大更专业的分析功能。命令格式jhat [ options ] heap-dump-file3.1.6 jstack:java栈跟踪工具作用jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。命令格式jstack [option] vmid主要选项选项作用-F当正常输出的请求不被响应时,强制输出线程堆栈-l除堆栈外,显示关于锁的附加信息-m如果调用到本地方法的话,可以显示c/c++的堆栈3.2 JConsole:JDK可视化工具JConsole ( Java Monitoring and Management Console ) 是—种基于JMX的可视化监视管理工具。它管理部分的功能是针对JMX MBean进行管理,由于MBean可以使用代码、中间件服务器的管理控制台或者所有符合JMX规范的软件进行访问。概览“概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图内存“内存”页签相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(Java堆和永久代)的变化趋势。线程“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。3.3 VisualVM:可视化工具VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了“All-in-One” 的描述字样,预示着它除了运行监视、故障处理外,还提供了很多其他方面的功能。如性能分析,VisualVM的性能分析功能甚至比起JProfiler、YourKit等专业且收费的Profiling工具都不会逊色多少,而且VisualVM的还有一个很大的优点:不需要被监视的程序基于特殊Agent运行,因此它对应用程序的实际性能的影响很小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。VisualVM兼容范围与插件安装VisualVM基于NetBeans平台开发,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM可以做到:显示虚拟机进程以及进程的配置、环境信息(jps、 jinfo)。监视应用程序的CPU、GC、堆、方法区以及线程的信息(jstat、jstack)。dump以及分析堆转储快照(jmap、jhat)。方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立个快照, 可以将快照发送开发者处进行Bug反馈。生成、浏览堆转储快照在VisualVM中生成dump文件有两种方式,可以执行下列任一操作:在“应用程序”窗口中右键单击应用程序节点,然后选择“堆Dump”。在“应用程序”窗口中双击应用程序节点以打开应用程序标签,然后在“监视”标签中单击“堆Dump”。分析程序性能在Profiler页签中,VisualVM提供了程序运行期间方法级的CPU执行时间分析以及内存分析,做Profiling分析肯定会对程序运行性能有比较大的影响,所以一般不在生产环境中使用这项功能。BTrace动态日志跟踪BTrace本身也是可以独立运行的程序。它的作用是在不停止目标程序运行的前提下,通过HotSpot虚拟机的HotSwap技术动态加入原本并不存在的调试代码。这项功能对实际生产中的程序很有意义:经常遇到程序出现问题,但排查错误的一些必要信息,譬如方法参数、返回值等,在开发时并没有打印到日志之中,以至于不得不停掉服务,通过调试增量来加入日志代码以解决问题。当遇到生产环境服务无法随便停止时,缺一两句日志导致排错进行不下去是一件非常郁闷的事情。BTrace的用法还有许多,打印调用堆栈、参数、返回值只是最基本的应用,在它的网站上有使用BTrace进行性能监视、定位连接泄漏和内存泄漏、解决多线程竞争问题的例子。 ...

December 19, 2018 · 1 min · jiezi

JVM详解4.类文件结构

博客地址:https://spiderlucas.github.io备用地址:http://spiderlucas.coding.me4.1 字节码平台无关:Sun公司以及其他的虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”。语言无关:语言无关的基础是虚拟机和字节码存储格式,Java虚拟机不和任何语言(包括Java)绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。4.2 Class类文件的结构Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。Class文件只有两种数据类型:无符号数、表。无符号数:无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表:表是由多个无符号数或其他表作为数据项构成的复合数据类型,表习惯性以_info结尾。表用于描述有层次的复合结构的数据,整个Class文件本质上就是一张表,由以下的数据项构成。容量计数器:无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干连续的数据项的形式。4.2.1 魔数与Class文件的版本魔数:每个Class文件的头4个字节称为魔数(Magic Number),其唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。值为0xCAFEBABE。Class的版本号:紧接着魔数的4个字节存储的是Class的版本号——第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。版本号兼容:高版本的JDK只能向下兼容以前版本的Class文件,不能运行以后版本的Class文件。4.2.2 常量池常量池:紧接着主次版本号后的是常量池,也可以理解为Class文件的资源仓库,它是与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还算第一个出现的表类型数据项目。常量池计数值:由于常量池中常量数量不固定,因此在入口处要放置一项u2类型的数据,代表常量池计数值(从1开始,因为计数的0代表“不引用任何一个常量池项目”的含义)。常量池存放数据:常量池中主要存放两大类常量——字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念——如文本字符串、声明为final的常量值等。符号引用则属于编译原理方面的概念,包括下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。动态连接:Java代码在javac编译的时候,并没有连接这一步骤,而是在虚拟机加载Class文件的时候动态连接。常量池中的项:常量池中每一项都是一个表,截止到JDK 7中更用14种各不相同的表结构数据,其共同特点就是表开始的第一位是一个u1类型的标识位。4.2.3 访问标志在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。4.2.4 类索引、父类索引、接口索引类索引和父类索引:是一个u2类型的数据,用于确定这个类的全限定类名和父类的全限定类名,指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引类型可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。接口索引集合:是一组u2类型的数据集合,用于描述这个类实现了哪些接口,这些被实现的接口按照从左到右排列在接口索引集合中。入口的第一项——u2类型的数据为接口计数器,表示索引表的容量;如果没有实现任何接口,则该计数器为0。4.2.5 字段表集合字段表:字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。一个字段包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。修饰符布尔值:上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。字段表结构类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count字段访问标志标志名称标志值含义ACC_PUBLIC0x0001字段是否publicACC_PRIVATE0x0002字段是否privateACC_PROTECTED0x0004字段是否protectedACC_STATIC0x0008字段是否staticACC_FINAL0x0010字段是否finalACC_VOLATILE0x0040字段是否volatileACC_TRANSIENT0x0080字段是否transientACC_SYNTHETIC0x1000字段是否由编译器自动产生的ACC_ENUM0x4000字段是否enumname_indexname_index是对常量池的引用,代表着字段的简单名称。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是“inc”和“m”。全限定名:以下面代码为例,“org/xxx/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。public class TestClass { private int m; public int inc() { return m + 1; }}descriptor_indexdescriptor_index也是对常量池的引用,代表着字段和方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。标识字符含义标识字符含义B基本类型byteJ基本类型longC基本类型charS基本类型shortD基本类型doubleZ基本类型booleanF基本类型floatV特殊类型voidI基本类型intL对象类型,如Ljava/lang/Object数组类型:每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,,一个整型数组“int[]”被记录为“[I”。描述方法:按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“( )”之内。如方法void inc()的描述符为“( ) V”,方法java.lang.String toString()的描述符为“( ) LJava/lang/String;”,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII) I”。attributes_count与attribute_info字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。对于本例中的字段m,他的属性表计数器为0,也就是说没有需要额外描述的信息,但是,如果将字段m的声明改为“final static int m=123”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。4.2.6 方法表集合方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表结合(attributes)几项,如字段表所示。方法访问标志标志名称标志值含义ACC_PUBLIC0x0001方法是否为publicACC_PRIVATE0x0002方法是否为privateACC_PROTECTED0x0004方法是否为protectedACC_STATIC0x0008方法是否为staticACC_FINAL0x0010方法是否为finalACC_SYNCHRONIZED0x0020方法是否为synchronizedACC_BRIDGE0x0040方法是否由编译器产生的桥接方法ACC_VARARGS0x0080方法是否接受不定参数ACC_NATIVE0x0100方法是否为nativeACC_ABSTRACT0x0400方法是否为abstractACC_STRICTFP0x0800方法是否为strictfpACC_SYNTHETIC0x1000方法是否由编译器自动产生的方法里的代码方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。重写与字段表集合相对应的,如果父类方法在子类汇总没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。自动添加方法有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。重载在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式汇总,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。4.2.7 属性表集合在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息,Java虚拟机运行时会忽略掉他不认识的属性。属性表的结构属性名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足下表所定义的结构:类型名称数量u2attribute_name_index1u4attribute_length1u1infoattribute_length虚拟机规范预定义的属性属性名称使用位置含义Code方法表Java代码编译成的字节码指令ConstantValue字段表final关键字定义的常量值Deprecated类、方法表、字段表被声明为deprecated的方法和字段Exceptions方法表方法抛出的异常EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法InnerClasses类文件内部类列表LineNumberTableCode属性Java源码的行号与字节码指令的对用关系LocalVariableTableCode属性方法的局部变量描述StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配Signature类、方法表、字段表JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为他记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦出后导致签名混乱,需要这个属性记录泛型中的相关信息SourceFile类文件记录源文件名称SourceDebugExtension类文件JDK 1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息,譬如在进行JSP文件调试时,无法同构Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的LocalVariableTypeTable类JDK 1.5中新增的属性,他使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加RuntimeVisibleAnnotations类、方法表、字段表JDK 1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的RuntimeInVisibleAnnotations类、方法表、字段表JDK 1.5新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的RuntimeVisibleParameter Annotations方法表JDK 1.5新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数RuntimeInVisibleAnnotations Annotations方法表JDK 1.5中新增的属性,作用与RuntimeInVisibleAnnotations属性类似,只不过作用对象为方法参数AnnotationDefault方法表JDK 1.5中新增的属性,用于记录注解类元素的默认值BootstrapMethods类文件JDK 1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符Code属性Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。如果方法表有Code属性存在,那么他的结构将如下表所示。类型名称数量u2attribute_name_index1u4attribute_length1u2max_stack1u2max_locals1u4code_length1u1codecode_lengthu2exception_table_length1exception_infoexception_tableexception_table_lengthu2attributes_count1attribute_infoattributesattributes_countattribute_name_index:是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,他代表了该属性的属性名称。attribute_length:指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减少6个字节。max_stack:代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值分配栈帧(Stack Frame)中的操作帧深度。max_locals:代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。code_length和code:用来存储java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个u1数据类型的取值范围为0x000xFF,对应十进制的0255,也就是一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义。关于code_length:有一件值得注意的事情,虽然他是一个u4类型的长度值,理论上最大值可以达到2的32次方减1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即他实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。Exceptions属性这里的Exceptions属性是在方法表与Code属性平级的一项属性。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是说方法描述时在throws关键字啊后面列举的异常。他的结构见下表。类型名称数量类型名称数量u2attribute_name_index1u2number_of_exceptions1u4attribute_length1u2exception_index_tablenumber_of_exceptionsnumber_of_exceptions:项表示方法可能抛出number_of_exceptions种受查异常exception_index_table:每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。LineNumberTable属性LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。他并不是运行时必须的属性,但默认生成到Class文件之中,可以在Javac中分别使用-g : none或-g : lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。LineNumberTable属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2line_number_table_length1line_number_infoline_number_tableline_number_table_lengthline_number_table:是一个数量为line_number_table_length、类型为line_number_info的集合line_number_info表:包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。LocalVariableTable属性LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,她也不是运行时必须的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g : none或-g :vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当前其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。LocalVariableTable属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2local_variable_table_length1local_variable_infolocal_variable_tablelocal_variable_table_lengthu2start_pc1u2length1u2name_index1u2descriptor_index1u2index1start_pc和length:属性分别代表了这个局部变量的生命周期开始地字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。name_index和descriptor_index:都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。index:是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),他占用的Slot为index和index+1两个。姐妹属性:在JDK1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是吧记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入后,由于描述符中反省的参数化类型被擦除掉,描述符就不能准确的描述泛型类型了,因此出现了LocalVariableTypeTable。SourceFile属性SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g: source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长的属性,其结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2sourcefile_index sourcefile_index数据项:是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码我呢见的文件名。ConstantValue属性ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。类似“int x = 123”和“static int x=123”这样的变量定义在Java程序中是非常常见的事情,但虚拟机对这两种变量赋值的方法和时刻都有所不同。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。虽然有final关键字才更符合“ConstantValue”的语义,但虚拟机规范中并没有强制要求字段必须设置了ACC_FINAL标志,只要求了有ConstantValue属性的字段必须设置ACC_STATIC标志而已,对final关键字的要求是javac编译器自己加入的限制。而对ConstantValue属性值只能限于基本类型和String,不过不认为这是什么限制,因为此属性的属性值只是一个常量池的索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性在想支持别的类型也无能为力。ConstantValue属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2constantvalue_index1ConstantValue属性:是一个定长属性,他的attribute_length数据项值必须固定为2。constantvalue_index数据项:代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。InnerClasses属性InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为他以及他所包含的内部类生成InnerClasses属性。该属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2number_of_class1inner_classes_infoinner_classnumber_of_classesnumber_of_classes:代表需要记录多少个内部类信息。inner_classes_info表:每一个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info的结构见下表。类型名称数量u2inner_class_info_index1u2outer_class_info_index1u2inner_name_index1u2inner_class_access_info1inner_name_index:是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0.inner_class_access_flags:是内部类的访问标志,类似于类的access_flags,他的取值范围见下表。标志名称标志值含义ACC_PUBLIC0x0001内部类是否为publicACC_PRIVATE0x0002内部类是否为privateACC_PROTECTED0x0004内部类是否为protectedACC_STATIC0x0008内部类是否为staticACC_FINAL0x0010内部类是否为finalACC_INTERFACE0x0020内部类是否为synchronizedACC_ABSTRACT0x0400内部类是否为abstractACC_SYNTHETIC0x1000内部类是否嫔妃由用户代码产生的ACC_ANNOTATION0x2000内部类是否是一个注解ACC_ENUM0x4000内部类是否是一个枚举Deprecated及Synthetic属性Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。属性的结构非常简单,其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置,见下表:类型名称数量u2attribute_name_index1u4attribute_length1Deprecated属性用于表示每个类、字段或者方法,已经被程序作者定位不在推荐使用,他可以通过在代码中使用@deprecated注释进行设置。Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置他们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。 StackMapTable属性StackMapTable属性在JDK 1.6发布周增加到了Class文件规范中,他是一个复杂的变长属性,位于Code属性的属性表,这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。这个类型检查验证器最初来源于Sheng Liang为Java ME CLDC实现的字节码验证器。新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型(Verification Types)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在JDK 1.6中首次提供,并在JDK 1.7中强制代替原本基于类型推断的字节码验证器。StackMapTable属性中包含零至多个栈映射栈(Stack Map Frames),每个栈映射帧都显示或隐式的代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。StackMapTable属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2number_of_entries1stack_map_framestack_map_frame_entriesnumber_of_entries《Java虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着他带有一个隐式的StackMap属性。这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。Signature属性Signature属性在JDK 1.5发布后增加到了Class文件规范之中,他是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK 1.5大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含饿了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为他记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都统统被擦除掉。使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。Signature属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2signature_index1其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。如果当前的Signature属性是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表的属性,则这个结构表示方法类型签名,如果当前Signature属性是字段表的属性,则这个结构表示字段类型签名。BootstrapMethods属性BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,他是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。《Java虚拟机规范(Java SE 7版)》规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确地BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,类文件的属性表中最多也只能一个BootstrapMethods属性。BootstrapMethods属性与JSR-292中的InvokeDynamic指令和java.lang.Invoke包关系非常密切。目前的Javac暂时无法生成InvokeDynamic指令和BootstrapMethods属性,必须通过一些非常规的手段才能使用到他们,也许在不久的将来,等JSR-292更加成熟一些,这种状况就会改变。BootstrapMethods属性的结构见下表。类型名称数量u2attribute_name_index1u4attribute_length1u2num_bootstrap_methods1bootstrap_methodbootstrap_methodsnum_bootstrap_methodsnum_bootstrap_methods:项的值给出了bootstrap_methods[]数组中的引导方法限定符的数量。bootstrap_methods[]数组:的每个成员包含了一个指向常量池CONSTANT_MethodHandle结构的索引值,他代表了一个引导方法,还包含了这个引导方法静态参数的序列(可能为空)。bootstrap_method:结构见下表。类型名称数量u2bootstrap_method_ref1u2num_bootstrap_arguments1u2bootstrap_argumentsnum_bootstrap_argumentsbootstrap_method_ref:bootstrap_method_ref项的值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。num_bootstrap_arguments:num_bootstrap_arguments项的值给出了bootstrap_arguments[]数组成员的数量。bootstrap_arguments[]:bootstrap_arguments[]数组的每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构之一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。4.3 字节码指令Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。操作码总数:Java虚拟机操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过256条放弃操作数对齐:由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte1和byte2),那他们的值应该是这样的:(byte1 << 8) | byte24.3.1 字节码与数据类型大多数的指令都包含了其操作所对应的数据类型信息,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。大部分与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,他没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有一些指令如无条件跳转指令goto则是与数据类型无关的。由于Java虚拟机的操作码最多只有256个,Java虚拟机的指令被设计成非完全独立的(Java虚拟机规范中把这种特性称为“Not Orthogonal”,即并非每种数据类型和每一种操作都有对应的指令)。大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译器或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型(Computational Type)。4.3.2 加载和存储指令加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_、lconst_<l>、fconst_<f>、dconst_<d>扩充局部变量表的访问索引的指令:wide以尖括号结尾的(例如iload_<n>)这些指令助记符实际上是代表了一组指令(例如iload_<n>,他代表了iload_0、iload_1、iload_2和iload_3这几条指令)。这几组指令都是某个带有一个操作数的通用指令的特殊形式。对于这若干组特殊指令来说,他们省略掉了显示的操作数,不需要进行取操作数的动作,实际上操作数就隐含在指令中。除了这点之外,他们的语义与原生的通用指令完全一致(例如iload_0的语义与操作数为0时的iload指令语义完全一致)。4.3.3 运算指令运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。由于没有直接支持byte、short、char和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现,所有的算术指令如下:加法指令:iadd、ladd、fadd、dadd。减法指令:isub、lsub、fsub、dsub。乘法指令:imul、lmul、fmul、dmul。除法指令:idiv、ldiv、fdiv、ddiv。求余指令:irem、lrem、frem、drem。取反指令:ineg、lneg、fneg、dneg。位移指令:ishl、ishr、iushr、lshl、lshr、lushr。按位或指令:ior、lor。按位与指令:iand、land。按位异或指令:ixor、lxor。局部变量自增指令:iinc。比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。整数运算在处理整型数据时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算场景都不应该抛出运行时异常。对long类型数值进行比较时,虚拟机采用带符号的比较方式,而浮点数运算虚拟机在处理浮点数时必须严格遵循IEEE 754规范中所规定的行为和限制。也就是说,Java虚拟机必须完全支持IEEE 754中定义的非正规浮点数值(Denormalized Floating-Point Numbers)和逐级下溢(Gradual Underflow)的运算规则。所有的运算结果都必须舍入到适当的精度,非精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的。Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常(这里所讲的是Java语言中的异常,勿与IEEE 754规范中的浮点异常互相混淆,IEEE 754的浮点异常是一种运算信号),当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示。所有使用NaN值作为操作数的算术操作,结果都会返回NaN。对浮点数值进行比较时(dcmpg、dcmpl、fcmpg、fcmpl),虚拟机会采用IEEE 754规范所定义的无信号比较(Nonsignaling Comparisons)方式。4.3.4 类型转换指令类型转换指令可以将两种不同的数值类型进行相互转换,JVM直接支持小范围类型向大范围类型的安全转换,而处理大范围类型到小范围类型的窄化类型转换则需要显示地使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换会导致结果产生不同的正负号、不同的数量级、数值精度丢失的情况,但永远不可能抛出运行时异常。4.3.5 对象创建与访问指令类实例与数组都属于对象,但是其创建与操作使用了不同的字节码指令,指令如下:创建类实例:new创建数组:newarray, anewarray, multianewarray访问类字段(static字段)和实例字段:getfield, putfield, getstatic, putstatic把一个数组元素加载到操作数栈:baload, caload, saload, iaload, laload, faload, etc.把一个操作数栈的值存储到数组元素中:bastore, castore, sastore, iastore, etc.取数组长度:arraylength检查类实例类型:instanceof, checkcast4.3.6 操作数栈管理指令如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:pop、pop2复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2将栈最顶端的两个数值互换:swap4.3.7 控制转移指令控制转移指令可以让Java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件的修改PC寄存器的值。控制转移指令如下。条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。复合条件分支:tableswitch、lookupswitch。无条件分支:goto、goto_w、jsr、jsr_w、ret。int、reference、null指令集:在Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作;为了可以无需明显标识一个实体值是否null,也有专门的指令用来检测null值。转化成int类型:与算术运算时的规则一致,对于boolean类型、byte类型、char类型和short类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),运算指令会返回一个整形值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为int类型的比较操作,int类型比较是否方便完善就显得尤为重要,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。4.3.8 方法调用和返回指令方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn;另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。以下列举了5条用于方法调用的指令:invokevirtual——指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。invokeinterface——指令用于调用接口方法,他会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。invokespecial——指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic——指令用于调用类方法(static方法)。invokedynamic——指令用于运算时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。4.3.9 异常处理指令在Java程序中显示抛出异常的操作(throw 语句)都由athrow指令来实现除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。4.3.10 同步指令Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构是使用管程(Monitor)来支持的。方法级的同步方法级的同步是隐式的,即无需通过字节码指令来控制,他实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。同步一段指令集同步一段指令集通常是由Java语言中的synchronized语句块来表示的。Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monoitorexit指令依然剋有正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,他的目的就是用来执行monitorexit指令。信号量与管程管程:管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。信号量:信号量是一种抽象数据类型,由一个整形 (sem)变量和两个原子操作组成:P():sem减1,如果sem<0等待,否则继续;V():sem加1,如果sem<=0,说明当前有等着的进程,唤醒挂在信号量上的等待进程,可以是一个或多个 。4.4 公有设计和私有实现Java虚拟机规范描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集。这些内容与硬件、操作系统及具体的Java虚拟机实现之间是完全独立的。Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义,一个优秀的虚拟机实现,在满足虚拟机规范的约束下对具体实现做出修改和优化也是完全可行的,并且虚拟机规范中明确鼓励实现者这样做。只要优化后Class文件依然可以被正确读取,并且包含在其中的语义能得到完整的保持,那实现者就可以选择任何方式去实现这些语义。虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的可移植性,选择哪种特性取决于Java虚拟机实现的目标和关注点是什么。虚拟机实现的方式主要有以下两种:将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。将输入的Java虚拟机代码在加载或执行时翻译成宿主CPU的本地指令集(即JIT代码生成技术)。 ...

December 19, 2018 · 1 min · jiezi

JVM详解1.Java内存模型

博客地址:https://spiderlucas.github.io备用地址:http://spiderlucas.coding.me1.1 基础知识1.1.1 一些基本概念JDK(Java Development Kit):Java语言、Java虚拟机、Java API类库JRE(Java Runtime Environment):Java虚拟机、Java API类库JIT(Just In Time):Java虚拟机内置JIT编译器,将字节码编译成本机机器代码。OpenJDK:OpenJDK是基于Oracle JDK基础上的JDK的开源版本,但由于历史原因缺少了部分(不太重要)的代码。Sun JDK > SCSL > JRL > OpenJDKJCP组织(Java Community Process):由Java开发者以及被授权者组成,负责维护和发展技术规范、参考实现(RI)、技术兼容包。1.1.2 编译JDK参见《深入理解Java虚拟机》1.6节走进JVM之一 自己编译openjdk源码1.2 Java内存模型1.2.1 运行时数据区域根据Java虚拟机规范(Java SE7)的规定,JVM的内存包括以下几个运运行时数据区域:程序计数器程序计数器(Program Counter Register)是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。程序计数器是线程私有的,每条线程都有一个独立的独立的程序计数器,各条线程之间计数器互不影响。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。Java虚拟机栈Java虚拟机栈是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于包含局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成这个过程,就对应这一个栈帧在虚拟机栈中的入栈到出栈的过程。局部变量表存放了编译期可知的各种基本数据类型和对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址) 。其中64位长度的long和double类型会占用2个局部变量空间,其余的数据类型只会占用1个局部变量空间。局部变量表所需的内存空间在编译期间完成内存分配。当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。在Java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出);如果虚拟机栈可以动态扩展(现在大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError异常(没有足够的内存)。本地方法栈本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地Native方法服务。在虚拟机规范中对本地方法栈中的使用方法、语言、数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。本地方法栈也会抛出StackOverFlowError和OutOfmMemoryError异常。Java堆Java堆(Java Heap)是Java虚拟机管理内存中的最大一块。Java堆是所有线程共享的一块内存管理区域。此内存区域唯一目的就是存放对象的实例,几乎所有对象实例都在堆中分配内存。这一点在Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也不是变的那么“绝对”了。Java堆是垃圾回收器管理的主要区域,因此很多时候也被称为GC堆(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和年老代。再在细致一点的划分可以分为:Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域存放的都是对象实例。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。Java堆可以处在物理上不连续的内存空间,只要逻辑上是连续的即可。在实现上既可以实现成固定大小,也可以是可扩展的大小,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存实例完成分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常。方法区方法区是各个线程共享的内存区域方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一部分,但是他还有个别名叫做Non-heap(非堆),目的应该是与Java堆区分开来。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久存在了。这区域的内存回收目标重要是针对常量池的回收和类型的卸载,一般来说这个内存区域的回收‘成绩’比较难以令人满意。尤其是类型的卸载条件非常苛刻,但是这部分的回收确实是必要的。在Sun公司的bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完成回收导致的内存溢出。注意:方法区与永久代对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价。仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如 BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步改为采用Native Memory来实现方法区的规划了,在目前已经发布的JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。运行时常量池(见1.2.2)直接内存直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。显然,本机直接内存的分配不会受到 Java 堆大小的限制。但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xms 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。1.2.2 常量池Class常量池Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。字面量(Literal):文本字符串(如String str = “SpiderLucas"中SpiderLucas就是字面量)、八种基本类型的值(如int i = 0中0就是字面量)、被声明为final的常量等;符号引用(Symbolic References):类和方法的全限定名、字段的名称和描述符、方法的名称和描述符。每个class文件都有一个class常量池。字符串常量池参考资料来源:Java中的常量池、彻底弄懂字符串常量池等相关问题、Java中String字符串常量池字符串常量池中的字符串只存在一份。字符串常量池(String Constant Pool)是存储Java String的一块内存区域,在JDK 6之前是放置于方法区的,在JDK 7之后放置于堆中。在HotSpot中实现的字符串常量池功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。StringTable的长度:在JDK 6中,StringTable的长度是固定的,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;在JDK 7中,StringTable的长度可以通过参数指定:-XX:StringTableSize=1024。字符串常量池中存放的内容:在JDK 6及之前版本中,String Pool里放的都是字符串常量;JDK 7中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。intern() 函数在JDK 6中,intern()的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量;如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量。在JDK 7中,intern()的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中。字符串常量池案例 String s1 = new String(“Spider”); // s1 -> 堆 // 该行代码创建了几个对象 // 两个对象(不考虑对象内部的对象):首先创建了一个字符串常量池的对象,然后创建了堆里的对象 s1.intern(); // 字符串常量池中存在"Spider”,直接返回该常量 String s2 = “Spider”; // s2 -> 字符串字符串常量池 System.out.println(s1 == s2); // false String s3 = new String(“Str”) + new String(“ing”); // s3 -> 堆 // 该行代码创建了几个对象? // 反编译后的代码:String s3 = (new StringBuilder()).append(new String(“Str”)).append(new String(“ing”)).toString(); // 六个对象(不考虑对象内部的对象):两个字符串常量池的对象"Str"和"ing",两个堆的对象"Str"和"ing",一个StringBuilder,一个toString方法创建的new String对象 s3.intern(); // 字符串常量池中没有,在JDK 7中以后会把堆中该对象的引用放在字符串常量池中(JDK 6中创建一个jdk1.6中会在字符串常量池中建立该常量) String s4 = “String”; // s4 -> 堆(JDK 6:s4 -> 字符串字符串常量池) System.out.println(s3 == s4); // true(JDK6 false) String s5 = “AAA”; String s6 = “BBB”; String s7 = “AAABBB”; // s7 -> 字符串常量池 String s8 = s5 + s6; // s8 -> 堆(原因就是如上字符串+的重载) String s9 = “AAA” + “BBB”; // JVM会对此代码进行优化,直接创建字符串常量池 System.out.println(s7 == s8); // false System.out.println(s7 == s9); // true(都指向字符串常量池)方法区与运行时常量池运行时常量池(Runtime Constant Pool)是方法区的一部分。Class常量池的内容将在类加载后进入方法区的运行时常量池中存放。Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置如Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。1.3 HotSpot中的对象1.3.1 对象的创建new一个对象的全部流程从常量池中查找该类的符号引用,并且检查该符号引用代表的类是否被加载、解析、初始化。如果类已经被加载,则跳转至3;否则跳转至2。执行类的加载过程。为新对象分配内存空间:由于对象所需要内存大小在类加载完成时可以确定,所以可以直接从Java堆中划分一块确定大小的内存。把分配的内存空间都初始化为零值(不包括对象头),如果使用TLAB则该操作可以提前至TLAB中,这是为了保证对象的字段都被初始为默认值。执行init方法,按照程序员的意愿进行初始化。对象分配内存空间详解指针碰撞:如果堆内存是规整,已经分配和为分配的内存有一个指针作为分界点,那么只需要将指针向空闲内存移动即可。空闲列表:如果内存是不规整的,虚拟机需要维护一个列表,记录那些内存块是可用的。在分配的时候从足够大的空间划分给对象,并更新该列表。Java堆是否规整取决于GC是否会压缩整理,Serial、ParNew等带Compact过程的收集器,分配算法是指针碰撞;是用CMS这种基于Mark-Sweep算法的收集器时,分配算法是空闲列表。分配内存的并发问题无论是指针碰撞还是空闲列表,都有可能因为并发而产生问题,解决方法有两种:对分配内存空间的动作进行同步处理——实际上JVM采用CAS(Compare And Swap)配上失败重试的方式保证更新操作的原子性。把内存分配的动作按照线程划分在不同的空间,每个线程在Java堆中预先分配一小块内存,成为本地缓冲内存(Tread Local Allocation Buffer,TLAB)。哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完了,才需要同步锁定。可以通过-XX:+/-UseTLAB参数来设定。CAS原理一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。1.3.2 对象的内存布局在HotSpot虚拟机中,对象在内存中的存储布局可以分为3部分:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)。对象头第一部分对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,原理是它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。 对象头第二部分对象头的第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息不一定要经过对象本身。如果对象是一个数组:对象头中还需要一块用于记录数组长度的数据。实例数据接下来实例数据部分是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的都需要记录下来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。对齐填充第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。1.3.3 对象的访问定位建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范里面只规定了是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的。对象的两种访问定位方式主流的访问方式有使用句柄和直接指针两种。句柄访问:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息,如下图所示。直接指针:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如下图所示。两种方式比较句柄访问的优势:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。直接指针的优势:最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。HotSpot虚拟机:它是使用第二种方式进行对象访问。但在整个软件开发的范围来看,各种语言、框架中使用句柄来访问的情况也十分常见。1.4 OOM异常分类1.4.1 堆溢出Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免GC,那么在对象数量到达最大堆容量限制之后便会产生堆溢出。/** * VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 1.将堆的最小值-Xms与最大值-Xmx参数设置为一样可以避免堆自动扩展 * 2.通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机出现内存异常时Dump当前堆内存堆转储快照 * 3.快照位置默认为user.dir /public class HeapOOM { static class OOMObject {} public static void main(String[] args) { // 保留引用,防止GC List<OOMObject> list = new ArrayList<>(); for (;;) { list.add(new OOMObject()); } }}// 运行结果// java.lang.OutOfMemoryError: Java heap space// Dumping heap to java_pid72861.hprof …// Heap dump file created [27888072 bytes in 0.086 secs]堆转储快照以下是JProfiler对转储快照的分析内存泄漏与内存溢出重点:确认内存中的对象是否是必要的,也就是分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用。内存泄漏的堆积最终会导致内存溢出。内存泄漏的分类(按发生方式来分类)常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。处理方式如果是内存泄漏:需要找到泄漏对象的类型信息,和对象与GC Roots的引用链的信息,分析GC无法自动回收它们的原因。如果不存在内存泄漏,即内存中的对象的确必须存活:那就应当检查JVM的参数能否调大;从代码上检查是否某些对象生命周期过长、持有状态时间过长,尝试减少程序运行期的内存消耗。1.4.2 栈溢出在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的。栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。这里把异常分成两种情况,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。StackOverflowError/* * VM args: -Xss256k * 1. 设置-Xss参数减小栈内存 * 2. 死递归增大此方法栈中本地变量表的长度 /public class SOF { int stackLength = 1; void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { SOF sof = new SOF(); try { sof.stackLeak(); } catch (Throwable e) { System.out.println(“Stack Length:” + sof.stackLength); throw e; } }}// Stack Length:2006// Exception in thread “main” java.lang.StackOverflowError// at s1.SOF.stackLeak(SOF.java:13)// at s1.SOF.stackLeak(SOF.java:13)多线程导致栈OOM异常/* * VM Args: -Xss20M * 通过不断创建线程的方式产生OOM /public class StackOOM { private void dontStop() { for (;;) { } } private void stackLeakByThread() { for (;;) { Thread thread = new Thread(this::dontStop); thread.start(); } } public static void main(String[] args) { new StackOOM().stackLeakByThread(); }}通过不断创建线程的方式产生OOM异常,但是这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系。或者准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。原因:操作系统分配给每个进程的内存是有限制的,假设操作系统的内存为2GB,剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。所以每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。解决方法:如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过“减少内存”的手段来解决内存溢出——减少最大堆和减少栈容量来换取更多的线程。1.4.3 方法区和运行时常量池溢出由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,所以对于动态生成类的情况比较容易出现永久代的内存溢出。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。/* * (JDK 8)VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10m * (JDK 7之前)VM Args: -XX:PermSize=10M -XX:MaxPermSize=10m /public class MethodAreaOOM { static class OOMClass {} public static void main(final String[] args) { for (;;) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMClass.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, objects); } }); enhancer.create(); } }}// Exception in thread “main” java.lang.OutOfMemoryError: Metaspace// at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)// at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)// at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)// at com.ankeetc.commons.Main.main(Main.java:28)方法区溢出场景方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景主要有:使用了CGLib字节码增强,当前的很多主流框架,如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等JVM上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性1.4.4 本机直接内存溢出下面代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。/* * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * DirectMemory容量可通过-XX:MaxDirectMemorySize指定 * 如果不指定,则默认与Java堆最大值(-Xmx指定)一样。 */public class Main { private static final long _1024MB = 1024 * 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1024MB); } }}// Exception in thread “main” java.lang.OutOfMemoryError// at sun.misc.Unsafe.allocateMemory(Native Method)// at com.ankeetc.commons.Main.main(Main.java:25)DirectMemory特征由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。1.5 不同版本的JDK参考资料Java8内存模型—永久代(PermGen)和元空间(Metaspace)JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)关于永久代和方法区在 HotSpot VM 中 “PermGen Space” 其实指的就是方法区“PermGen Space” 和方法区有本质的区别。前者是 JVM 规范的一种实现(HotSpot),后者是 JVM 的规范。只有 HotSpot 才有 “PermGen Space”,而对于其他类型的虚拟机,如 JRockit、J9并没有 “PermGen Space”。不同版本JDK总结JDK 7之后将字符串常量池由永久代转移到堆中JDK 8中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。-XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载。同时GC会对该值进行调整——如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize:最大空间,默认是没有限制的。-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集PermSize、MaxPermSize参数已移除 ...

December 18, 2018 · 3 min · jiezi

一次生产 CPU 100% 排查优化实践

前言到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。还真是想什么来什么,前些天还故意把某些服务器的负载提高(没错,老板让我写个 BUG!),不过还好是不同的环境互相没有影响。定位问题拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 ps 命令拿到了应用的 PID。接着使用 ps -Hp pid 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。果然某些线程的 CPU 使用率非常高。为了方便定位问题我立马使用 jstack pid > pid.log 将线程栈 dump 到日志文件中。我在上面 100% 的线程中随机选了一个 pid=194283 转换为 16 进制(2f6eb)后在线程快照中查询:因为线程快照中线程 ID 都是16进制存放。发现这是 Disruptor 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 OOM:强如 Disruptor 也发生内存溢出?没想到又来一出。为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。http://fastthread.io/其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。也就是说都是 Disruptor 队列的堆栈,同时都在执行 java.lang.Thread.yield 函数。众所周知 yield 函数会让当前线程让出 CPU 资源,再让其他线程来竞争。根据刚才的线程快照发现处于 RUNNABLE 状态并且都在执行 yield 函数的线程大概有 30几个。因此初步判断为大量线程执行 yield 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 Disruptor 有关。解决问题而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 Disruptor 队列来解耦。假设现在有 7 个业务类型,那就等于是创建 2*7=14 个 Disruptor 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。同时发现配置的消费等待策略为 YieldingWaitStrategy 这种等待策略确实会执行 yield 来让出 CPU。代码如下:初步看来和这个等待策略有很大的关系。本地模拟为了验证,我在本地创建了 15 个 Disruptor 队列同时结合监控观察 CPU 的使用情况。创建了 15 个 Disruptor 队列,同时每个队列都用线程池来往 Disruptor队列 里面发送 100W 条数据。消费程序仅仅只是打印一下。跑了一段时间发现 CPU 使用率确实很高。同时 dump 线程发现和生产的现象也是一致的:消费线程都处于 RUNNABLE 状态,同时都在执行 yield。通过查询 Disruptor 官方文档发现:YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用自旋 + yield的方式来提高性能。当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。同时查阅到其他的等待策略 BlockingWaitStrategy (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。于是在和之前同样的条件下将等待策略换为 BlockingWaitStrategy。和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。优化解决看样子将等待策略换为 BlockingWaitStrategy 可以减缓 CPU 的使用,但留意到官方对 YieldingWaitStrategy 的描述里谈道:当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 Disruptor 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 YieldingWaitStrategy)。跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。总结所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 Disruptor 队列。由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。所以我们的调整方式如下:为了快速缓解这个问题,先将等待策略换为 BlockingWaitStrategy,可以有效降低 CPU 的使用率(业务上也还能接受)。第二步就需要将应用拆分(上文模拟的一个 Disruptor 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。本文的演示代码已上传至 GitHub:https://github.com/crossoverJie/JCSprout你的点赞与分享是对我最大的支持 ...

December 17, 2018 · 1 min · jiezi

没错,老板让我写个 BUG!

前言标题没有看错,真的是让我写个 bug!刚接到这个需求时我内心没有丝毫波澜,甚至还有点激动。这可是我特长啊;终于可以光明正大的写 bug 了????。先来看看具体是要干啥吧,其实主要就是要让一些负载很低的服务器额外消耗一些内存、CPU 等资源(至于背景就不多说了),让它的负载可以提高一些。JVM 内存分配回顾于是我刷刷一把梭的就把代码写好了,大概如下:写完之后我就在想一个问题,代码中的 mem 对象在方法执行完之后会不会被立即回收呢?我想肯定会有一部分人认为就是在方法执行完之后回收。我也正儿八经的去调研了下,问了一些朋友;果不其然确实有一部分认为是在方法执行完毕之后回收。那事实情况如何呢?我做了一个试验。我用以下的启动参数将刚才这个应用启动起来。java -Djava.rmi.server.hostname=10.xx.xx.xx -Djava.security.policy=jstatd.all.policy -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=8888 -Xms4g -Xmx4g -jar bug-0.0.1-SNAPSHOT.jar这样我就可以通过 JMX 端口远程连接到这个应用观察内存、GC 情况了。如果是方法执行完毕就回收 mem 对象,当我分配 250M 内存时;内存就会有一个明显的曲线,同时 GC 也会执行。这时观察内存曲线。会发现确实有明显的涨幅,但是之后并没有立即回收,而是一直保持在这个水位。同时左边的 GC 也没有任何的反应。用 jstat 查看内存布局也是同样的情况。不管是 YGC,FGC 都没有,只是 Eden 区的使用占比有所增加,毕竟分配了 250M 内存嘛。那怎样才会回收呢?我再次分配了两个 250M 之后观察内存曲线。发现第三个 250M 的时候 Eden 区达到了 98.83% 于是再次分配时就需要回收 Eden 区产生了 YGC。同时内存曲线也得到了下降。整个的换算过程如图:由于初始化的堆内存为 4G,所以算出来的 Eden 区大概为 1092M 内存。加上应用启动 Spring 之类消耗的大约 20% 内存,所以分配 3 次 250M 内存就会导致 YGC。再来回顾下刚才的问题:mem 对象既然在方法执行完毕后不会回收,那什么时候回收呢。其实只要记住一点即可:对象都需要垃圾回收器发生 GC 时才能回收;不管这个对象是局部变量还是全局变量。通过刚才的实验也发现了,当 Eden 区空间不足产生 YGC 时才会回收掉我们创建的 mem 对象。但这里其实还有一个隐藏条件:那就是这个对象是局部变量。如果该对象是全局变量那依然不能被回收。也就是我们常说的对象不可达,这样不可达的对象在 GC 发生时就会被认为是需要回收的对象从而进行回收。在多考虑下,为什么有些人会认为方法执行完毕后局部变量会被回收呢?我想这应当是记混了,其实方法执行完毕后回收的是栈帧。它最直接的结果就是导致 mem 这个对象没有被引用了。但没有引用并不代表会被马上回收,也就是上面说到的需要产生 GC 才会回收。所以使用的是上面提到的对象不可达所采用的可达性分析算法来表明哪些对象需要被回收。当对象没有被引用后也就认为不可达了。这里有一张动图比较清晰:当方法执行完之后其中的 mem 对象就相当于图中的 Object 5,所以在 GC 时候就会回收掉。优先在 Eden 区分配对象其实从上面的例子中可以看出对象是优先分配在新生代中 Eden 区的,但有个前提就是对象不能太大。以前也写过相关的内容:大对象直接进入老年代而大对象则是直接分配到老年代中(至于多大算大,可以通过参数配置)。当我直接分配 1000M 内存时,由于 Eden 区不能直接装下,所以改为分配在老年代中。可以看到 Eden 区几乎没有变动,但是老年代却涨了 37% ,根据之前计算的老年代内存 2730M 算出来也差不多是 1000M 的内存。Linux 内存查看回到这次我需要完成的需求:增加服务器内存和 CPU 的消耗。CPU 还好,本身就有一定的使用,同时每创建一个对象也会消耗一些 CPU。主要是内存,先来看下没启动这个应用之前的内存情况。大概只使用了 3G 的内存。启动应用之后大概只消耗了 600M 左右的内存。为了满足需求我需要分配一些内存,但这里有点需要讲究。不能一直分配内存,这样会导致 CPU 负载太高了,同时内存也会由于 GC 回收导致占用也不是特别多。所以我需要少量的分配,让大多数对象在新生代中,为了不被回收需要保持在百分之八九十。同时也需要分配一些大对象到老年代中,也要保持老年代的使用在百分之八九十。这样才能最大限度的利用这 4G 的堆内存。于是我做了以下操作:先分配一些小对象在新生代中(800M)保持新生代在90%接着又分配了老年代内 (100%-已使用的28%);也就是 273060%=1638M 让老年代也在 90% 左右。效果如上。最主要的是一次 GC 都没有发生这样也就达到了我的目的。最终内存消耗了 3.5G 左右。总结虽说这次的需求是比较奇葩,但想要精确的控制 JVM 的内存分配还是没那么容易。需要对它的内存布局,回收都要有一定的了解,写这个 Bug 的过程确实也加深了印象,如果对你有所帮助请不要吝啬你的点赞与分享。你的点赞与分享是对我最大的支持 ...

December 12, 2018 · 1 min · jiezi

猫头鹰的深夜翻译:JDK Vs. JRE Vs. JVM之间的区别

什么是Java Development Kit (JDK)?JDK通常用来开发Java应用和插件。基本上可以认为是一个软件开发环境。JDK包含Java Runtime Environment(JRE),JRE包含加载器/解释器,编译器(javac),文档生成器(Javadoc),打包功能(jar)和其它在开发中所需要功能:加载代码校验代码执行代码提供运行时环境什么是Java Runtime Environment(JRE)Java Runtime Environment(JRE)又称为Java RTE。JRE中包含核心类和支持文件。它还包含JVM。JVM会提供运行时环境。确定JVM运行的特定类型。其类型主要由Sun和其它的几个机构提供。其实现是一个满足JVM特定前提条件的客户端程序。运行时实例无论何时运行Java类,都会产生JVM。JDK一个物理存在的工具包。它包含JRE和其他工具。什么是Java Virtual Machine(JVM)JVM为执行Java字节码提供一个运行环境。它是一个抽象的独立于平台运行的机器。它的实现主要包含三个部分,描述JVM实现规格的文档,具体实现和满足JVM要求的计算机程序以及实例(具体执行Java字节码)。JVM的主要任务包括:加载代码校验代码执行代码提供运行时环境JDK, JRE和JVM之间的区别JRE的组成部署机制:Java Web Start, Java插件等UI工具包:AWT,Swing,Java2D等集成库:IDL,JDBC,RMI等其它基础库:I/O,JNI,JMX等Lang和utils基础库:lang,util,格式化,序列化,打包等JVM:Java HotSpot客户端和服务端虚拟机JRE功能为了了解JRE的功能,可以看一下是如何加载Example.class这个类的。该类先被转化为一组字节码并放入.class文件中。Java ClassLoaderClassLoader将执行程序所需的每个重要类放入堆栈中。它通过命名系统来提供彼此之间的安全性。源码可以来自于硬盘,系统以及其它来源。Java 字节码校验器JVM通过字节码校验器检查格式并找出非法代码。校验器确JVM执行代码时能够够快,以及这段代码不会损害现存的框架。Java解释器解释器有两个功能:执行字节码正确调用隐藏的设备

November 24, 2018 · 1 min · jiezi

不改一行代码定位线上性能问题

背景最近时运不佳,几乎天天被线上问题骚扰。前几天刚解决了一个 HashSet 的并发问题,周六又来了一个性能问题。大致的现象是:我们提供出去的一个 OpenAPI 反应时快时慢,快的时候几十毫秒,慢的时候几秒钟才响应。尝试解决由于这种也不是业务问题,不能直接定位。所以尝试在测试环境复现,但遗憾的测试环境贼快。没办法只能硬着头皮上了。中途有抱着侥幸心里让运维查看了 Nginx 里 OpenAPI 的响应时间,想把锅扔给网络。结果果然打脸了;Nginx 里的日志也表明确实响应时间确实有问题。为了清晰的了解这个问题,我简单梳理了这个调用过程。整个的流程算是比较常见的分层架构:客户端请求到 Nginx。Nginx 负载了后端的 web 服务。web 服务通过 RPC 调用后端的 Service 服务。日志大法我们首先想到的是打日志,在可能会慢的方法或接口处记录处理时间来判断哪里有问题。但通过刚才的调用链来说,这个请求流程不短。加日志涉及的改动较多而且万一加漏了还有可能定位不到问题。再一个是改动代码之后还会涉及到发版上线。工具分析所以最好的方式就是不改动一行代码把这个问题分析出来。这时就需要一个 agent 工具了。我们选用了阿里以前开源的 Tprofile 来使用。只需要在启动参数中加入 -javaagent:/xx/tprofiler.jar 即可监控你想要监控的方法耗时,并且可以给你输出报告,非常方便。对代码没有任何侵入性同时性能影响也较小。工具使用下面来简单展示下如何使用这个工具。首先第一步自然是 clone 源码然后打包,可以克隆我修改过的源码。因为这个项目阿里多年没有维护了,还残留一些 bug,我在它原有的基础上修复了个影响使用的 bug,同时做了一些优化。执行以下脚本即可。git clone https://github.com/crossoverJie/TProfilermvn assembly:assembly到这里之后会在项目的 TProfiler/pkg/TProfiler/lib/tprofiler-1.0.1.jar 中生成好我们要使用的 jar 包。接下来只需要将这个 jar 包配置到启动参数中,同时再配置一个配置文件路径即可。这个配置文件我 copy 官方的解释。#log file namelogFileName = tprofiler.logmethodFileName = tmethod.logsamplerFileName = tsampler.log#basic configuration items# 开始取样时间startProfTime = 1:00:00# 结束取样时间endProfTime = 23:00:00# 取样的时间长度eachProfUseTime = 10# 每次取样的时间间隔eachProfIntervalTime = 1samplerIntervalTime = 20# 端口,主要不要冲突了port = 50000debugMode = falseneedNanoTime = false# 是否忽略 get set 方法ignoreGetSetMethod = true#file paths 日志路径logFilePath = /data/work/logs/tprofile/${logFileName}methodFilePath =/data/work/logs/tprofile/${methodFileName}samplerFilePath =/data/work/logs/tprofile/${samplerFileName}#include & excludes itemsexcludeClassLoader = org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader# 需要监控的包includePackageStartsWith = top.crossoverjie.cicada.example.action# 不需要监控的包excludePackageStartsWith = com.taobao.sketch;org.apache.velocity;com.alibaba;com.taobao.forest.domain.dataobject最终的启动参数如下:-javaagent:/TProfiler/lib/tprofiler-1.0.1.jar-Dprofile.properties=/TProfiler/profile.properties为了模拟排查接口响应慢的问题,我用 cicada 实现了一个 HTTP 接口。其中调用了两个耗时方法:这样当我启动应用时,Tprofile 就会在我配置的目录记录它所收集的方法信息。我访问接口 http://127.0.0.1:5688/cicada-example/demoAction?name=test&id=10 几次后它就会把每个方法的明细响应写入 tprofile.log。由左到右每列分别代表为:线程ID、方法栈深度、方法编号、耗时(毫秒)。但 tmethod.log 还是空的;这时我们只需要执行这个命令即可把最新的方法采样信息刷到 tmethod.log 文件中。java -cp /TProfiler/tprofiler.jar com.taobao.profile.client.TProfilerClient 127.0.0.1 50000 flushmethodflushmethod success其实就是访问了 Tprofile 暴露出的一个服务,他会读取、解析 tprofile.log 同时写入 tmethod.log.其中的端口就是配置文件中的 port。再打开 tmethod.log :其中会记录方法的信息。第一行数字为方法的编号。可以通过这个编号去 tprofile.log(明细)中查询每次的耗时情况。行末的数字则是这个方法在源码中最后一行的行号。其实大部分的性能分析都是统计某个方法的平均耗时。所以还需要执行下面的命令,通过 tmethod.log tprofile.log 来生成每个方法的平均耗时。java -cp /TProfiler/tprofiler.jar com.taobao.profile.analysis.ProfilerLogAnalysis tprofiler.log tmethod.log topmethod.log topobject.logprint result success打开 topmethod.log 就是所有方法的平均耗时。4 为请求次数。205 为平均耗时。818 则为总耗时。和实际情况是相符的。方法的明细耗时这是可能还会有其他需求;比如说我想查询某个方法所有的明细耗时怎么办呢?官方没有提供,但也是可以的,只是要麻烦一点。比如我想查看 selectDB() 的耗时明细:首先得知道这个方法的编号,在 tmethod.log 中可以看查到。2 top/crossoverjie/cicada/example/action/DemoAction:selectDB:84编号为 2.之前我们就知道 tprofile.log 记录的是明细,所以通过下面的命令即可查看。grep 2 tprofiler.log通过第三列方法编号为 2 的来查看每次执行的明细。但这样的方式显然不够友好,需要人为来过滤干扰,步骤也多;所以我也准备加上这样一个功能。只需要传入一个方法名称即可查询采集到的所有方法耗时明细。总结回到之前的问题;线上通过这个工具分析我们得到了如下结果。有些方法确实执行时快时慢,但都是和数据库相关的。由于目前数据库压力较大,准备在接下来进行冷热数据分离,以及分库分表。在第一步操作还没实施之前将部分写数据库的操作改为异步,减小响应时间。考虑接入 pinpoint 这样的 APM工具。类似于 Tprofile 的工具确实挺多的,找到适合自己的就好。在还没有使用类似于 pinpoint 这样的分布式跟踪工具之前应该会大量依赖于这个工具,所以后续说不定也会做一些定制,比如增加一些可视化界面等,可以提高排查效率。你的点赞与分享是对我最大的支持 ...

November 12, 2018 · 1 min · jiezi

乐观锁、悲观锁,这一篇就够了!

乐观锁乐观锁顾名思义就是在操作时很乐观,认为操作不会产生并发问题(不会有其他线程对数据进行修改),因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS(compare and swap)算法实现。简单理解:这里的数据,别想太多,你尽管用,出问题了算我怂,即操作失败后事务回滚、提示。1.1 版本号机制1.1.1 实现套路:取出记录时,获取当前version更新时,带上这个version执行更新时, set version = newVersion where version = oldVersion如果version不对,就更新失败核心SQL:update table set name = ‘Aron’, version = version + 1 where id = #{id} and version = #{version}; 1.1.2 实例-Mybatis-plus 乐观锁实现原文查看请点击 Mybatis-plus 乐观锁实现1.2 CAS算法乐观锁的另一种技术技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 操作中包含三个操作数 :需要读写的内存位置V进行比较的预期原值A拟写入的新值B如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值)。CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。1.2.1 实例-concurrent包的实现由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:A线程写volatile变量,随后B线程读这个volatile变量。A线程写volatile变量,随后B线程用CAS更新这个volatile变量。A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:首先,声明共享变量为volatile; 然后,使用CAS的原子条件更新来实现线程之间的同步;同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程1.2.2 缺点ABA问题比如说一个线程T1从内存位置V中取出A,这时候另一个线程T2也从内存中取出A,并且T2进行了一些操作变成了B,然后T2又将V位置的数据变成A,这时候线程T1进行CAS操作发现内存中仍然是A,然后T1操作成功。尽管线程T1的CAS操作成功,但可能存在潜藏的问题。循环时间长开销大自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。只能保证一个共享变量的原子操作当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。从Java 1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。2. 悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加(悲观)锁。一旦加锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。悲观锁在MySQL、Java有广泛的使用MySQL的读锁、写锁、行锁等Java的synchronized关键字3. 总结读的多,冲突几率小,乐观锁。写的多,冲突几率大,悲观锁。高并发,悲观锁。觉得有用记得收藏、点赞哦!

October 9, 2018 · 1 min · jiezi

Java程序员进阶必备 - JVM快速入门

这是我在公司给团队小伙伴一次技术小分享。新手司机可以收藏、学习,老司机可以批评指正。ps:内容参考了众多优秀博文、书籍,部分图片来源于博文,如有侵权请联系删除。1. 前言为什么Java可以实现所谓的“一次编写,到处运行”,主要是因为虚拟机的存在。Java虚拟机负责Java程序设计语言的安全特性和平台无关性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译器只需要生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。Java虚拟机使得Java摆脱了具体机器的束缚,使跨越不同平台编写程序成为了可能。Java虚拟机基本上都是JDK自带的虚拟机HotSpot,这款虚拟机也是目前商用虚拟中市场份额最大的一款虚拟机,可以通过在命令行程序中输入java -version来查看:其实市面上还有很多别的优秀的虚拟机。Sun公司除了有大名鼎鼎的HotSpot外,还有KVM、Squawk VM、Maxine VM,BEA公司有JRockit VM、IBM公司有J9 VM等等。2. 内存模型(JMM)Java虚拟机(JVM)内部定义了程序在运行时需要使用到的内存区域。内存区域主要分为主内存和工作内存。主内存即主机物理内存,工作内存按作用域可划分为线程独享区和线程共享区。宏观来看是这样子的,如下图:Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量和主内存副本拷贝,线程对变量所有的操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,Jvm运行时内存模型,不包含主内存。如下图:下面将会逐一详细介绍上面的内存区域。2.1 线程独享区虚拟机栈,即Java stack。声明周期和线程相同,方法执行会创建栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。本地方法栈,即Native method stack。作用和虚拟机栈一样,不过面向的是本地方法。不属于jvm规范,hotspot没有这块区域。程序计数器,即Program counter register。区域小,是线程执行的字节码的行号指示器,相当于存的是一条条的指令。2.2 线程共享区堆用于存放对象实例,是所有内存区域中最大的一块。实际上这块内存还被划分的更细:新生代和老年代,空间占用比例为1 : 2,新生代再细致一点有:Eden空间、From Survivor(S0)、To Survivor(S1),空间占用比例为8 : 1 : 1。进一步划分的目的是更好地回收内存,或者更快地分配内存。方法区用于存放虚拟机加载的类信息、常量(常量池)、静态变量、即使编译器编译后的代码等数据,即“HotSpot”的永久代。在JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念,采用的是Native Memory来实现方法区的规划。2.3 直接内存直接内存,即主内存,并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。JDK1.4中新加入的NIO(New Input/Output)类,可以直接使用Native函数库直接分配堆外内存,这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。3. 垃圾回收(GC)哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何确定要回收的对象,以及采用什么样的策略去回收,适合什么样的场景,这是我们要关注的几个点。3.1 确定对象算法了解一个对象满足什么样的条件就认为是可被回收的对象是重要的一环。3.1.1 引用计数法给对象添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。当计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。public class ReferenceCountingGC{ public static void main(String[] args){ ReferenceCountingGC objectA = new ReferenceCountingGC(); ReferenceCountingGC objectB = new ReferenceCountingGC(); objectA.instance = objectB; objectB.instance = objectA; }}3.1.2 可达性分析法这个算法的基本思想是通过一系列称为GC Roots的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。在Java语言中可以作为GC Roots的对象包括:虚拟机栈中引用的对象方法区中静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI(即Native方法)引用的对象3.2 回收算法3.2.1 复制算法采用内存空间比例为1 : 1的2块内存上,只使用其中一块,当需要回收时,将存活的对象复制到另外一块,原有的那一块内存空间直接全部清除。这种算法比较简单粗暴,缺点也很明显,内存只能使用1/2。3.2.2 标记-清除算法对标识为可清理的对象直接进行清理操作,不会发生复制或者移动,相对复制算法成本比较小。缺点:对标记的对象清除之后,由于未移动过对象,将产生大量不连续的内存碎片,当大对象出现时,由于没有足够的连续内存导致不得不对碎片进行整理,也就是Full GC。3.2.3 标记-整理算法标记-整理算法能够解决标记-清除算法带来的碎片化问题3.3 垃圾收集器根据上面提到的回收算法,jvm内置了拥有众多的收集器来适应不同的场景。根据运行环境的物理配置信息,会自动的选择使用client模式、server模式的垃圾收集器,还可以继续根据运行时数据的情况来筛选适合当前场景的垃圾收集器。上图展示了新生代和老年代的几种垃圾收集器,其中有连线的代表是可以组合使用的。4. jvm参数4.1 参数规则标准参数:例如 javap -verbose-X参数:所有的这类参数都以-X开始,例如常用的-Xmx,布尔类型的参数: +或-,然后才设置JVM选项的实际名称。例如,-XX:+类似true,表启用,-XX:-类似false。非布尔值的参数:如string或者integer,我们先写参数的名称,后面加上=,最后赋值。例如 -XX:ParamName=Value4.2 常用参数清单-Xms 即 -XX:InitialHeapSize的缩写,指定JVM的初始内存大小-Xms20M 设置JVM启动内存的最小值为20M,必须以M为单位-Xmx 即 -XX:MaxHeapSize的缩写,指定JVM的最大堆内存大小-Xmx20M 表示设置JVM启动内存的最大值为20M,单位为M,将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展。-verbose:gc 输出虚拟机中GC的详细情况-Xss128k 设置虚拟机栈的大小为128k-Xoss128k 设置本地方法栈的大小为128k。HotSpot不区分虚拟机栈和本地方法栈,因此对于HotSpot这个参数无效。-XX:PermSize=10M JVM初始分配的永久代的容量,必须以M为单位-XX:MaxPermSize=10M JVM允许分配的永久代的最大容量,必须以M为单位,大部分情况下这个参数默认为64M-Xnoclassgc 关闭JVM对类的垃圾回收-XX:+TraceClassLoading 查看类的加载信息-XX:+TraceClassUnLoading 查看类的卸载信息-XX:NewRatio=4 设置年轻代:老年代的大小比值为1:4,这意味着年轻代占整个堆的1/5-XX:SurvivorRatio=8 设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8-Xmn20M 设置年轻代的大小为20M-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照-XX:+UseG1GC 让JVM使用G1垃圾收集器-XX:+PrintGCDetails 在控制台上打印出GC具体细节-XX:+PrintGC 在控制台上打印出GC信息-XX:PretenureSizeThreshold=3145728 对象大于3145728(3M)时直接进入老年代分配,单位为byte-XX:MaxTenuringThreshold=1 对象年龄大于1,自动进入老年代-XX:CompileThreshold=1000 一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译-XX:+PrintHeapAtGC 可以看到每次GC前后堆内存布局-XX:+PrintTLAB 可以看到TLAB的使用情况-XX:+UseSpining 开启自旋锁-XX:PreBlockSpin 更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁4.3 使用参数命令行java -jar projectName.jar -verbose:gc -Xms20M -Xmx20MEclipseIDEA5. 常用工具所谓工具,就是通过一些简便的脚本去执行程序去呈现结果数据。这里涉及到一些语法格式。统一语法都类似这种形式:$ cmd [option id[ pid | vmid |hostid ]]其中hostid为可选项,默认为localgost, vmid/pid依赖jps获取5.1 jpsjps是Java Process Status的缩写,查看当前java进程的运行状态快照。理解为linux命令ps的java版本-m 运行时传入的参数-v 虚拟机参数-l 运行的主类全限定名或jar包名称示例jps -mlv5.2 jstatjstat 是JVM Statistics Monitoring Tool的缩写,查看虚拟机统计信息监控数据,如类信息、内存、垃圾收集、JIT编译等-gc 显示gc的信息,查看gc次数以及时间-class 监视类装载、卸载数量、总空间以及类装载所耗费的时间-gc 监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久带等的容量、已用空间、GC时间合计等信息-gccapacity 监视内容基本与-gc相同,但输出主要关注Java堆各个区域使用到的最大、最小空间-gcutil 监视内容基本与-gc相同,但输出主要关注已使用的空间占总空间的百分比-gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因-gcnew 监视新生代GC状况-gcnewcapacity 监视内容基本与-gcnew相同,但输出主要关注使用到的最大、最小空间-gcold 监视老年代GC状况-gcoldcapacity 监视内容基本与-gcold相同,但输出主要关注使用到的最大、最小空间-gcpermcapacity 输出永久代使用到的最大、最小空间-compiler 输出JIT编译器编译过的方法、耗时等信息-printcompilation 输出已经被JIT编译的方法jstat -gcutil pid 依赖jps获得pid查看类装载、内存、垃圾收集、jit编译信息示例jstat -gcutil 3333 1000 10 对pid为3333的进程每隔1秒打印1次,总打印10次5.3 jinfojinfo 即Configuration Info for Java,实时查看和调整jvm参数-flag <name> 打印jvm参数的值-flag [+|-]<name> 启用/禁用jvm参数-flag <name>=<value> 修改jvm参数值-flags <pid> 打印所有jvm参数值-sysprops <pid> 打印java系统属性<no option> <pid> 打印上面所有信息示例jinfo -flags 7298 打印pid为7298的虚拟机运行时的所有参数xxx5.4 jmapjmap 即Memory Map for Java,内存映像工具用于生成堆转存快照-dump 生成Java堆转储快照。格式为-dump:[live, ]format=b,file=<filename>,其中live自参数说明是否只dump出存活的对象-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux和Solaris系统有效-heap 显示Java堆详细信息,如使用哪种收集器、参数配置、分代状况等。只在Linux和Solaris系统有效-histo 显示堆中对象统计信息,包括类、实例数量、合计容量-permstat 以ClassLoader为统计口径显示永久代内存状态。只在Linux和Solaris系统下有效-F 当虚拟机进行对-dump选项没有响应时,可使用这个选项强制生成dump快照。只在Linux和Solaris系统下有效示例jmap -dump:live,format=b,file=heap.bin 7298 将pid为7298的虚拟机内活对象导出为heap.bin二进制文件5.5 jhatjhat即JVM Heap Analysis Tool,虚拟机堆分析工具xxx示例jhat /data/dump.bin 分析导出的堆快照5.6 jstackjstack 即Stack Trace for Java, 堆栈跟踪工具,查看虚拟机线程快照。目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。-F 即force,强制打印线程快照信息-m 即mixed mode,同时打印java框架信息和本地库信息-l 即long listing,打印更长(更多)的列信息示例jstack -F 7298jstack -l 7298jstack -m 7298 ...

October 1, 2018 · 1 min · jiezi

简单理解:JVM为什么需要GC'

社区内有人发起了一个讨论,关于JVM是否一定需要GC?他们认为应用程序的回收目标是构建一个仅用来处理内存分配,而不执行任何真正的内存回收操作的 GC。即仅当可用的 Java 堆耗尽的时候,才进行顺序的 JVM 停顿操作。首先需要理解为什么需要GC。随着应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。社区的需求是尽量减少对应用程序的正常执行干扰,这也是业界目标。Oracle在JDK7时发布G1 GC的目的是为了减少应用程序停顿发生的可能性,让我们通过本文来了解G1 GC所做的工作。JVM发展历史简介还记得机器猫吗?他和康夫有一张书桌,书桌的抽屉其实是一个时空穿梭通道,让我们操作机器猫的时空机器,回到1998年。那年的12月8日,第二代Java平台的企业版J2EE正式对外发布。为了配合企业级应用落地,1999年4月27日,Java程序的舞台—Java HotSpot Virtual Machine(以下简称HotSpot )正式对外发布,并从这之后发布的JDK1.3版本开始,HotSpot成为Sun JDK的默认虚拟机。GC发展历史简介1999年随JDK1.3.1一起来的是串行方式的Serial GC ,它是第一款GC,并且这只是起点。此后,JDK1.4和J2SE1.3相继发布。2002年2月26日,J2SE1.4发布,Parallel GC 和Concurrent Mark Sweep (CMS)GC跟随JDK1.4.2一起发布,并且Parallel GC在JDK6之后成为HotSpot默认GC。HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?请记住以下口令:如果你想要最小化地使用内存和并行开销,请选Serial GC;如果你想要最大化应用程序的吞吐量,请选Parallel GC;如果你想要最小化GC的中断或停顿时间,请选CMS GC。那么问题来了,既然我们已经有了上面三个强大的GC,为什么还要发布Garbage First(G1)GC?原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。为什么名字叫做Garbage First(G1)呢?因为G1是一个并行回收器,它把堆内存分割为很多不相关的区间(Region),每个区间可以属于老年代或者年轻代,并且每个年龄代区间可以是物理上不连续的。老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象。而这样就会产生一种现象,即某些区间的垃圾(未被引用对象)多于其他的区间。垃圾回收时实则都是需要停下应用程序的,不然就没有办法防治应用程序的干扰 ,然后G1 GC可以集中精力在垃圾最多的区间上,并且只会费一点点时间就可以清空这些区间里的垃圾,腾出完全空闲的区间。绕来绕去终于明白了,由于这种方式的侧重点在于处理垃圾最多的区间,所以我们给G1一个名字:垃圾优先(Garbage First)。G1 GC基本思想G1 GC是一个压缩收集器,它基于回收最大量的垃圾原理进行设计。G1 GC利用递增、并行、独占暂停这些属性,通过拷贝方式完成压缩目标。此外,它也借助并行、多阶段并行标记这些方式来帮助减少标记、重标记、清除暂停的停顿时间,让停顿时间最小化是它的设计目标之一。G1回收器是在JDK1.7中正式投入使用的全新的垃圾回收器,从长期目标来看,它是为了取代CMS 回收器。G1回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区,但从堆的结构上看,它并不要求整个Eden区、年轻代或者老年代在物理上都是连续。综合来说,G1使用了全新的分区算法,其特点如下所示:并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力;并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况;分代GC:G1依然是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。随着G1 GC的出现,GC从传统的连续堆内存布局设计,逐渐走向不连续内存块,这是通过引入Region概念实现,也就是说,由一堆不连续的Region组成了堆内存。其实也不能说是不连续的,只是它从传统的物理连续逐渐改变为逻辑上的连续,这是通过Region的动态分配方式实现的,我们可以把一个Region分配给Eden、Survivor、老年代、大对象区间、空闲区间等的任意一个,而不是固定它的作用,因为越是固定,越是呆板。G1 GC垃圾回收机制通过市场的力量,不断淘汰旧的行业,把有限的资源让给那些竞争力更强、利润率更高的企业。类似地,硅谷也在不断淘汰过时的人员,从全世界吸收新鲜血液。经过半个多世纪的发展,在硅谷地区便形成只有卓越才能生存的文化。本着这样的理念,GC承担了淘汰垃圾、保存优良资产的任务。G1 GC在回收暂停阶段会回收最大量的堆内区间(Region),这是它的设计目标,通过回收区间达到回收垃圾的目的。这里只有一个例外情况,这个例外发生在并行标记阶段的清除(Cleanup)步骤,如果G1 GC在清除步骤发现所有的区间都是由可回收垃圾组成的,那么它会立即回收这些区间,并且将这些区间插入到一个基于LinkedList实现的空闲区间队列里,以待后用。因此,释放这些区间并不需要等待下一个垃圾回收中断,它是实时执行的,即清除阶段起到了最后一道把控作用。这是G1 GC和之前的几代GC的一大差别。G1 GC的垃圾回收循环由三个主要类型组成:年轻代循环多步骤并行标记循环混合收集循环Full GC在年轻代回收期,G1 GC暂停应用程序线程,然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。G1的区间设计灵感为了加快GC的回收速度,HotSpot的历代GC都有自己的不同的设计方案,区间概念在软件设计、架构领域并不是一个新名词,关系型数据库、列式数据库最先使用这个概念提升数据存、取速度,软件架构设计时也广泛使用这样的分区概念加快数据交换、计算。为什么会有区间这个设计想法?大家一定看过电视剧《大宅门》吧?大宅门所描述的北京知名医术世家白家是这本电视剧的主角。白家有三兄弟,没有分家之前,由老爷子一手掌管全家,老爷子看似是个精明人,实质是个糊涂的人,否则也不会弄得后来白家家破人散。白家的三兄弟在没有分家之前,老大一家很老实,老二很懦弱,性格像女人,虽然肚子里明白道理,但是不敢出来做主。老三年轻时混蛋一个,每次出外采购药材都要私吞家里的银两,造成账目混乱。老大为了家庭和睦,一直在私下倒贴银两,让老爷子能够看到一本正常的账目。这样的一家子聚在一起,迟早家庭内部会出现问题,倒不如分家,你也不用算计家里的钱了,分给你,分给你的钱有本事守住,没本事就一直拮据下去吧。这就是最原始的分区(Region)概念。我们回到技术,看看HBase的RegionServer设计方式。在HBase内部,所有的用户数据以及元数据的请求,在经过Region的定位,最终会落在RegionServer上,并由RegionServer实现数据的读写操作。RegionServer是HBase集群运行在每个工作节点上的服务。它是整个HBase系统的关键所在,一方面它维护了Region的状态,提供了对于Region的管理和服务;另一方面,它与Master交互,上传Region的负载信息上传,参与Master的分布式协调管理。HRegionServer与HMaster以及Client之间采用RPC协议进行通信。HRegionServer向HMaster定期汇报节点的负载状况,包括RS内存使用状态、在线状态的Region等信息。在该过程中HRegionServer扮演了RPC客户端的角色,而HMaster扮演了RPC服务器端的角色。HRegionServer内置的RpcServer实现了数据更新、读取、删除的操作,以及Region涉及到Flush、Compaction、Open、Close、Load文件等功能性操作。Region是HBase数据存储和管理的基本单位。HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion。再来看看软件系统架构方面的分区设计。以任务调度为例,假设我们有一个中心调度服务,那么当数据量不断增多,这个中心调度服务一定会遇到性能瓶颈,因为所有的请求都会最终指向它。为了解决这个性能瓶颈,我们可以将任务调度拆分为多个服务,即这多个服务都可以处理任务调度工作,那么问题来了,每个任务调度服务处理的源数据是否需要完全一致?根据华为公司发布的专利发明,显示他们对于每一个任务调度服务有数据来源区分的操作,即按照任务调度数量对源数据进行划分,比如3个任务调度服务,那么源数据按照行号对3取余的方式划分,如果运行了一段时间之后,任务调度服务出现了数量上的增减,那么这个取余划分需要重新进行,要按照那个时候的任务调度数量重新划分区间。回到G1。在G1中,堆被平均分成若干个大小相等的区域(Region)。每个Region都有一个关联的Remembered Set(简称RS),RS的数据结构是Hash表,里面的数据是Card Table (堆中每512byte映射在card table 1byte)。简单的说RS里面存在的是Region中存活对象的指针。当Region中数据发生变化时,首先反映到Card Table中的一个或多个Card上,RS通过扫描内部的Card Table得知Region中内存使用情况和存活对象。在使用Region过程中,如果Region被填满了,分配内存的线程会重新选择一个新的Region,空闲Region被组织到一个基于链表的数据结构(LinkedList)里面,这样可以快速找到新的Region。总结没有GC机制的JVM是不能想象的,我们只能通过不断优化它的使用、不断调整自己的应用程序,避免出现大量垃圾,而不是一味认为GC造成了应用程序问题。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..

August 30, 2018 · 1 min · jiezi