以下是读完《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》这本书的心得,我随便写,各位自己看,有不对的地方请指正,有些地方也没看懂(涉及了很多的编译原理内容),—— 多读书读看报,少吃零食多睡觉。
第一章,啥也没说讲讲故事发生的背景(比如主人公出生前,都得天降异象),主要是讲述了一下发展史吧,不累述。
第二章,主要讲的是jvm的内存管理:开局一张图,(我从网上copy的)感觉图的记忆比较简单,看图说话,分别介绍下每个部分的作用。
1、先说最简单的程序计数器,线程私有,记录当前线程运行的到哪里,(就像书签,夹好了书放起来,隔两天回来再继续看),程序计数器也一样,当完成线程切换或者循环等其他操作时,需要回到上一次运行指令的地方,如果是native方法则值为空。
2、栈,分为java栈 和本地方法栈,区别是啥,本地方法栈执行的是虚拟机需要的native方法。
栈也是线程私有的,与线程的生命周期相同,每个方法执行的时候都会创建一个栈帧,存放,局部变量表,操作数栈,方法出口等,方法的执行对应着栈帧出栈到入栈的过程。
3、堆,被所有线程共享,存放对象实例及数组,垃圾收集器的主要管理区域,可分新生代:eden、form survivor、to survivor;老年代。
4、方法区(方法区相当于接口,jdk7永久代和jdk8元空间是具体实现),被虚拟加载的类的信息,存放常量(存放在常量池,方法区的一部分),静态变量。垃圾回收主要是类的卸载和常量回收。(方法区回收:回收常量池中的常量,常量池中对象不再被引用。类的卸载需要三个条件:①类的所有实例都被回收,②类的ClassLoader也被回收,③ 对应的class对象没有被引用,无法通过反射获取)
5、对象的创建,内存分配,以及访问。对于面向对象语言最常听到的一个名词就是对象,那么对象是如何被创建,分配,以及访问的呢?
当遇到关键字new时,虚拟机会先查找改对象是否存在于常量池中,并且是否被类加载 器 加载过、解析初始化过,如果没有则需要进行类加载,内存分配两种方式:
① 当垃圾回收器带有压缩功能(serial,parnew),即用过的内存在一边,空闲的在另一边,中间用指针隔开,分配内存只需要移动指针即可,这种方式叫做指针碰撞。
② 垃圾收集器不带有压缩功能如:CMS(我们系统使用的就是这种),内存空间零散,大小不一,这时候就需要维护一张表,记录剩余空间的大小,以便存放对象这种方式叫做空闲列表。
除了这两个内存分配地址分配方式 之外,当内存分配的时候其实也存在并发问题,A、B同时请求一块地址,那么改如何分配呢?
这里也有两中选择,① CAS,② 本地线程分配缓冲( TLAB )
① CAS是什么,简单点说就是 更新A先要记录A的旧值, A更新为B时判断A的值 是否改变过,如果没有则更新,(ABA问题,忽略先)
②TLAB 内存的分配按线程划分,每块线程有一个独立的内存,内存用尽才会再申请新的,只有这个才会同步锁定。
内存分配完成,需要把分配到的内存空间初始化零值(除了对象头——存放对象的信息,比如:对象哈希码,那个类的实例) 。
之后执行对象的<init>方法,根据程序员的心意进行初始化。
6、对象内存布局;分为三块:对象头,实例数据,补齐填充(如果位不够填充值,薯片包装盒子里的气)
①对象头:作者讲了很多很细,有些也听不动,总结起来如下:一、存放运行时数据(hash码,GC分代年龄——决定对象是否要被回收,线程持有的锁等等)二、对象类型指针(看看下面对象访问的那张图,确定对象是哪个类的实例,如果是一个数组还要存放长度),关于对象头可以参考我的这个博客 Synchronized关键字------------ 对象头_synchronized 对象头线程id_君莫笑_0808的博客-优快云博客。
②实例数据,程序中被定义的字段安装定义的顺序和虚拟机的分配策略:long/double, int, shorts/chars,bytes/boolean,oops,相同宽度的会被分配到一起。
7、对象访问:两种方式,句柄访问,直接指针。
① 句柄访问,需要划分出来一块单独的空间,栈中的reference ,直接访问句柄池,句柄池中存储这对象实例数据指针和类型指针,分别对应堆中的实例池,和方法区中的对象类型,如下图:
② 直接指针,直接存储对象地址,由对象存储对象类型数据,如下图:
对比:句柄访问,垃圾回收对象地址移动,句柄池修改,reference不需要修改;直接指针(hotspot采用此方式进行对象访问),减少了一次二次寻址。
第三章:垃圾回收,说说垃圾回收算法,以及几种垃圾回收器。
一、确认什么样的对象需要清除:两种算法
① 引用计数,对象被引用一次则计数器加1,引用失效则减1,当引用计数为0的时候表示对象需要被清理。(循环引用清除不了了)
② 可达法,从根节点GCRoot起向下找,走过的 路径成为引用链,如果对象到GCRoot没有引用连相连则不可达,表示可能被清理。
在Java语言中,可作为GC Roots 的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈(即一般说的 Native 方法)中JNI引用的对象
③ 被判断为不可达的对象,不会立马被回收掉,需要看对象是否重写了finalize(),如果没有重写,或者已经调用一次,会被放入一个慢队列中,稍后有虚拟机重新标记,如果第二次被标记时依然不可达,则会被回收。
二、垃圾收集算法,一般新生代采用的是复制算法,老年代采用的标记——整理法。
1、标记清除
标记所有需要被回收的对象,然后回收,缺点:产生大量的空间碎片。
2、复制(主要回收新生代)
将内存划分为2块,将存活的对象复制到另外一块内存中,将剩余的空间全部清理,缺点:内存利用率低
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。
新生代 90%的对象会“朝生夕死”,所以对于空间的划分 eden:8,form survivor:1,to survivor:1,但是我们不能保证剩余的空间一定够用,这个时候就需要分配担保,如果survivor的空间不足以存放存活的对象,则直接将对象放到老年代,即需要老年代的空间做担保(如果老年代空间也不够则发生fullGC)。新生代老年代的空间占比如下图:
3、标记——整理
标记--------整理 就是带有整理功能的标记清除,先标记存活的对象然后向一边移动,再清理边界以外的内存空间。
4、分代收集
就是把堆分成,新生代,老年代,新生代朝生夕死,适合使用复制算法,老年代没有人再为他担保,一般采用标记清除或者标记整理,为什么采用分代收集? 为了减少gc次数,减少stop the world时间。
hotspot算法实现,设置很多个安全点,到达安全点的线程会被挂起,直至gc完成。
三、垃圾收集器
1、serial收集器,单行的收集器,垃圾收集过程会挂起所有正在执行的线程,对应老年代收集器serialOld。
2、parnew收集器,并行收集器,相对于serial,增加了多个线程执行垃圾回收,配合CMS使用。对应老年代收集器parnew。
3、parallel Scavenge收集器(吞吐量优先收集器),关注的是吞吐量,吞吐量=运行代码时间/(运行代码+垃圾收集时间)。
4、CMS收集器(我们系统使用的垃圾收集器,标记——清除),第一款真正并行的收集器,用户和垃圾收集器同时工作,(依然会出现短暂的stop the world),分为四个步骤:
① 初始标记:标记GC Root直接关联到的对象。
② 并发标记:GC Root tracing 查找GC Roots
③ 重新标记:修正并发标记过程中导致的一些变化的标记。
④ 并发清除:清除标记 的内存空间。
缺点:CMS回收线程 =(CPU数量+3)/ 4,cpu核数较少时占用资源明显。无法清除浮动垃圾,GC的同时在运行客户程序,这部分新产生的垃圾无法被标记回收,称为浮动垃圾,CMS的垃圾回收并不是满了之后才会回收在JDK1.6中设置阈值为82%,如果预留空间不够,则会切换至servial Old收集器,效率大大降低。采用标记——清除产生大量的空间碎片,默认情况下会在每次fullGC前进行一次空间整理,此过程中出现长时间停顿。
5、G1收集器(年老代:标记——整理)
6、内存分配与回收
对象创建完成后,首先会在新生代的Eden分配,如果空间不足则会发生一次minor GC,大对象需要连续的内存空间存放,会造成频繁的minorGC,可以设置参数-XX:PretenureSizeThreshold,直接将他们放入年老代,长期存活的对象进入年老代,每熬过一次minorGC对象年龄会被+1,默认加到15 的时候,会进入老年代,或者内存中相同年龄的对象总和大于survivor空间的一半,发生minor GC,发生minor GC之前由于分配担保,年老代会检测下剩余空间是否大于新生代对象的内存总和,小于则发生一次full GC,或者设置HandlePromotionFailure=true,会更具历次晋升年老代的空间计算一个平均值,如果够用则不发生minorGC,-XX:CMSInitiatingOccupancyFraction=58 是指设定CMS在对内存占用率达到58%的时候开始GC (因为CMS会有浮动垃圾,所以一般都较早启动GC)。
7、常用命令
1、查看堆使用情况,gc次数:jstat -gc 6512(pid) 1000(间隔多少时间打印) 1000(打印多少次)
2、查看GC种类: java -XX:+PrintCommandLineFlags -version
UseParallelGC 即 Parallel Scavenge + Parallel Old,再查看详细信息
查看详情: java -XX:+PrintGCDetails -version
3、jmap -dump:format=b,file=/tmp/jmap_heapdump.hprof (生成文件保存目录) <pid>