首页 文章 精选 留言 我的

精选列表

搜索[jvm],共6415篇文章
优秀的个人博客,低调大师

《JVM 垃圾收集策略与算法》

对象已死 在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事是确定这些对象之中哪些还"存活"着,哪些已经"死去"了。 1. 引用计数法 在对象中添加一个引用计数器,每当有一个地方引用它时,这个计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为零的对象就是不可能在被使用的。但是在Java领域,至少主流的Java虚拟机里边都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外的情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。 public class ReferenceCountingGc { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性唯一的意义是占点内存,以便能在GC日志中看清楚是否有回收 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } public static void main(String[] args) { testGC(); } } 2. 可达性分析法 当前主流的商用程序语言(Java、C#等)的内存管理系统,都是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径被称为"引用链",如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达,则证明此对象是不可能再被使用的。 在Java技术体系中,固定可作为GC Roots的对象包括以下几种: 在虚拟机栈中引用的对象 在本地方法栈中引用的对象 在方法区中类静态属性引用的对象 在方法区中常量引用的对象 所有被同步锁(synchronize关键字)所持有的对象 ....... 3. 再谈引用 在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。 强引用是最传统"引用"的定义,是指在程序代码中普遍存在的引用赋值,即类似Object object = new Object() 这种引用关系。无论任何情况下,只要强引用关系存在,垃圾回收器就永远不会回收掉被引用的对象。 软引用是用来描述一些还有用,但是非必须的对象。 只被软引用关联的对象,在系统将要发生内存溢出前会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被软引用关联的对象只能生存到下次垃圾收集为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 虚引用也被称为"幽灵引用",它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。 4. 生存还是死亡 即使在可达性分析法中判定为不可达的对象,也不是"非死不可"的,这时候它们暂时还处于"缓刑"阶段,要真正宣布一个对象死亡,只要要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会第一次被标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况视为"没有必要执行"。 如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的"执行"是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者跟极端的发生了死循环,将很可能导致F-Queue队列中的其它对象永久的处于等待,甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个变量或者对象的成员变量,那在第二次标记时它将被移出"即将回收"集合;如果对象这个时候还没有逃脱,那基本上它真的要被回收了。 public class FinalizeEscapeGc { public static FinalizeEscapeGc SAVA_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed"); FinalizeEscapeGc.SAVA_HOOK = this; } public static void main(String[] args) throws Throwable{ SAVA_HOOK = new FinalizeEscapeGc(); // 对象第一次成功拯救自己 SAVA_HOOK = null; System.gc(); // 因为Finalizer方法的优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVA_HOOK != null) { SAVA_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } // 下边的代码与上边的完全相同,但是这次自救失败了 SAVA_HOOK = null; System.gc(); // 因为Finalizer方法的优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVA_HOOK != null) { SAVA_HOOK.isAlive(); } else { System.out.println("no, i am dead"); } } } 输出: finalize method executed yes, i am still alive no, i am dead 从上述代码可以看出,SAVA_HOOK对象的finalize() 确实被垃圾收集器触发过,并且在被收集前成功逃脱了 另外值得注意的是,代码中有两段完全相同的片段,执行结果却是一次逃脱一次失败了。这是因为任何一个对象的finalize() 都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize() 方法不会被再次执行,因此第二段代码的自救行动失败了。 5. 回收方法区 在Java堆中尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收由于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃的常量跟Java堆中的对象非常类似。 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻。需要同时满足一下三个条件: 该类所有的实例都已经被回收,也就是说Java堆中不存在该类及其任何派生子类的实例 加载该类的类加载器已经被回收,这个矫健除非是精心设计的可替换类加载器的场景,如:OSGi、JSP的重加载等,否则通常是很难达成的 该类对应的java.lang.Class对象没有在任何地方被引用过,无法在任何地方通过反射访问该类的方法 Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区早晨过大的内存压力。 垃圾收集算法 弱分代假说:绝大多数对象都是朝生夕灭 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡 跨代引用假说:跨代引用相对于同代引用来说仅占极少数 新生代收集:Minor GC/Young GC 老年代收集:Major GC/Old GC 混合收集:Mixed GC 整堆收集:Full GC 1. 标记-清除算法 算法分为“标记”和“清除”两个部分,首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象。也可以反过来标记存活的对象,统一回收所有未标记的对象。 主要有两个缺点:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清楚的动作,导致标记和清楚两个过程的执行效率都随对象数量增长而降低;第二个是内存空间碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序中需要分配较对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 2. 标记-整理算法(老年代) 其中的标记过程与“标记-清除”算法一样,但是后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。 这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。 3. 复制算法(新生代) 它将可用内存按容量划分为大小相等的两部分,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活着,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺点也显而易见,这种复制回收算法的代价是将可用内存缩小为原来的一半,空间浪费未免太多了一点。 在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制策略,现在称为“Appel式回收”。HotSpot虚拟机的Serial、parNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已使用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%新生代是会被“浪费”的。当Survivor空间不足以容纳一次MInor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。 内存的分配担保好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时偿还贷款,只需要有一个担保人能保证如果我们不能还款时,可以从他的账户扣钱,那银行就认为没有什么风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来存活的对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。 4. 增量算法 增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

优秀的个人博客,低调大师

深入理解JVM - 方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。 解析 所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution),在Java语言中符合这种要求的主要有静态方法和私有方法。 方法调用指令 invokestatic:用于调用静态方法。 invokespecial:用于调用实例构造器<init>()方法、私有方法和父类中的方法。 invokevirtual:用于调用所有的虚方法。 invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象。 invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。 前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。 方法分类 在java语言中方法主要分为“虚方法”和“非虚方法”。 非虚方法:在类加载的时候就可以把符号引用解析为该方法的直接引用。比如:静态方法、私有方法、实例构造器、父类方法和被final修饰的方法。 虚方法:需要在运行时才能将符号引用转换成直接引用,如,分派。 分派 分派(Dispatch)它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。 静态分派 依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载,虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型来作为判定依据的。 public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayHello(Human guy) { System.out.println("hello,guy!"); } public void sayHello(Man guy) { System.out.println("hello,gentleman!"); } public void sayHello(Woman guy) { System.out.println("hello,lady!"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.sayHello(man); sr.sayHello(woman); } } 运行结果: hello,guy! hello,guy! Human man = new Man(); 这里的Human 就是变量的“静态类型”(Static Type),或者叫“外观类型”(Apparent Type);Man就是变量的“实际类型”(Actual Type)或者叫“运行时类型”(Runtime Type)。 动态分派 我们把在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的表现就是重写。 public class DynamicDispatch { static abstract class Human { abstract void sayHello(); } static class Man extends Human { public void sayHello() { System.out.println("hello,Man!"); } } static class Woman extends Human { public void sayHello() { System.out.println("hello,Woman!"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } } 运行结果: hello,Man! hello,Woman! 我们通过javap命令看下main方法的字节码: ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class com/xiaolyuh/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method com/xiaolyuh/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class com/xiaolyuh/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method com/xiaolyuh/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 20: aload_2 21: invokevirtual #6 // Method com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 24: return LineNumberTable: line 27: 0 line 28: 8 line 29: 16 line 30: 20 line 31: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 args [Ljava/lang/String; 8 17 1 man Lcom/xiaolyuh/DynamicDispatch$Human; 16 9 2 woman Lcom/xiaolyuh/DynamicDispatch$Human; } ... 通过字节码我们发现:在main方法中,sayHello()方法的调用对应的符号引用是一样的,com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 。在这里我们可以得出一个结论:在动态分派的情况下,在编译时期我们是无法确定方法的直接引用的,那么它是怎么实现重载方法的调用的呢?问题关键是在invokevirtual指令上,在执行invokevirtual指令时,invokevirtual指令会去确定方法的调用版本。 invokevirtual指令的运行过程 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。 当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段 动态分派的实现 因为动态方法执行非常频繁,并且动态分派的方法版本选择需要在运行时,在实际接受者类型的方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,虚拟机会为类型在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。在图中,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。 虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕。 单分派与多分派 方法的接收者与方法的参数统称为方法的宗量。分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据两个及以上的宗量对目标方法进行选择。 静态分派需要根据静态类型和方法参数两个宗量来确定方法调用,所以属于多分派。 动态分派只需要根据实际类型一个宗量来确定方法的调用,所以属于单分派。 在动态分派的过程中,方法签名是确定的,所以方法参数就不会变,方法调用就取决于参数的实际类型。 总结 解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。分派(Dispatch)调用则要复杂许多,它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册