第五章 Java 的高级
1、说说你对 Java 中反射的理解
Java 中的反射首先是能够获取到Java 中要反射类的字节码,获取字节码有三种方法:1. Class.forName(className) 2. 类名.class 3. this.getClass()。然后将字节码中的方法,变量,构造函数等映射成相应的 Method、 Filed、 Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
2、写一个 ArrayList 的动态代理类
final List list = new ArrayList();List proxyInstance =(List)Proxy.newProxyInstance(list.getClass().getClassLoader(),list.getClass().getInterfaces(),new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(list, args);}});proxyInstance.add("你好");System.out.println(list);因为改动态代理方法使用的是匿名内部类实现的,所以上面的list对象使用final修饰(被final修饰其地址值不变,才可以在匿名内部类中被使用)import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy; public class JDKProxy implements InvocationHandler { private Object target; public JDKProxy (Object target){ this.target=target; } public Object createProxy(){ //得到目标的classload ClassLoader classLoader = target.getClass().getClassLoader(); //得到目标对象实现的接口数组 Class[] interfaces = target.getClass().getInterfaces(); return Proxy.newProxyInstance(classLoader, interfaces, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("瞎搞"); return method.invoke(target, args); } 在spring框架中使用JDK动态代理实现,可以使用实现接口的方式,没有使用匿名内部类,则变量可以不使用final修饰,改例子中使用的是private.
3、动静态代理的区别,什么场景使用?
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。动态代理是实现 JDK 里的 InvocationHandler 接口的 invoke 方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过 Proxy 里的 newProxyInstance 得到代理对象。还有一种动态代理 CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。AOP 编程就是基于动态代理实现的,比如著名的 Spring 框架、 Hibernate 框架等等都是动态代理的使用例子.
4、你所知道的设计模式有哪些?
Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。总体来说设计模式分为三大类:创建型模式,共五种: 工厂方法模式、 抽象工厂模式、 单例模式、 建造者模式、原型模式。结构型模式,共七种: 适配器模式、装饰器模式、 代理模式、外观模式、桥接模式、组合模式、 享元模式。行为型模式,共十一种: 策略模式、模板方法模式、 观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
5、单例设计模式
1、单例类只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。1、懒汉式,线程不安全是否 Lazy 初始化:是是否多线程安全:否实现难度:易描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }2、懒汉式,线程安全是否 Lazy 初始化:是是否多线程安全:是实现难度:易描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。优点:第一次调用才初始化,避免内存浪费。缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }3、饿汉式是否 Lazy 初始化:否是否多线程安全:是实现难度:易描述:这种方式比较常用,但容易产生垃圾对象。优点:没有加锁,执行效率会提高。缺点:类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }4、双检锁/双重校验锁(DCL,即 double-checked locking)JDK 版本:JDK1.5 起是否 Lazy 初始化:是是否多线程安全:是实现难度:较复杂描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }5、登记式/静态内部类是否 Lazy 初始化:是是否多线程安全:是实现难度:一般描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }6、枚举JDK 版本:JDK1.5 起是否 Lazy 初始化:否是否多线程安全:是实现难度:易描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。public enum Singleton { INSTANCE; public void whateverMethod() { } }经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
6、JVM 垃圾回收机制和常见算法?
1)引用计数器算法(废弃)引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为 0 的时候,JVM 就认为对象不再被使用,是“垃圾”了。引用计数器实现简单,效率高;但是不能解决循环引用问问题(A 对象引用 B 对象,B 对象又引用 A 对象,但是A,B 对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在 JDK1.1 之后,这个算法已经不再使用了。2)可达性分析算法通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链连接的时候,则说明这个对象是不可用的。GC Roots 对象包括:a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。b) 方法区域中的类静态属性引用的对象。c) 方法区域中常量引用的对象。d) 本地方法栈中 JNI(Native 方法)的引用的对象。回收算法:1) 标记—清除算法(Mark-Sweep)(DVM 使用的算法)标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。2) 复制算法(Copying)复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的 JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是 1:1(大概是 8:1)。3) 标记—整理算法(Mark-Compact)标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。4) 分代收集(Generational Collection)分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。
7、谈谈 JVM 的内存结构和内存分配
a) Java 内存模型Java 虚拟机将其管辖的内存大致分三个逻辑部分:方法区(Method Area)、 Java 栈和 Java 堆。1、方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。常数池,源代码中的命名常量、 String 常量和 static 变量保存在方法区。2、Java Stack 是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。最典型的 Stack 应用是方法的调用, Java 虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法则对应的 方法帧被弹出(pop)。栈中存储的数据也是运行时确定的。3、Java 堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回的内存管理模型。堆中存储的数据常常是大小、数量和生命期在编译时无法确定的。Java 对象的内存总是在 heap 中分配。b) java 内存分配1、基础数据类型直接在栈空间分配;2、方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收;3、引用数据类型,需要用 new 来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;4、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收;5、局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待 GC 回收;6、方法调用时传入的实际参数,先在栈空间分配,在方法调用完成后从栈空间释放;7、字符串常量在 DATA 区域分配 , this 在堆空间分配;8、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!
8、Java 中引用类型都有哪些?
Java 中对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。1、强引用(StrongReference)Java中默认声明的就是强引用。如果一个对象拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。Java 的对象是位于 heap 中的,heap 中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下代码:String abc=new String("abc"); //1SoftReference softRef=new SoftReference(abc); //2WeakReference weakRef = new WeakReference(abc); //3abc=null; //4softRef.clear();//5第一行在 heap 堆中创建内容为“abc”的对象,并建立 abc 到该对象的强引用,该对象是强可及的。第二行和第三行分别建立对 heap 中对象的软引用和弱引用,此时 heap 中的 abc 对象已经有3个引用,显然此时 abc 对象仍是强可及的。第四行之后 heap 中对象不再是强可及的,变成软可及的。第五行执行之后变成弱可及的。只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。2、软引用(SoftReference)软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。3、弱引用(WeakReference)如果一个对象只具有弱引用,那该类就是可有可无的对象,因为只要该对象被 gc 扫描到了随时都会把它干掉。即无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。4、虚引用(PhantomReference)"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。建立虚引用之后通过 get 方法返回结果始终为 null,通过源代码你会发现,虚引用通向会把引用的对象写进referent,只是 get 方法返回结果为 null。先看一下和 gc 交互的过程再说一下他的作用。1 不把 referent 设置为 null, 直接把 heap 中的 new String("abc")对象设置为可结束的(finalizable)。2 与软引用和弱引用不同, 先把 PhantomRefrence 对象添加到它的 ReferenceQueue 中.然后在释放虚可及的对象。引用队列(ReferenceQueue)引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。与软引用、弱引用不同,虚引用必须和引用队列一起使用。
9、heap 和 stack 有什么区别?
1. 申请方式stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于 Java 需要手动 new Object()的形式开辟。2. 申请后系统的响应stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。3. 申请大小的限制stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。4. 申请效率的比较:stack:由系统自动分配,速度较快。但程序员是无法控制的。heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。5. heap 和 stack 中的存储内容stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。6. 数据结构层面的区别还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第 1 个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
10、解释内存中的栈 (stack) 、堆 (heap) 和方法区 (method area) 的用法
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代, 再具体一点可以分为 Eden、 Survivor(又可分为From Survivor 和 To Survivor)、 Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量、 JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、 "hello"和常量都是放在常量池中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。String str = new String("hello");上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
11、Java 类加载器的种类都有哪些?
1)启动类加载器(Bootstrap ClassLoader)这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。2)扩展类加载器(Extension ClassLoader)这个加载器负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器3)应用程序类加载器(Application ClassLoader)这个加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器
12、类什么时候被初始化?
1)创建类的实例,也就是 new 一个对象2)访问某个类或接口的静态变量,或者对该静态变量赋值3)调用类的静态方法4)反射(Class.forName("com.lyj.load"))5)初始化一个类的子类(会首先初始化子类的父类)6)JVM 启动时标明的启动类,即文件名和类名相同的那个类类的初始化步骤:1)如果这个类还没有被加载和链接,那先进行加载和链接2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。
13、Java 类加载体系之 ClassLoader 双亲委托机制
工作过程:如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把整个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
![3963c54b5185177ac6fae3ddc36d6c66.png](https://i-blog.csdnimg.cn/blog_migrate/577f7d4495aea62d7b97def00fd53dd5.jpeg)
14、描述一下 JVM 加载 class(类的加载过程)
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。1、加载加载阶段是类加载过程中的一个阶段,在加载阶段,虚拟机要完成三件事情: 1、通过一个类的全限定名来获取定义此类的二进制字节流 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口2、验证验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,校验过程包含文件格式验证、元数据验证、字节码验证和符号引用验证。3、准备准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中进行分配。这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中;这里的初始值通常情况下是数据类型的零值。4、解析解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用是指以一组符号来描述所引用的目标,直接引用是指直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。5、初始化初始化阶段是类加载过程中的最后一步,这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。
15、既然有 GC 机制,为什么还会有内存泄露的情况
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如 hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
16、先行发生原则(happens-before)
1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。 准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、 循环等结构。 2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。 3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。 4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。 6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。 7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。 8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
17、Java 中为什么会有 GC 机制呢?
1、安全性考虑;2、减少内存泄露;3、减少程序员工作量。
18、对于 Java 的 GC 哪些内存需要回收?
(1)引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。实现简单,判定效率高,但它很难解决对象之间相互循环引用的问题。因此,虚拟机也不是通过引用计数算法来判断对象是否存活的。(2)可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达),则证明此对象是不可用的。可作为GC Roots的对象包括下面几种:* 虚拟机栈中引用的对象;* 方法去中类静态属性引用的对象;* 方法区中常量引用的对象;* 本地方法中Native方法引用的对象。虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象并不一定会被回收。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记:如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。判断对象是否存活都与“引用”有关,Java中又加入了一些更富表达意义的引用概念:强引用,软引用,弱引用和虚引用,依次来判定一个对象是否可以被回收。1、强引用:类似Object obj = new Object();只要强引用还在,就永远不会被回收。2、软引用:用来描述一些还有用但非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收。3、弱引用:也是描述一些还有用但非必须的对象,比软引用强度更弱,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。4、虚引用:为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
19、在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?
引起内存溢出的原因有很多种,常见的有以下几种:1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;2.集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;3.代码中存在死循环或循环产生过多重复的对象实体;4.使用的第三方软件中的 BUG;5.启动参数内存值设定的过小;内存溢出的解决方案:第一步,修改 JVM 启动参数,直接增加内存。 (-Xms, -Xmx 参数一定不要忘记加。)第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。2.检查代码中是否有死循环或递归调用。3.检查是否有大循环重复产生新对象实体。4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。5.检查 List、 MAP 等集合对象是否有使用完后,未清除的问题。 List、 MAP 等集合对象会始终存有对对象的引用,使得这些对象不能被 GC 回收。第四步,使用内存查看工具动态查看内存使用情况。
20、JVM内存分哪几个区,每个区的作用是什么?
堆和方法区是所有线程共有的,而虚拟机栈、本地方法栈和程序计数器则是线程私有的。方法区:1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载,必须在该类没有任何引用的情况下才能被GC回收2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。3. 该区域是被线程共享的,所以访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。OutOfMemoryError虚拟机栈:1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。2. 虚拟机栈是线程私有的,它的生命周期与线程相同。3. 局部变量表里存储的是编译期可知的基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定。long和double占用2个局部变量空间,其余占用1个4. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。OutOfMemoryError StackOverflowError本地方法栈:1. 本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务2. 主要用于存储本地方法的局部变量表,本地方法的操作数栈等信息OutOfMemoryError StackOverflowErrorJava堆:java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。OutOfMemoryError程序计数器:1. 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令2. 分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。3. 该内存区域是唯一一个java虚拟机规范没有规定任何OOM(OutOfMemoryError)情况的区域。
21、内存溢出(OutOfMemoryError 异常)
除了程序计数器外,其他的几个运行时区都有发生OutOfMemoryError异常的可能1、Java堆溢出 Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制之后就会产生内存溢出异常;2、虚拟机栈和本地方法栈溢出 a. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常(栈溢出) b. 如果虚拟机栈扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常3、方法区和运行时常量溢出 方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,静态变量,字段描述,方法描述,即时编译后的代码等数据,如果运行时方法区产生了大量的类,超出了方法区的最大容量,就会产生内存溢出。注:jdk8中永久区被移除了,取而代之的是元数据区,元数据区是堆外直接内存,可以提供更大的空间4、本机直接内存溢出 主要是当直接向操作系统分配内存的时候,内存无法分配的时候,手动抛出异常。1、用ThreadPoolExecutor自定义线程池,看线程的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM;2、如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务;3、最大线程数一般设为2N+1最好,N是CPU核数;4、核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数;5、如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果;
22、JVM的内存分配和回收策略
1、对象优先在Eden分配大多数情况下,对象在新生代Eden去中分配,但Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC2、大对象直接进入老年代所谓大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(例如byte[]数组)。大对象对虚拟机内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们3、长期存活的对象将进入老年代4、动态对象年龄判定5、空间分配担保
23、JVM中的新生代和老年代(Eden空间、两个Survior空间)
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。 年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。 年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩3)永久代 指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制 采用元空间而不用永久代的几点原因: 1、为了解决永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出 2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出(因为堆空间有限,此消彼长) 3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低 4、Oracle 可能会将HotSpot 与 JRockit 合二为一
24、JVM内存分代
Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。为什么要分代? 堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。 有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处内存分代划分 Java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念,它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域新生代(Young) 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。老年代(Old) 在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。永久代(Permanent) 永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。Minor GC 和 Full GC的区别 新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。 老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。元数据区OOM测试元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=50m
25、ClassLoader的双亲委派
ClassLoader负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定目前了解4种类加载器:1.BootstrapClassLoader 启动类加载器这货是用C++编写的 是最顶层的类装载器,用来加载jre/lib/rt.jar下的类2.ExtensionClassLoader拓展类加载器用来加载jre/lib/ext/*.jar的所有类3.AppClassLoader 应用类加载器应用类加载器,也叫系统类加载器,用来加载当前应用classpath下的所有类4.User-Definend 用户自定义的类加载器双亲委派机制双亲委派模型的式作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完全这个加载请求时,子加载器才会尝试自己去加载。沙箱机制沙箱机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏
24、java程序从编译到执行会经历哪些重排序
1、编译器优化重排序:属于java编译器重排序,会按照JMM(Java内存模型)的规范严格进行,编译器重排序一般不会对程序的正确逻辑造成影响;2、指令级并行重排序:JMM会要求java编译器在生成指令时加入内存屏障。可以将内存屏障理解为一个密不透风的保护罩,把不能重排序的java指令保护起来,那么处理器在遇到内存屏障保护的指令时就不会对它进行重排序了;指令重排序对单线程没有什么影响,它不会影响程序的运行结果,但是会影响多线程的正确性3、内存系统重排序:属于CPU重排序,情况与2相似
25、什么是内存栅栏?
简单来说内存屏障(Memory Barrier,或内存栅栏,Memory Fence)就是从本地或工作内存到主存之间的拷贝动作。程序在运行的过程中,所有的变更会先在寄存器或本地内存中完成,然后才会拷贝到主存以跨越内存栅栏。Java内存模型中volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。Java并发API中很多操作都隐含有跨越内存栅栏的含义:volatile、synchronized、Thread中的函数如start()和interrupt()、ExecutorService中的函数以及像CountDownLatch这样的同步工具类等
26、同步、异步
同步:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行;异步:如果有多个任务或者事件要发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待
27、Concurrent Model Failure和ParNew promotion failed什么情况下会发生
promotion failed该问题是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。(promotion failed时老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold 降级为GC-Serail Old)。解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者Survivor空间concurrent mode failure该问题是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的。解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
28、JVM参数介绍
-Xms :初始堆大小-Xmx :最大堆大小 -XX:NewSize=n :设置年轻代大小 -XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n :设置持久代大小 收集器设置 -XX:+UseSerialGC :设置串行收集器 -XX:+UseParallelGC :设置并行收集器 -XX:+UseParalledlOldGC :设置并行年老代收集器 -XX:+UseConcMarkSweepGC :设置并发收集器 垃圾回收统计信息 -XX:+PrintHeapAtGC GC的heap详情 -XX:+PrintGCDetails GC详情 -XX:+PrintGC 输出GC日志-XX:+PrintGCDetails 输出GC的详细日志-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log 日志文件的输出路径-XX:+PrintTenuringDistribution 打印年龄信息等 -XX:+HandlePromotionFailure 老年代分配担保(true or false) 并行收集器设置 -XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间 -XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数-XX: PretenureSizeThreshold 大对象直接进入老年区