Java note jvm
-
原图 JVM 的主要组件包括: 类加载器(class loader), 运行时数据区(runtime data areas), 以及执行引擎(execution engine).
-
类加载的过程
1)加载:在加载阶段,虚拟机完成以下三件事:
a)通过一个类的全限定名来获取此类的二进制字节流
b)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
c)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2)验证(连接1):
a)文件格式验证
b)元数据验证
c)字节码验证
d)符号引用验证
3)准备(连接2):准备阶段是正式为类变量(不是实例变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配
4)解析(连接3):将常量池内的符号引用替换为直接引用的过程
5)初始化:真正开始执行类中定义的java程序代码
类的生命周期再上面的状态下添加:使用(Using)、卸载(Unloading)
参考:http://www.programcreek.com/2013/04/what-can-you-learn-from-a-java-helloworld-program/ -
类加载的时机:在以下情况中,如果类没有进行过初始化,则需要先触发其初始化:
1)遇到new、getstatic、putstatic或invokestatic这四条字节码时
2)使用java.lang.refect包的方法对类进行反射调用的时候
3)当初始化一个类的时候,其父类满足条件上面的条件
4)当虚拟机启动时,会初始化主类(包含main()方法的类) -
类加载器:实现“通过一个类的全限定名来获取此类的二进制字节流”动作的代码模块。 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性
从java虚拟机的角度讲,只存在两种不同的类加载器:
1)一种是启动类加载器(Bootstrap ClassLoader),这个类由C++实现,是虚拟机的一部分
2)另一种是其他所有类加载器,都有java实现,独立于虚拟机外部,且全都继承自抽象类java.lang.ClassLoader -
Java的垃圾回收机制是怎么工作的? http://javarevisited.blogspot.sg/2011/04/garbage-collection-in-java.html
当一个对象从任何正在运行的线程都不可达的时候,这个对象就会被垃圾回收。循环引用不算正常引用,依然要被回收。
通常一下情况下对象会被垃圾回收:
1) 对象的所有引用都显示的置为了null
2) 对象离开了创建的定义域内
3) 父对象被显示的置为null
4) 对象只有通过 WeakHashMap 的弱引用 -
程序计数器(program counter register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
-
java虚拟机栈(java virtual machine stacks)与程序计数器都是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这里的栈也就是虚拟机栈中局部变量表部分。 局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、 对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或其它与此对象相关的位置) 和returnAddress类型(指向了一条字节码指令的地址)
栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(virtual machine stack)的栈元素。 其中存储了方法的局部变量表、操作数栈、动态链接方法和方法返回地址等信息 -
本地方法栈(native method stack)与虚拟机栈所发挥的作用是非常相似的,只不过是针对的是本地方法。与虚拟机栈一样,它也会抛出StackOverflowError和OutOfMemoryError
-
java堆(java heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存。java堆是垃圾收集器管理的主要区域,因此也被称为GC堆(Garbage Collected Heap)。因为现代收集器基本都采用分代收集算法,所以java堆还可以细分为:新生代和老生代。
-
方法区(Method Area)与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来
-
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池
-
对象的创建过程:
1)虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
2)在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有“指针碰撞(Bump the Pointer)”和“空闲列表(Free List)”
3)内存分配完之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
4)接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。 这些对象存放在对象的对象头(Object Head)之中
5)至此,从虚拟机的视角来看,一个新的对象已经产生了,但从java程序的视角来看,对象创建才刚刚开始——方法还没有执行,所有字段都还为零 参考:[http://www.infoq.com/cn/articles/jvm-hotspot](http://www.infoq.com/cn/articles/jvm-hotspot){:target="_blank"} -
对象的内存布局:在HOTSPOT虚拟机中,对象在内存中存储的布局可以分为3快区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding):
1)HOTSPOT虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据官方称为“Mark Word”。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。另外,如果对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机无法通过数组的元数据确定数组的大小
2)实例数据部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型的字段内容
3)第三部分对齐填充并不是必然存在的,它仅仅起占位符的作用 -
对象的访问定位:目前主流的访问方式有使用句柄和直接指针两种:
1)如果使用句柄访问,那么java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动时(垃圾收集)只会改变句柄中的实例数据指针,而不会修改reference
2)如果直接使用指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,使用直接指针访问方式的最大好处就是速度快,它节省了一次指针定位的时间开销。hotspot使用的就是这种方式 -
GC需要完成的三件事:
1)哪些内存需要回收
2)什么时候回收
3)如何回收 -
引用计数法(Reference Counting):给对象添加一个引用计数器,每当有一个地方引用它,计数加1,反之减1,计数为0则不能再使用。java中没有使用该方法是因为无法解决循环引用问题
-
可达性分析法(Reachability Analysis),是主流商用程序语言广泛采纳的一种方法。其基本思路就是通过一系列成为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(从图论的角度讲,就是从GC Roots到这个对象不可达),则证明此对象时不可用的。在java中,可作为GC Roots的对象包括下面几种:
1)虚拟机栈(栈帧中本地变量表)中引用的对象
2)方法区中类静态属性引用对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Java Native Interface)引用的对象 -
引用的概念:
1)强引用(StrongReference):就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要引用还存在,垃圾收集器永远不会回收掉被引用的对象
2)软引用(SoftReference):是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
3)弱引用(WeakReference):也是用来描述非必须对象的。但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
4)虚引用(PhantomReference):也称为幽灵引用或者幻影引用,她是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
参考:https://www.rallydev.com/blog/engineering/java-references-strong-soft-weak-phantom -
回收方法区:永久代(方法区别名)的垃圾收集主要回收两部分内容:废弃常量和无用的类
1)废弃常量:常量池中的字符串、类(接口)、方法、字段的符号引用等都是回收的对象
2)无用的类:要同时满足以下三个条件的类才是无用的类:
a)该类的所有实例都已经被回收部
b)加载该类的ClassLoader已经被回收
c)该类对应的java.long.Class对象没有在任何地方被引用,无法在任何地方通过反射方法访问该类的方法 -
垃圾收集算法:
1)标记-清除算法(Mark-Sweep):算法分“标记”和“清除”两个阶段:首先标记所有需要回收的对象,然后在清除所有被标记的对象。它的不足主要有两个方面:1.效率不高,2.产生碎片
2)复制算法(Copying):将可用内存按容量等分为两份,每次只是用其中一块。当这块的内存用完了,就将还存活的复制到另外一块上,然后把当前快清理掉。该算法实现简单,运行高效,只是可用内存缩小了一半。现在商业虚拟机都采用这种收集算法来回收新生代。研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一个Survivior空间上,最后清理掉Eden和刚才用过的Survivor空间。hotspot默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内容会被“浪费”。
3)标记-整理算法(Mark-Compact):复制算法不适用与老年代,老年代使用标记-整理算法,该算法与标记-清除算法一样,只是第二轮不是简单清除,而是整理集中。
4)分代收集算法:不同的代,使用不同的算法,如上所述 -
OopMap: OopMap是用于记录Java栈中对象的引用的一个数据结构。它的主要目的是找到GC 的 root节点, 并在每次对象移入堆中时更新对象的引用。
OopMaps的主要作用就是帮助虚拟机直接得知哪些地方存放着对象引用, 不需要虚拟机一个不漏得检查完所有执行上下文和全局的引用位置。
参考: http://www.infoq.com/cn/articles/jvm-memory-collection
安全点(SafePoint):用于停下来记录OopMap的位置。
参考:http://www.infoq.com/cn/author/%E5%91%A8%E5%BF%97%E6%98%8E
安全区域(Safe Region):是指在一段代码片段之中,引用关系不会变化。在这个区域中的任意位置开始GC都是安全的。 有这个安全区域是因为有些还是线程可能无法在一定时间内运行到安全点,比如正在sleep的线程,那么,sleep就是安全区域了。 -
垃圾收集器:
1)Serial收集器:运行时需要“Stop The World”,优势在于简单而高效,是虚拟机在Client模式在的默认新生代收集器。 Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。 是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率。
2)ParNew收集器:多线程版的Serial,多用于Server版,是除了Serial外唯一可以与CMS配合的新生代收集器。 ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样
3)Parallel Scavenge收集器:目标是可控制吞吐量。 Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。 parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。 吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。 其中垃圾收集花掉1分钟,那吞吐量就是99%。
4)Serial Old收集器:Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集, 使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
5)Parallel Old收集器:Parallel的老年代版本,“吞吐量优先”组合搭档, Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
6)CMS收集器(Concurrent Mark Sweep):包括a)初始标记b)并发标记c)重新标记d)并发清除 四个步骤
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。
初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,
并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。
重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作, 所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,主要有三个显著缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢, 总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。 由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后, CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行, 即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集, 而是需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活, 也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。 要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败, 这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败, 性能反而降低。
最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。 空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象, 内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题, CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数, 用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后, 跟着来一次碎片整理过程。
CMS堆结构
7)G1收集器(Garbage-First): 包括a)初始标记b)并发标记c)最终标记d)筛选回收 四个步骤,优势有a)并行与并发b)分代收集c)空间整合d)可预测的停顿。
G1是一款面向服务端应用的垃圾收集器,Sun(Oracle)赋予它的使命是(在比较长期的)未来可以替换掉JDK 5中发布的CMS(Concurrent Mark Sweep)收集器, 与其他GC收集器相比,G1具备如下特点: 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间, 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆, 但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器, 从局部(两个Region之间)上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片, 收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿:这是G1相对于CMS的另外一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外, 还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒, 这几乎已经是实时Java(RTSJ)的垃圾收集器特征了。
参考:非常推荐 -
对象优先在Eden分配,当Eden存储空间不够时,虚拟机将发起一起Minor GC(新生代GC,相应的,老年代GC叫Major GC或Full GC)。
大对象直接进入老年代,避免Eden以及两个Survivor区之间发生大量的内存复制
长期存活的对象将进入老年代 -
Class类文件结构:根据java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构体来存储数据, 这种伪结构体中只有两种数据结构:无符号数和表 1)无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数, 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
2)表是由多个无符号数或者其他表作为数据项构成的符合数据类型
魔数+Class文件的版本+常量池入口+访问标志+类索引、父类索引、接口索引集合+字段表集+方法表集合+属性表集合 -
java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode) 以及跟随其他的零至多个代表此操作所需参数(称作操作数,Operrands)而构成。
-
java语法糖: 1)泛型:只在程序源码中存在,编译后就没有了,只是插入了强制转换
2)自动装箱、拆箱:尽量少用
3)条件编译:使用条件为常量的if语句 -
JAVA内存模型JMM(JAVA MEMORY MODEL):用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现java程序在各种平台下都能达到一致的内存访问效果
原子性(atomicity):由java内存模型来直接保证的原子性变量操作包括read,load,assing,use,store,write. 开放给用户的原子操作就是synchronized
可见性(visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
有序性(ordering):线程内部表现为串行性 -
实现线程主要是三种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程
-
java的线程状态:
1)新建(new):创建后未启动
2)运行(runable):可运行或正在运行
3)无限期等待(waiting):不会分配cpu时间,等待唤醒
4)限期等待(timed waiting):一定时间后系统自动唤醒
5)阻塞(blocked):等待进入同步区
6)结束(terminated):结束执行
blog comments powered by Disqus