深入理解Java虚拟机---笔记

原文链接:https://www.cnblogs.com/wtzbk/p/7985156.html

深入理解Java虚拟机---笔记(原文上修改)

 

part1 走进java

java发展史,Java虚拟机发展史,有空的时候翻翻看 当看历史小说故事。

 

part2 自动内存管理机制

第2章 Java内存区域与内存溢出异常

Java运行时数据区域

1.线程计数器,是一块较小的内存空间,用来指定当前线程执行字节码的行数,每个线程计数器都是私有的,因为每个线程都需要记录执行的行数;这里解释一下为什么每个线程都需要一个线程计数器,JVM的多线程是通过线程轮流切换分配执行时间来实现的,在任何时刻,每个处理器都只会执行一个线程中的指令,当线程进行切换的时,为了线程能恢复当正确的位置,所以每个线程必须有个独立的线程计数器,这样才能保证线程之间不互相影响。

  这里注意下,如果线程执行是一个Java方法的时候,计数器记录的是虚拟机字节码指令的地址;当执行的是Native的方法的时候,计数器指令为空;该内存区域是Java虚拟机唯一没有规定任何OutOfMemoryError的区域。

 2.Java虚拟栈,这个也是一个线程私有的,生命周期与线程是同步的,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程;

  这里解释一下局部变量表,局部变量表存储方法相关的局部变量,包括基本数据,对象引用和返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

  虚拟机栈规定了2种异常情况,一种是线程请求栈的深度大于虚拟机栈所允许的深度,这时候将会抛出StackOverflowError异常,如果当Java虚拟机允许动态扩展虚拟机栈的时候,当扩展的时候没办法分配到内存的时候就会报OutOfMemoryError异常;

    3.本地方法栈,与虚拟机栈执行的基本相同,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的;

    4.Java堆,堆区是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的内存区域,主要存储对象的实例。

       当堆中没有内存完成实例分配,并且堆无法扩展的时候,将会抛出OutOfMemoryError异常;当前虚拟机都是可以扩展的;

       GC堆 GC的主要区域,后面讲,是重点。

   5.方法区,这个也是线程共享的内存区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码数据等;

      方法区在物理上也是不需要连续的,可以选择固定大小或者扩展的大小,还可以选择不实现垃圾收集,方法区的垃圾回收是比较少的,这就是方法区为什么被称为永久区的原因,但是方法区也是可以执行回收的,该区域主要是针对常量池和类型的卸载;在方法区也规定当方法区无法满足内存分布的时候,将会抛出OutOfMemoryError异常;

      运行时常量池是方法区的一部分,常量池主要用于存放编译生成的各种字面量和符合引用,这部分内容在类加载后进入方法区的运行时常量池中存放。具有动态性,并非只有预置入class文件中常量池内容才能放入运行下常量池,运行期间也可以放入新的常量到池中。比较多的是 String。intern()

由于常量池属于方法区的一部分,所以当常量池没有内存空间的时候就抛出OutOfMemoryError异常;

   6.直接内存,不是虚拟机运行时的一部分,可以直接访问堆外的内存;所以当内存空间无法动态扩展的时候就会出现OutOfMemoryError异常;

   以上基本是JVM内存分布的内容,简单的理解水满则溢出就是这个道理,系统的整个空间是一个大的容器,分不同的部分或者桶去分担整个容量,当那个桶不够的时候自然会溢出。明白内存区域的分布我们看下对象是如何分配在内存空间里面的?

  Java对象这里指的是引用类型的对象,这里用Student stu=new Student()为例子访问,Student stu作为引用对象,存在与Java虚拟机栈上,new Student()保存在Java堆中,堆中记录Student类型的信息包括方法,接口,对象类型等地址,这些类型的执行的数据存储在方法区中;

  这里需要说明一下对象访问的方式,主要包括2种句柄访问和直接指针访问:

 1. 句柄访问主要是Java堆中划分一块句柄池,虚拟机栈中存放句柄池中的地址,句柄池中包括对象的实例数据和对象类型的数据的地址,基本分布如下图:

  

   2.直接指针访问,就是虚拟机栈直接指向Java堆中的对象类型指针和对象的实例数据,然后对象类型指针在指向方法区中对象类型的实例数据,分布如下图:

 HotSpot就是第二种访问方式,优点在于访问速度快,省去一次指针开销时间,JVM内存分布基本介绍到这里,接下来说下如何保证正确回收?

作为补充 可以看看
2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建
2.3.2 对象的内存布局
2.3.3 对象的访问定位
2.4 实战:OutOfMemoryError异常
2.4.1 Java堆溢出
2.4.2 虚拟机栈和本地方法栈溢出
2.4.3 方法区和运行时常量池溢出
2.4.4 本机直接内存溢出
 

 

第3章 垃圾收集器与内存分配策略

回收是已经没有用的对象,那怎么判断一个对象没用引用?这里需要简单介绍2种方法:引用计数法和可达性分析算法;

  引用计数法:对象中添加一个引用计数器,每当有一个地方引用计数器就增加1,引用失效就减少1,计数器为0就不可用;缺点就在于无法处理对象直接相互引用的问题,因为相互引用以后无法使计数器为0,所以无法回收;

  可达性分析算法,也就是我们常说的GC Root,,当一个对象没有与任何引用链相连的时候,就可以对该对象进行回收,下面是Java中GC Root对象使用的几个地方:

 

  

   以上对象简单就是分为可用和不可用这2种,现在Java对引用概念进行扩充:

 方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。

 

  •    JVM什么情况下触发GC以及GC触发的方式?

  第一个问题:当内存空间不足的时候就需要触发GC,GC回收的时候采用的是分代收集的算法,主要分为年轻代和老年代,接下来我们简单介绍一下这2种方式:

   年轻代:当一个对象被创建的时候,内存分配首先分配在年轻代,大部分对象创建以后都不再使用,对象很快变得不可达,就是对象无用,由于垃圾是被年轻代清理掉的,所以被叫做Minor GC或者Young GC。

   老年代:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。

   明白这2块主要存放什么东西以后接下来我们看下GC的整体结构,看一个对象如何被Kill掉的流程:

   

   1.当一个对象被创建的时候(new)首先会在年轻代的Eden区被创建,直到当GC的时候,根据可达性算法,看一个对象是否消亡,没有消亡的对象会被放入年轻带的Survivor区,消亡的直接被Minor GC Kill掉;

   2.进入到Survivor区的对象也不是安全的,当下一次Minor GC来的时候还是会检查Enden和Survivor存放对象区域中对象是否存活,存活放入另外一块Survivor区域;

   3.当2个Survivor区切换几次以后,会直接进入老年代,当然进入到老年代也不是安全的,当老年代内存空间不足的时候,会触发Major GC,已经消亡的依然还是被Kill掉;

   推荐:http://blog.csdn.net/sd4015700/article/details/50109939

   GC的算法:标记--清除,复制,标记--整理这3种算法;

   

 

   

 

 

  • 分代收集

    现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

    一般将堆分为新生代和老年代。

  • 新生代使用:复制算法(Eden 空间和两块较小的 Survivor:  8:1:1)
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法           

 

  • 内存分配与回收策略

    • 对象优先在Eden分配
      • 新生代GC(MinorGC):发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
      • 老年代GC(MajorGC/FullGC):老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
    • 大对象直接进入老年代
    • 长期存活的对象将进入老年代
      对象在Survivor区每经过一次MinorGC就会长大一岁,超过阈值则进入老年代
    • 动态年龄判定
      相同年龄大小的对象总和超过Survivor空间的一半则大于等于该年龄的进入老年代。
    • 空间分配担保:防止MinorGC时Survivor空间不足,需要老年代部分空间进行担保,(since6u24)只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则进行FullGC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

(一)调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

(二)老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

(三)空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。

(四)JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

(五)Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

 

了解算法和GC内存分布以后我们接下来介绍垃圾回收器(7种GC单独整理)https://mp.csdn.net/postedit/81606342

   监控和优化,工具,推荐博客:http://blog.csdn.net/renfufei/article/details/56678064



 
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值