Java虚拟机学习记录(七)——内存分配与回收策略

一、前言

Java的内存分配,从全局来看,就是堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接的栈上分配,对象的分配主要在新生代的Eden区上,如果开启了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中(大的对象直接进入老年代)。

二、对象优先在Eden分配

Java堆的新生代中,被分为Eden和两个survivor。大多数情况下,对象在Eden区优先分配,当Eden区没有足够的空间进行分配时,虚拟机将会发起一起Minor GC

新生代GC(Minor GC):指发生在新生代的垃圾回收动作。
老年代GC(Major GC/Full GC):指发生在老年代的GC。

三、大对象直接进入老年代

大对象指的是要占用大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象的内存分配堆虚拟机来说是一件很难的事,因为往往会因为找不到这样的连续的内存空间而不得不提前触发一次Full GC。因此,在写程序中要尽量避免这样大对象,尤其是生命周期很短的大对象。大对象的分配一般会直接进入老年代。(这里可以认为JVM是很不支持生命周期很短的大对象的创建的)。

四、长期存活的对象将进入老年代

虚拟机为每个对象定义了一个年龄计数器,在一次GC后仍然存活的对象它的年龄就会+1,如果对象在Eden出生并且经过第一次Minor GC仍然存活并且Survivor能够容纳就会被移到Survivor(其实就是标记-复制法)。当它的年龄达到一定的程度(默认为15岁),就会被移入老年代。可以通过-XX:MaxTenuringThreshold来设定这个年龄阈值。

五、动态年龄判定

为了更好地适应不同程序的内存情况,虚拟机并不是永远都要求对象的年龄必须达到了MaxTenuringThreshold才能进入老年代,如果在Survivor区中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代(节省了一半以上的Survivor内存)。

六、空间分配担保

在新生代的Minor GC是采用的标记——复制法,所以一次Minor GC后会将存活的对象移到另一个空闲的Survivor区中,但是没有人可以保证一次GC后存活的对象是Survivor能够容纳的,那么就需要老年代的进行空间分配担保,以防止在容纳不下的情况下有后备的解决方案。所以过程是:

Minor GC发生之前:检查老年代的连续空间是否能够容纳下新生代的所有对象,如果可以,则可以确保Minor GC是正常的。如果不可以,虚拟机就会检查HandlePromotionFailure设置值是否允许担保失败。如果允许,那么就会检查老年代最大可用连续空间是否大于历次进入老年代对象的平均大小,如果大于,则尝试一次Minor GC;如果小于,或者不允许担保失败,就要进行一次Full GC(所以可以理解为Full GC是为了给Minor GC腾出空间,所以Full GC之后往往跟随着一次Minor GC)。

Java虚拟机学习记录(六)——HotSpot算法实现

一、前言

在JVM运行的过程中,垃圾回收是性能提升的重中之重,垃圾回收的前提是准确判定哪些对象是可以回收的,在前面的学习中说到,Java的大多数虚拟机都是通过可达性分析算法来判定对象能否回收。那么如何去找这些根节点(GC ROOTS)的位置也是一个要解决的问题。

二、HotSpot枚举根节点(GC Roots)

根节点主要在全局性的引用(常量和类的静态属性)和执行上下文中(例如栈帧的本地变量表中),因为这些区域的占用内存往往很大,不可能去逐个检索,因为这个操作太耗时了。

同样的,可达性分析的时间严格要求还体现在GC停顿上,GC停顿就是指在可达性分析期间所有的Java执行线程得全部停下来,等分析完成之后再开始重新运行,如果不停顿,就可能导致分析期间,引用关系还在不断的改变。很显然,这个停顿时间必须短不然用户体验极差。Sun把这个停顿叫做Stop the world。

HotSpot采用一种叫OopMap的数据结构来记录所有的执行上下文和全局引用位置,这样就可以直接得到所有的GC Roots的地址了。

但是在Java程序运行过程中,有很多指令会导致对象的引用关系发生变化,如果每个变化都要写入到OopMap中,那样就需要维护一个很大的OopMap数据结构,会占用大量的空间。所以在Jvm中有一个安全点(SafePoint)的概念。HotSpot只在安全点处记录了这些信息,然后开始GC。安全点的选定不能太少造成GC等待时间过长,也不能频繁GC增加运行负荷。

如何让所有线程跑到安全点时停止下来,有两种方案可以选择,抢先式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)

其中抢先式中断不需要线程的执行代码主动配合,在GC发生时,首先让所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程去主动轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外加上创建对象需要分配内存的地方。

但是安全点只能很好的解决运行中的程序,对于”不运行”的程序也就也就无法进入安全点,也就无法进行GC。这里的不运行指的是线程没有分配到CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程是无法响应JVM的中断请求的,”走”安全的地方去中断挂起,JVM显然也不太可能等待线程重新被分配CPU时间。这种情况需要借助安全区域(Safe Region)来解决。

安全区域指的是在一段代码中,引用关系不会发生变化。在这个区域任意开GC都是安全的。我们也可以把安全区域当成是安全点的扩展。

当线程执行到安全区域中时,首先标识自己进入了安全区域。在这段时间内JVM发起GC就不用管安全区域里的线程了。但是在安全区域内的线程重新获得CPU时间要离开Safe Region时,要检查系统是否已经完成了根节点枚举。完成了才能离开否则就要等待完成。

##三 HotSpot垃圾收集器的实现

a.Serial收集器

这是一款最基本,发展历史最悠久的收集器。这个收集器是一个单线程收集器,并且在收集垃圾时,必须暂停其他所有工作线程。适用于作为Client模式下的虚拟机。

b.ParNew收集器

其实就是Serial收集器的多线程版本。在单CPU的环境中,性能比不上Serial收集器,但是多CPU的时候性能是要好过Serial收集器的。

c.parallel Scavenge收集器

新生代收集器,复制算法,这个收集器与其他收集器的区别在于,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码所花费的时间/(运行用户代码时间+垃圾收集时间)。而其他收集则是关注减少GC停顿的时间。

d.Serial Old收集器

是Serial收集器的老年代版本,使用标记整理算法。也是给Client模式下的虚拟机使用。在server模式下,还有两大用途:1.在JDK1.5之前和Parallel Scavenge配合使用; 2作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure失败时使用。

e.Parallell Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。配合paraller Scavenge使用。

f.CMS收集器

Concurrent Mark Sweep。以获取最短时间停顿为目标,基于“标记-清除”算法。包括四个步骤:
1.初始标记
2.并发标记
3.重新标记
4.并发清除
他有一下几个缺点:
1.对CPU资源敏感,对性能影响大
2.无法清理浮动垃圾
3.容易产生内存碎片,触发Full GC。

g.G1收集器

是一款面向服务器的垃圾收集器。有以下特点:
1.并行和并发
2.分代收集
3.空间整合
4.可预测的停顿

Java虚拟机学习记录(五)-垃圾收集器

一、前言

众所周知,Java与C的一个显著的区别在于c需要手动的去管理内存,而Java几乎不需要去做这样的处理。原因在于Java的虚拟机有一套自己的内存回收策略。

在Java虚拟机运行过程中,虚拟机栈、程序计数器、本地方法栈随着线程的生命周期的变化而变化,因此这一部分的内存是不需要额外的去回收。但是对于Java堆来说,几乎所有的对象的创建(这里之所以说几乎,是因为随着JIT编译器、对象逃逸分析和栈上分配的技术发展,部分对象不需要在堆中分配内存)都是在堆中进行,如果不作内存回收,很快就会被占满内存,那么怎么样去分析得到那些对象已经不再需要则是问题的关键。所以,要实现垃圾回收,先要判断对象是否已死,然后再对已死对象执行回收算法。下面记录几种常见的在垃圾回收中的算法

二、对象是否已死的判定——引用计数法

这种方法原理很简单,就是说每个对象增加一个引用就给他的计数器加1,当引用失效时(这里的引用失效,我觉得是说当程序的执行离开了对象的作用域,那么这个引用就算是失效了),计数器就减一。当计数器再次变为0时,这个对象就判定它已经可以回收了。

但是这种方法也有一个严重的问题,当ObjectA.instance = ObjectB;ObjectB.instance = ObjectA时,这里两个对象互相引用着导致引用计数一直不为0。因此在JVM的主流实现中,都不采用这种方法。据说python的GC算法就是引用计数加上辅助算法完成的。

三、对象是否已死的判定——可达性分析算法

可达性算法是借助树的数据结构的一个算法,通过判定一个对象是否可以通过树的根到达来确定其是否需要回收。引用一张来自网上的图片图片引用地址

可达性分析

在这里,虽然object 8,object 9,object 10,object 11,object 12都互相持有引用,但是因为从GC Roots中无法到达,所以可以判定为可回收对象。很好的解决了引用计数法的弊端。
在Java中,可作为GC Roots的对象包括:
虚拟机栈(栈帧中的本地变量表)中的引用对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

四、垃圾回收算法——标记-清除算法

标记-清除算法共有两个阶段。标记和清除

标记是将内存空间扫描一遍,对所有可回收的对象做标记。清除是对内存空间再做一遍扫描,清除可回收的对象。这种方法有两个问题:一是要做两遍扫描,效率不高,二是会产生大量的内存碎片(回收的对象随机分布造成),当分配一个很大的对象时很有可能找不到这样的连续空间而提前触发一次垃圾回收。因此这种算法大多数的JVM的实现都不使用。

五、垃圾回收算法——复制算法

复制算法最大的特点是将内存空间分为大小相同的两部分。当开始垃圾回收时,只需将存活的对象移到另一块没有使用的内存空间,然后将使用的一边全部回收,这样的做法简单高效,但是对内存的浪费实在是太大了。就像是你买了8G的内存条只能使用4G,你肯定是不愿意的。

但是实际上,现在的商业虚拟机都采用这种算法的优化版本来回收新生代。因为在新生代中(Java堆会分为新生代[Young Generation]和年老代[Old Generation])中的对象绝大多数都是可回收的,那么实际上每次做垃回收时,新生代中存活的对象并不多。所以并不需要按照1:1进行空间分割,一般情况下,会将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和一块Survivor空间,当进行垃圾回收时,将这两块空间中的存活对象移到另一块空闲的Survivor空间。然后将对象全部清除。这样只有10%的内存会被浪费。

经历了几次垃圾回收都依然存活的对象会被放置到年老代中,因此年老代中的对象都是不容易被回收的。

因为没有办法保证每次的垃圾回收过程存活的对象都不超过10%,所以当Survivor空间不够用时,需要依赖其他内存(这里指年老代)进行分配担保(Handle Promotion)。

六、垃圾回收算法——标记-整理法

上面的复制算法很明显不适用于年老代,因为年老代中的对象特点是存活率大。标记-整理算法与标记-清除算法类似,但是它不是直接对对象进行清除,而是将存活的对象向一端移动,然后直接清理掉端边界意外的内存。引用一张来自网络上的图片图片引用地址

此处输入图片的描述

七、扩展一下引用的知识

在四、五的判定对象是否已死中,均涉及到了引用。在上面的表述中,似乎只有引用和死亡两种状态。但是事实上,Java规定了四种引用状态来帮助Java程序获得更好的性能。怎么去理解这个,一般来说,当Java虚拟机的内存足够时,有的对象虽然已经不需要了,但是完全没有必要将它丢弃,只有在进行垃圾回收后内存仍然不足时才将这些对象回收。这样就增加了这些对象的复用。免去了下一次使用这些对象又要重新创建的问题。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2后,Java扩充了引用的概念,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

a.强引用
Object obj = new Object()这种就是强引用,只要这个引用存在,无论如何JVM都不会回收这些对象

b.软引用
软引用用来描述一些还有用但并非必需的对象。用软引用的对象在系统进行过垃圾回收仍然内存不足时才会进行回收。在JDK1.2之后,提供了SoftReference类来实现软引用。

c.弱引用
弱引用也是用来描述非必需的对象,但是强度比软引用还弱一点。在下一次GC时必定会回收。

d.虚引用
虚引用对对象的生存时间构不成影响,就和没有引用与之关联一样,所以任何时候的GC都会将其回收。对一个对象设置虚引用的唯一目的就是这个对象被回收时会收到一个系统通知。在JDK1.2以后,提供了PhantomReference类来实现虚引用。

用代码来检验一下软引用、弱引用和虚引用。

package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class TestReference {
    public static boolean run = true;
    public static void main(String[] args){
        WeakReference<String> weakReference = new WeakReference<String>(new String("weak Reference"));
        SoftReference<String> softReference = new SoftReference<String>(new String("soft Reference"));
        final ReferenceQueue<String> queue = new ReferenceQueue<String>();
        new Thread(new Runnable() {

            @Override
            public void run() {
                while(run){
                    Object object = queue.poll();
                    if(object != null){
                        try {
                            Field referent = Reference.class.getDeclaredField("referent");
                            referent.setAccessible(true);
                            Object result = referent.get(object);
                            System.out.println("即将回收"+result.getClass()+(String)result);
                        } catch (NoSuchFieldException | SecurityException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalArgumentException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }

            }
        }).start();
        PhantomReference<String> phantomReference = new PhantomReference<String>(new String("phantom Reference"), queue);

        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
        System.out.println("*****************下面开始GC******************");
        System.gc();        //System.gc只是建议系统进行垃圾回收,并不是立刻执行
        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
    }
}

上面这段代码的输出为

weak Reference
soft Reference
null
*****************下面开始GC******************
null
soft Reference
null
即将回收class java.lang.Stringphantom Reference

可以看到,软引用在gc之后仍然是存在的,而弱引用gc之后变成null了,虚引用一直为null。并且我们通过新开一个线程来检测虚引用被回收的通知。所以正确的使用soft Reference和weak Reference可以实现缓存和防止内存泄露,而虚引用一般用来实现细粒度的内存控制。比如下面代码实现一个缓存。程序在确认原来的对象要被回收之后,才申请内存创建新的缓存。Java幽灵引用的作用

八、总结

可以看出,不论是对java堆中内存空间进行分代,还是对引用进行四种类型的划分,都是为了解决在java程序运行过程中因为存在各种各样的对象,单一的垃圾回收算法没有办法高效的进行。事实上垃圾回收算法再不断的变化,每一种当前存在的垃圾回收算法都有它适应的运行环境。因此在什么时候使用什么样的垃圾回收算法,对于程序的性能来说也是至关重要的。

Java虚拟机学习记录(四)-对象的内存布局和访问定位

一、前言

jvm创建对象的过程分为类加载检查,分配对象空间,初始化类空间,设置对象信息,对象初始化。那么在分配对象空间时是如何分配的,怎么保证能够定位到对象的内存位置。

二、对象的内存布局

对象在内存中的布局可以分为三部分:对象头(Object Header),实例数据(Instance Data)和对齐填充(Padding)。

a.对象头

对象头有两部分,一部分是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向他的类元数据的指针(表示这个对象是哪个类实例化出来的)。并不是所有的虚拟机实现都必须在对象数据上保留类元数据的指针。因为查找对象的类元数据信息并不一定要经过对象本身(通过句柄)。另外,如果对象是一个Java数组,那在对象头中还必须要有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的类元数据信息确定Java对象大小,但是数据的类元数据中却无法确定数据的大小。

b.实例数据

存储对象中的各种类型的字段内容以及普通对象的指针(oops,Ordinary Object Pointers)。

c.对象填充

不是必然存在,没有特别意义,作为占位符存在。

三、对象的访问定位

建立对象是为了使用对象,对象的访问是通过栈上的reference数据来操作堆上的具体对象。reference是java虚拟机规范的一个指向对象的引用,但并没有规定如何去具体实现,一般来说,有两种实现方式:句柄和直接指针。

a.句柄

采用句柄方式会在Java堆中开辟出一块句柄池空间,Java栈中的本地变量表中存放着指向句柄池中某一个句柄的reference,然后句柄保存有指向某个实例的指针和指向方法区的对象类元数据。
reference---->句柄------>对象和类元数据,共三次指针定位
这种方式的优点是GC清理垃圾时会移动对象地址,栈中的reference不需要改变只需要改变句柄中指向对象的指针。

b.直接指针访问

reference---->对象实例数据(对象实例数据的对象头包含类元指针)------>类元数据,共两次指针定位
这种方式的优点是速度更快,节省了一次指针定位的时间。Sun HotSpot采用的就是这种对象的访问定位方式。

c.注意

在JDK1.8中,已经完全移除了方法区,类元数据的存储放在了本地内存中,这样就不会再收到方法区大小的限制。

Java虚拟机学习记录(三)-对象创建的过程

在Java程序运行时几乎每时每刻都有对象在被创建出来,从语言层面上看,只是new了一个对象,但是在虚拟机中这个对象的创建过程时比较复杂的(这里的对象仅适用于普通的Java对象,不包括数据和Class对象)。我把这其中的步骤总结为下面几步

1.类加载检查
当虚拟机接受到new指令时,首先去查常量池中能否定位到这个类的符号引用,并且检查这个符号引用的类是否已经被加载、解析和初始化过。如果没有那就必须执行类的加载过程。简单来说,就是虚拟机中有没有这个类的信息,如果没有就得去加载。

2.为对象分配内存
对象需要的内存在类加载完成之后就已经完全确定了,为对象分配内存的任务等同于在Java堆上划分出一块确定大小的内存。

这个划分内存的动作有两种情况。

如果Java堆中的内存时规整的,使用过的放一边,未使用的放另一边,中间用一个指针作为分界点的指示器。那么分配内存的动作就是将指针向未使用的那一边移动所需要的内存大小。这种分配方式叫做指针碰撞(Bump the Pointer)。

如果Java堆中的内存时不规整的,这种情况很明显不适用于指针碰撞,一般这种情况下Java虚拟机会维持一张表来记录内存的使用情况,哪些内存使用了,哪些内存时空闲的都会记录好,需要分配内存时在表中查找到合适的内存区域分配,然后更新这张表即可。这张表被称为空闲列表(Free List)

使用指针碰撞还是空闲列表由Java堆是否规整决定,而Java堆是否规整则由Java虚拟机的GC算法是否带有压缩整理功能决定。

在并发环境下,简单的修改指针指向的内存位置并不安全,因为A对象分配内存的时候,指针还没有移动的同时,B对象也要开始分配内存,因此使用的还是未发生改变的指针。解决方案有两种,一种是对分配内存空间的动作作同步处理————实际上虚拟机采用CAS配上失败重试的方式来保证操作的原子性;另一种是把内存分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程分配内存,就在那个线程的TLAB上分配,只有TLAB分配完并分配新的TLAB时,才需要同步锁定。

3.内存初始化
在分配完内存后,虚拟机需要将分配到的内存空间初始化为0(不包括对象头),如果使用TLAB,那么在TLAB分配时就可以完成这一步骤。这一步骤保证了对象中的字段不初始化就能直接使用。

package test;

public class NoInitInt {
    private int noInitInteger;

    public static void main(String[] args){
        int i = 0;
        System.out.println(i);
    }

    public int getNoInitInteger() {
        return noInitInteger;
    }

    public void setNoInitInteger(int noInitInteger) {
        NoInitInt noInitInt = new NoInitInt();
        System.out.println(noInitInt.getNoInitInteger());
    }
}

如上程序所示,输出为0

4.对象设置

Java虚拟机需要设置对象是哪个类的实例,如果才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。这些信息被存放在对象的对象头中(Object Header)

5.初始化

经过上面的步骤,从Java虚拟机的角度一个新的对象已经生成了,但是从Java程序员的角度,这个对象还差一步,就是对象的初始化

Java虚拟机学习记录(二)-运行时数据区域

一、前言

Java虚拟机在执行Java程序的过程中,会把他管理的内存划分为多个不同的数据区域,这些区域各自有各自的用途,以及创建和销毁的时间,有的区域随虚拟机进程的启动而存在,有的区域依赖用户线程的启动和结束而建立和销毁。

Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1.方法区(线程共享),JDK1.7中已经开始改变,JDK1.8中被元空间替代
2.堆(线程共享)
3.虚拟机栈(线程隔离)
4.本地方法栈(线程隔离)
5.程序计数器(线程隔离)

Java虚拟机运行时涉及到的另外的内存区域
1.直接内存

二、方法区(Method Area)

这是一个线程共享的数据区域,用来保存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机将方法区描述为堆的一个逻辑部分(在HotSpot中具体表现为在方法区的内存管理和堆中的内存管理是一套方案,当然这里的一套方案并不是说他们是一模一样的),但是方法区有一个别名叫做Non-Heap(非堆),应该是为了和堆作区别(Heap)。

在HotSpot虚拟机中,方法区又被很多人叫做“永久代”(Permanent Generation),本质上两者并不等价,仅仅是HotSpot团队将GC分代收集扩展至方法区,也就是使用永久代来实现方法区。这样就和Java堆使用了同样的GC分代收集策略,不必重新实现一套管理策略。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的。

然而在实际应用中,使用永久代来实现方法区并不是一个好的选择,更容易遇到内存溢出的问题。在JDK1.7中,已经将字符串常量池从永久代移出了。

字符串常量池的移出:JDK1.7 被转移到Java堆中(Java Heap)

Java虚拟机规范堆方法区的限制宽松,方法区不需要连续的内存,也可以不实现垃圾收集。事实上,垃圾收集的频率在这个区域是很小的,但是并不是所有在此的数据真的是“永久”的,这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载。这个区域的回收成绩难以令人满意。当方法区无法满足内存分配的需求时,会抛出OutOfMemoryError的错误。

关于类型卸载,我理解为在Java虚拟机运行时,会将类信息加载到方法区,当某些类不会用到的时候(unreachable),就会从方法区中卸载这个类以节省内存),具体可以看下面的文章
1.Java类加载原理解析
2.Java虚拟机类型卸载和类型更新解析

在方法区中有一块叫做运行时常量池(Runtime Constant Pool)的区域,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分在类加载到方法区后进入运行时常量池。

字面量(literal): 我的理解为字面量指的几种基本类型。int,boolean,char,float,double,string,long,byte,null
其中float和double统称为floating-point literal,当你创建一个浮点数时,默认的是double类型。这也是为什么float a = 0.1;是错误的,你得float a = (float)0.1;
具体字面量的内容可以参考这里:Java Literals
符号引用: 符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
JVM中的直接引用和符号引用

关于常量池中的一些细节可以看这里的对比
Java常量池

方法区的变迁:
1、JDK1.2 ~ JDK6
在 JDK1.2 ~ JDK6 的实现中,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;

2、JDK7
由于 GC 分代技术的影响,使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行,必须单独处理;并且 Oracle 同时收购了 BEA 和 Sun 公司,同时拥有 JRockit 和 HotSpot,在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难,所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。
JDK7中符号表被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。

3、JDK8
在 JDK8 中,永久代已完全被元空间(Meatspace)所取代。
引用:Java 内存之方法区和运行时常量池

##三、堆(Java Heap)
和方法区一样,这也是一个线程共享的数据区域。在java虚拟机执行Java程序时,它占据了大多数的内存,几乎所有的对象的存储都是在这个区域。为什么说几乎呢?因为随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致一系列微妙的变化发生。

JIT编译器:及时编译器(Just-In-Time compiler)
逃逸分析技术:全部变量赋值,方法返回值,实例引用传递三种情况会发生指针逃逸,如果在方法内新建对象,并且这个对象没有离开过这个方法,则这个对象没有必要在堆中分配内存,直接在Java虚拟机栈中分配内存,省去了在堆中分配内存堆GC造成的压力。
什么是逃逸分析(Escape Analysis)?
栈上分配:将对象在栈上分配内存
标量替换优化技术:Java中的原始类型无法再分解,可以看作标量(scalar);指向对象的引用也是标量;而对象本身则是聚合量(aggregate),可以包含任意个数的标量。如果把一个Java对象拆散,将其成员变量恢复为分散的变量,这就叫做标量替换。拆散后的变量便可以被单独分析与优化,可以各自分别在活动记录(栈帧或寄存器)上分配空间;原本的对象就无需整体分配空间了。
HotSpot 17.0-b12的逃逸分析/标量替换的一个演示

Java堆是垃圾收集器管理的主要区域,又成”GC堆”(Garbage Collected Heap)。

从内存回收的角度来看,现在收集器基本都采用分代收集算法。所以Java堆可以细分为:新生代和老年代,再细分一点,可以分为Eden空间J,From Survivor空间,to Survivor空间

从内存分配的角度来看,Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

Java堆的内存不要求在空间上是连续的,只要在逻辑上连续即可。

四、虚拟机栈(Java Virtual Machine Stack)

线程私有,和线程的生命周期相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧从虚拟机栈入栈到出栈的过程。

局部变量表中存放了编译区可知的各种基本数据类型(boolean,byte,char,int,float,double,long,short)、对象引用(reference,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄和其他与此对象相关的位置)

64位的double和long会占据两个局部变量空间(slot),其余数据类型只占一个。局部变量表所需的空间在编译期间分配完成,当进入一个方法时,这个方法需要在帧中分配多少局部变量空间完全时确定的。

在Java虚拟机中,对这个区域规定了两中异常情况。1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,则会抛出OutOfMemoryError异常。

五、本地方法栈(Native Method Stack)

跟虚拟机栈类似,不过是用来存储本地方法的。

六、程序计数器(Program Counter Register)

线程私有,这是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个。

每个线程都需要一个独立的程序计数器,保证各条线程执行中不会混乱。

如果当前执行的时Java方法,这个计数器记录的正是当前正在执行的虚拟机字节码指令的位置,如果执行的时Native方法,这个计数器为空。此区域时唯一一个在Java虚拟机规范中没规定任何OutOfMemoryError的区域。

七、直接内存(Direct Memory)

在JDK1.4中加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这个内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了频繁的在Java堆中和Native堆中复制数据。

Java虚拟机学习记录(一)-Java技术体系

##一、java技术体系概览

JDK(Java Development Kit)

  • Java程序设计语言
  • Java虚拟机
  • Java API类库

JDK是用于支持Java程序开发的最小环境。

JRE(Java Runtime Environment)

  • Java SE API子集
  • Java虚拟机

JRE是支持Java程序运行的标准环境

Java语言 Java Language
工具及工具API Java Javac JavaDoc Jar Javap Monitor JPDA
JConsole Java VisualVM JavaDB security Internationalization JMC RMI
IDL Deploy JFR Troubleshoot Scripting JVMTI Web Services
程序部署和发布 Java Web Start Applet/Java Plug-in
用户界面工具集 JavaFX
Swing Java 2D AWT Accessbility
Drag and Drop Input Methods Image I/O Print Service Sound
集成库 IDL JDBC JNDI RMI RMI-IIOP Scripting
其他基础库 Beans Int’l Support Input/Output JMX
JNI NetWorking Override Mechanism
Security Serialization Extension Mechanism XML JAXP Mechanism
语言和工具基础库 Math Collections Concurrency Utilities JAR
Logging Management Preferences API Ref Objects
Reflection Regular Expressions Versioning Zip Instrumentation
Java虚拟机 Java HotSpot Client and Server VM

这张表网上有很多版本,我以《深入理解java虚拟机》为参考,添加了JMC和JFR。可能在这划分中会有重叠部分,仅做参考。
其中

  • JDK: Java语言,工具及工具API,程序部署发布,用户界面工具集,集成库,其他基础库,语言和工具基础库,Java虚拟机
  • JRE: 程序部署发布,用户界面工具集,集成库,其他基础库,语言和工具基础库,Java虚拟机
  • Java SE API: 用户界面工具集,集成库,其他基础库,语言和工具基础库,Java虚拟机

##二. Java工具和工具API(对应了jdk中bin目录下的工具)
###自己目前使用过的有:
– java : 用来运行jar。对应bin目录下的java。
– javac: java的编译工具。对应bin目录下的javac。
– JConsole : 配置好环境的情况下,可以在控制台输入jconsole打开,jconsole可以配合JMX(其他基础库),达到对运行中的java程序进行监控甚至动态修改变量值的作用。具体的使用可以参考这一篇JMX整理,对应bin目录下的jconsole。
– security :
– keytool: 这个工具可以生成key和certificate,具体使用可以参考这一篇Java Security:keytool工具使用说明,securitykeytool 。对应bin目录下的keytool工具。
– jarsigner: jar密匙签名工具
– kinit: 主要用于获取或缓存Kerberos协议的票据授权票据。
– klist: 允许用户查看本地凭据缓存和密钥表中的条目(用于Kerberos协议)。
– ktab: Kerberos密钥表管理工具,允许用户管理存储于本地密钥表中的主要名称和服务密钥。
– policytool: 策略工具,用于管理用户策略文件(.java.policy)。

###尚未使用过的有
– javaDoc : 使用 javdoc 编译 .java 源文件时,它会读出 .java 源文件中的文档注释,并按照一定的规则与 Java 源程序一起进行编译,生成文档。对应bin目录下的javadoc。
– jar : JAR(Java Archive,Java 归档文件),是java 开发工具中的一个工具,位于JDK的安装目录的bin目录下。它是一个打包工具,有点类似winrar压缩工具,虽然一般是用来打包.class文件,但是实际上其它文件也是可以打包的。对应bin目录下的jar。
– Javap : java反编译工具。对应bin目录下的javap。
– JPDA : Java Platform Debugger Architecture(JPDA:Java平台调试架构),Java虚拟机后端和调试平台前端组成
– 1.Java虚拟机提供了Java调试的功能
– 2.调试平台通过调试交互协议向Java虚拟机请求服务以对在虚拟机中运行的程序进行调试
– jdb: Java调试工具(Java Debugger),主要用于对Java应用进行断点调试。
– Java VisualVM : java性能分析工具,对应bin目录下的jvisualvm。
– JavaDB : java的数据库。
– Int’l:可能指的是internationalization,即国际化。这里很不确定,在bin目录下有一个native2ascii,本地编码到ASCII编码的转换器(Native-to-ASCII Converter),用于”任意受支持的字符编码”和与之对应的”ASCII编码和(或)Unicode转义”之间的相互转换。
– JFR: Java飞行记录(好奇怪的翻译,Java Flight Recordings)。Java Flight Recordings (JFR) — Java 飞行记录器 – part 1
– JMC: Java任务控制工具(java Mission Control)。对应bin目录下的jmc
– RMI: Java远程方法调用(Remote Method Invocation,之前用过类似与thrift这种跨语言的远程调用框架,原理是socket通信)。对应bin目录下的
– java-rmi: Java远程方法调用(Java Remote Method Invocation)工具,主要用于在客户机上调用远程服务器上的对象。
– rmic: Java RMI 编译器,为使用JRMP或IIOP协议的远程对象生成stub、skeleton、和tie类,也用于生成OMG IDL。
– rmid: Java RMI 激活系统守护进程,rmid启动激活系统守护进程,允许在虚拟机中注册或激活对象。
– rmiregistry: Java 远程对象注册表,用于在当前主机的指定端口上创建并启动一个远程对象注册表。
– jstatd: jstatd(VM jstatd Daemon)工具是一个RMI服务器应用,用于监测HotSpot JVM的创建和终止,并提供一个接口,允许远程监测工具附加到运行于本地主机的JVM上。
– serialver: 序列版本命令,用于生成并返回serialVersionUID。
– IDL: 与RMI类似,是面向对象的远程调用,但不同的是他是跨语言的。对应bin目录下的
– idlj: IDL转Java编译器(IDL-to-Java Compiler),用于为指定的IDL文件生成Java绑定。IDL意即接口定义语言(Interface Definition Language)。
– servertool: Java IDL 服务器工具,用于注册、取消注册、启动和终止持久化的服务器。
– tnameserv: Java IDL瞬时命名服务。
– orbd: 对象请求代理守护进程(Object Request Broker Daemon),它使客户端能够透明地定位和调用位于CORBA环境的服务器上的持久对象。
– deploy:
– javafxpackager: JavaFX包装器,用于执行与封装或签名JavaFX应用有关的任务。
– pack200: JAR文件打包压缩工具,它可以利用Java类特有的结构,对普通JAR文件进行高效压缩,以便于能够更快地进行网络传输。
– unpack200: JAR文件解压工具,将一个由pack200打包的文件解压提取为JAR文件。
– Monitor: 我这里理解为一系列的java运行监视工具
– JPS:JVM进程状态工具(JVM Process Status Tool),用于显示目标系统上的HotSpot JVM的Java进程信息。对应bin目录下的jps。
– JSTAT: JVM统计监测工具(JVM Statistics Monitoring Tool),主要用于监测并显示JVM的性能统计信息。对应bin目录下的jstat。
– Troubleshoot: java的一系列错误定位工具
– JINFO: Java配置信息工具(Java Configuration Information),用于打印指定Java进程、核心文件或远程调试服务器的配置信息。对应bin目录下的jinfo。
– JStack: Java堆栈跟踪工具,主要用于打印指定Java进程、核心文件或远程调试服务器的Java线程的堆栈跟踪信息。
– JMAP: ava内存映射工具(Java Memory Map),主要用于打印指定Java进程、核心文件或远程调试服务器的共享对象内存映射或堆内存细节。。对应bin目录下的jmap。
– jhat: Heap Dump Browser, 根据dump文件进行分析,可以在浏览器中查看。
– jsadebugd: Java可用性代理调试守护进程(Java Serviceability Agent Debug Daemon),主要用于附加到指定的Java进程、核心文件,或充当一个调试服务器。
– Scripting:
– jrunscript: Java命令行脚本外壳工具(command line script shell),主要用于解释执行javascript、groovy、ruby等脚本语言。
– JVMTI: Java 虚拟机工具接口(Java Virtual Machine Toolkit Interface),用于替代在先前的 JDK 版本中作为试验功能存在的 Java 虚拟机剖析接口(Java Virtual Machine Profiling Interface,JVMPI)和 Java 虚拟机调试接口(Java Virtual Machine Debugging Interface,JVMDI)。通过 JVMTI 接口可以创建代理程序(Agent)以监视和控制 Java 应用程序,包括剖析、调试、监控、分析线程等等。
– Web Services:
– wsgen: 当从 Java 代码启动时,wsgen 命令行工具将生成 Java API for XML Web Services (JAX-WS) 应用程序所必需的工件。
– wsimport: wsimport 命令行工具用于处理现有 Web Service 描述语言 (WSDL) 文件,并生成开发 Java API for XML-Based Web Services (JAX-WS) Web Service 应用程序所必需的工件。
– schemagen: XML schema生成器,用于生成XML schema文件。
– xjc: 主要用于根据XML schema文件生成对应的Java类。

##程序部署和发布
– Java Web Start:允许用户直接从网络上运行基于java技术的应用。共有三种运行方式,无论哪一种,都会连上网络运行
– 1.从网页上点击链接。
– 2.从桌面图标或者开始菜单。
– 3.从java缓存视图中。
– Applet/Java Plug-in)
applet小程序
##用户界面工具集
– JavaFx: 2007年首次推出,具体资料可以看这些。JavaFX对Java开发者到底意味着什么JavaFX: Getting Started with JavaFX
– Swing: 是所谓的Lightweight组件,不是通过native方法来实现的,所以Swing的窗口风格更多样化。但是,Swing里面也有heaveyweight组件。比如JWindow,Dialog,JFrame。Swing由纯Java写成,可移植性好,外观在不同平台上相同。所以Swing部件称为轻量级组件, Swing是由纯JAVA CODE所写的,因此SWING解决了JAVA因窗口类而无法跨平台的问题,使窗口功能也具有跨平台与延展性的特性,而且SWING不需占有太多系统资源,因此称为轻量级组件
– Java 2D:包括Graphics,Graphics2D接口。Trail: 2D Graphics
– AWT:调用系统的native接口绘制,所以风格和系统相关。由于不同 操作系统 的图形库所提供的功能是不一样的,在一个平台上存在的功能在另外一个平台上则可能不存在。为了实现Java语言所宣称的”一次编译,到处运行”的概念,AWT 不得不通过牺牲功能来实现其平台无关性,也就是说,AWT 所提供的图形功能是各种通用型操作系统所提供的图形功能的交集。由于AWT 是依靠本地方法来实现其功能的,我们通常把AWT控件称为重量级控件。
– Accessbility: 无障碍使用,针对残疾人士。Java Accessibility Guide
– Drag and Drop: 实现拖放功能。Lesson: Drag and Drop and Data Transfer
– Input Methods:用户输入
– Image I/O:图片的输入输出
– Print Service:打印服务
– Sound:声音

##三. 集成库
– IDL: Java IDL技术添加CORBA(Common Object Request Broker Architecture)到Java平台,提供了标准的互操性和连通性。Java IDL使得分布式,web的java应用能够使用OMG(Object Management Group)定义的行业标准的IDL(Interface Definition Language)语言和IIOP(Internet Inter-ORB Protocol) 协议透明的调用远程服务。
– JDBC: Java数据库连接API(Java Database Connectivity API)。
– JNDI: Java 命名与目录接口(Java Naming and Directory Interface),所有与系统外部的资源的引用,都可以通过JNDI定义和引用。通俗点理解我觉得应该就是将配置用xml保存。JNDI 是什么
– RMI: 远程方法调用API(Remote Method Invocation)。
– RMI-IIOP:通过IIOP(Internet Inter-ORB Protocol)协议的远程方法调用(RMI)。
– Scripting:能让java应用运行js引擎。

##四. 其他基础库
– Beans: Java Beans是Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是
– 可序列化
– 提供无参构造器
– 提供getter方法和setter方法访问对象的属性。

名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。

    public class PersonBean implements java.io.Serializable {

    /**
     * name 属性(注意大小写)
     */
    private String name = null;

    private boolean deceased = false;

    /** 无参构造器(没有参数) */
    public PersonBean() {
    }

    /**
     * name 属性的Getter方法
     */
    public String getName() {
        return name;
    }

    /**
     * name 属性的Setter方法
     * @param value
     */
    public void setName(final String value) {
        name = value;
    }

    /**
     * deceased 属性的Getter方法
     * 布尔型属性的Getter方法的不同形式(这里使用了is而非get)
     */
    public boolean isDeceased() {
        return deceased;
    }

    /**
     * deceased 属性的Setter方法
     * @param value
     */
    public void setDeceased(final boolean value) {
        deceased = value;
    }
}
  • Internationalization Support: 国际化支持。特点如下:
    • 额外的本土化数据和任何地方运行结果相同
    • 文字信息存储在代码之外,动态加载(方便程序的翻译)
    • 不需要重新编译就能支持新的语言
    • 基于文化的数据,比如日期,货币,呈现方式
    • 可以快速本土化
  • Input/Output: 输入/输出
    • 通过数据流、序列化、文件系统的输入输出
    • 字符集、解码、编码、byte到unicode直接的转换
    • 文件、文件属性、文件系统
    • 提供建立使用异步的或可复用的、无阻塞的输入输出的服务的API
    • java.io (description) – Supports system input and output, and object serialization. to the file system
    • java.nio (description) – Defines buffers for bulk memory operations. Buffers may be allocated in direct memory for high performance.
    • java.nio.channels (description) – Defines channels, an abstraction for devices capable of performing I/O operations; defines selectors for multiplexed, non-blocking I/O
    • java.nio.channels.spi (description) – Provides implementations for channels
    • java.nio.file – Defines interfaces and classes to access files and file systems.
    • java.nio.file.attribute – Defines interfaces and classes for accessing file system attributes.
    • java.nio.file.spi – Defines classes for creating a file system implementation.
    • java.nio.charset (description) – Defines charsets, decoders, and encoders, for translating between bytes and Unicode characters
    • java.nio.charset.spi (description) – Provides implementations for charsets
  • JMX: Java管理扩展(Java Management Extensions)。提供接口结合jconsole工具使用,可以实时查看java程序运行时的变量值,以及修改他们。
  • JNI: 提供java对c的支持,可以调用c的方法。
  • NetWorking: 提供使用URLS,URIS的类,socket类提供连接服务,安全功能。
  • Override Mechanism: 重载机制。
  • Security: 提供安全相关功能的API,例如可配置的访问控制,电子签名,认证和签名,加密,安全网络交互。
  • Serialization: 序列化,可以将对象(Object)序列化变成二进制流,然后从二进制流中读取出对象。
  • Extension Mechanism: 扩展机制。在我的理解中,是指java的包机制,类加载机制,包括从URL中获取。
    • java.lang.ClassLoader
    • java.lang.Package
    • java.lang.Thread
    • java.net.JarURLConnection
    • java.net.URLClassLoader
    • java.security.SecureClassLoader
  • XML JAXP Mechanism:提供丰富的API集用来处理XML文档和数据

##五. 语言和工具基础库
– Math: 提供和数学计算相关的方法
– Collections:
– 1.分为两类
– 最基本的一类是 java.util.Collection,有以下的后代
– java.util.Set
– java.util.SortedSet
– java.util.NavigableSet
– java.util.Queue
– java.util.concurrent.BlockingQueue
– java.util.concurrent.TransferQueue
– java.util.Deque
– java.util.concurrent.BlockingDeque
– 另外的一类是 java.util.Map,Map并不是真正的Collections,但是他们有着和Collections类似的接口,所以可以像Collections一样操作。
– java.util.SortedMap
– java.util.NavigableMap
– java.util.concurrent.ConcurrentMap
– java.util.concurrent.ConcurrentNavigableMap
– 2.许多Collections的修改操作都是可选的,当尝试使用没有实现的修改操作时会抛出UnsupportedOperationException,接口的实现必须明确哪些方法是支持的。
– 如果Collections不可修改,则是unmodifiable,如果可以修改,则是modifiable
– 如果Collections一成不变,则是immutable,如果是变化的,则是mutable
– 如果Lists的大小(Size)是不变的,则是fixed-size,否则则是variable-size.
– 如果Lists支持快速的索引访问(时间为常量),则是Random Access,不支持则是sequential access。RandomAccess标记接口意味着Lists支持Random Access。
– 3.有一些元素的存储是有限制的(比如Map的key–value)
– 是一个特定的类型
– 不能为null
– obey some arbitrary predicate
– 4.接口表格

    |Interface|HashTable|Resizable Array|Balanced Tree|Linked List|Hash Table + Linked List|
    |:----:|
    |Set      |HashSet  |               |TreeSet      |           |LinkedHashSet           |
    |List     |         | ArrayList     |             |LinkedList |                        |
    |Deque    |         | ArrayDeque    |             |LinkedList |                        |
    |Map      |HashMap  |               |TreeMap      |           |LinkedHashMap           |

- 5.AbstractCollection, AbstractSet, AbstractList, AbstractSequentialList and AbstractMap类已经提供了基本功能的实现
- 6.Concurrent Collection(提供多线程下的Collections)
    - 接口
        - BlockingQueue
        - TransferQueue
        - BlockingDeque
        - ConcurrentMap
        - ConcurrentNavigableMap
    类
        - LinkedBlockingQueue
        - ArrayBlockingQueue
        - PriorityBlockingQueue
        - DelayQueue
        - SynchronousQueue
        - LinkedBlockingDeque
        - LinkedTransferQueue
        - CopyOnWriteArrayList
        - CopyOnWriteArraySet
        - ConcurrentSkipListSet
        - ConcurrentHashMap
        - ConcurrentSkipListMap
- 7.额外注意事项
   -  HashTable在官方的Collections中并没有任何提及,通过查看HashTable的源代码,发现它是继承自Dictionary,实现了Map的接口。而HashMap则是Map的实现,继承的AbstractMap也是Map的实现
    - HashTable和HashMap有以下的区别:
        - HashMap => 不同步、可空键值、效率高、containsKey/containsValue
        - Hashtable => 同步、非空键值、效率略低、contains/containsKey/containsValue

8.这一部分的内容越整理越多,还是另开一篇去记录。

– Concurrency Utilities: 提供了一系列支持并发的接口和类
– java.util.concurrent
– java.util.concurrent.atomic:一系列的原子性类,比如AtomicInteger这样的,不需要额外的同步操作就可以支持并线程并发
– java.util.concurrent.locks:提供了多种锁机制

  • JAR: 提供格式化读写JAR文件的类
  • Logging: 提供日志功能
  • Management: 提供一系列标准的接口(JMX)用来管理资源,例如应用、设备、服务、Java虚拟机。关于JMX的内容上面以及整理过了。
  • Preferences API: 提供存储,读取用户和系统配置的方法。
  • Ref(Reference Objects): 提供了和垃圾收集器相关的接口,程序可以使用引用对象来获取一个对象的引用,这样做在之后的垃圾回收中仍然可能会将这个引用对象回收。程序也可以被设计成当垃圾收集器认为一个给定的对象的可达性(gc(垃圾回收)的可达性算法,这个在后续的垃圾回收算法中会整理出来)改变时被通知。因此,引用对象在设计简单的缓存时是很有用的,因为在低内存的情况下它会被自动回收,内存充裕时,因为对象保持了引用,所以不会被垃圾回收器回收。
  • Objects: 万物皆对象,你有吗。
  • Reflection: Java反射,也是很有用的一个库。spring的IOC就是Java反射的使用 。
  • Regular Expressions:正则表达式,也是很常用的库。
  • Versioning:可以让java程序在运行时被识别它的需求的运行环境的版本号。
  • Zip: 指的应该是Java Archive (JAR) Files,可以将多个文件压缩成一个的技术。
  • Instrumentation: 和Sound相关

##六. Java虚拟机
– HotSpot Client and Server VM: 现在我们最多接触到的应该就是HotSpot虚拟机了,但是在java最初发布到现在,出现过许多或经典或优秀或有特色的虚拟机实现。
– 1.Sun Classic/Exact VM
– 2.Sun HotSpot VM
– 3.Sun Mobile-Embedded VM/Meta-Circular VM
– 4.BEA JRockit/IBM J9 VM
– 5.Azul VM/BEA Liquid VM
– 6.Apache Harmony/Google Android Dalvik VM
– 7.Microsoft JVM及其他
##七.参考文章
1. JDK自带工具一览表
2. java监控工具(jps,jstat,jstack,jmap,jvisualvm等)
3. JDK Tools and Utilities
4. Java Web Start Technology
5. Java™ Platform Overview
6. Java™ Naming and Directory Interface (JNDI)
7. Java™ Internationalization Support
8. Java™ I/O, NIO, and NIO.2
9. The Extension Mechanism
10. Collections Framework Overview
11. Java Concurrency Utilities
12. 《深入理解Java虚拟机第一章》

c++和Java的对象内存异同

##前言

好久都没有写博客了。经过一段时间的实习,收获上感觉并不是很大,还好自己也有看一些东西。因为毕业设计做的是c++方面的编程,算是初探c++的内容。这里简单记录自己对c++和java的对象内存区别,谈谈c++为什么性能要比java高。想到哪里就记到哪里,因为很多东西自己也要查证才能确定。

##1.栈和堆

栈和堆在数据结构上是两种不同的结构,栈的特点是先进后出,而堆则可以看做是一大堆数据的集合。在c++和Java的内存中,也有堆和栈的概念。
首先理解一下,内存中栈中的每一个元素都被称为栈帧,每个栈帧中存储一个方法(function)中的用到的变量内存。程序执行时,就是一个栈中的每一帧被读取执行的过程。堆则是主要用来存储对象。那么在c++和java中到底有什么不同呢?

“`c++
object* processObject(int param){
object obj;
obj.param = param;
return &obj;
}

int main(){
object* obj = processObject(10);
obj->do_something(); //1
}

<pre><code><br />1处是会出现一个空指针错误的。而如果这是一段java的代码则完全没有问题。

在c++和java中,一个栈帧被执行完,这部分内存就直接被释放了。而在c++中,所有通过声明产生的对象都是在栈上开辟内存空间,所以函数执行完后这个对象也就被释放了,那么返回出来就是一个空指针了。只有通过new生成的对象才会在堆中申请空间,因此通过new出来的对象都需要在不用时手动释放内存,不然就会内存泄露。而在java中,对象是在堆中生成的(JDK7中的字符串是在常量池中,int等字面值在限定范围内也会在常量池中申请内存),所有方法内的对象都是一个指向堆中相应对象的指针(注意是指针而不是引用,很多人认为java中向方法传递参数是引用传递,但其实是值传递,只不过这个值是指向对象的指针)。而在java中不需要手动释放内存则是因为Java拥有GC机制(Garbage collect,垃圾回收)。这个在之后再谈。

##2 java的强弱引用和c++的智能指针

java的强弱引用和c++的智能指针都是在希望可以更好的进行内存管理的前提下出现的,java的强弱引用可以帮助GC机制进行粒度更细的内存回收,而c++的智能指针则是让没有GC机制的c++有了一定能力的自动释放内存的能力。
###a. java的强弱引用
java的引用共有四种,分别为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
之所以定义出这么多引用,是希望在GC发生时,可以更灵活的进行对象的销毁。

强引用就是通过new出来的对象,强引用表示的都是必需的对象,这是无论如何都不会被回收的。

而软引用则表示有用但非必需的对象。当系统内存不够将要发生内存溢出异常的时候,会将软引用的对象列入回收范围,进行二次回收。只有这次回收仍然内存不足才会抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现这个。

弱引用也用来表示非必需的对象,不同的是,这个引用并不能帮助对象躲过任何的GC,也就是说无论如何,发生GC时这个对象都会被回收,弱引用的唯一作用就是用来取得一个对象实例。JDK1.2之后提供了WeakReference类来实现这个。

综合上述,虚引用的存在也比较清晰了。它不仅不能帮助对象躲过GC,甚至不能取得对象的实例。唯一存在的用处就是当其引用的对象被GC回收时会收到一个系统的通知。JDK1.2之后使用PhantomReference类来实现。

###b. C++的智能指针
相比于Java的GC机制以及为GC服务的各种引用,C++的智能指针则稍显简单。但是这个看似简单的c++的智能指针,对于c++来说意义也许并不是那么低。在我的理解中,c++之所以实用,因为相对于c,c++有着丰富高效的STL库和被大多数开发者接受的OOP。对于一个巨大的项目来说,OOP往往可以更好的帮助系统模块化,降低耦合,而STL库则是开发者进行高效工作的基础。相对于Java,单单从c++没有GC机制,就意味着它有着更高的性能,另外Java的虚拟机也是影响性能的一个方面。可也是因为c++没有GC机制,对于一个多人合作的巨大的项目来说,内存泄露则是面临的首要问题。举个很低端的例子
“`c++
class Example{
Mysql* getMysqlConnect(){
Mysql* mysql = new Mysql;
return mysql;
}
};

一个项目中所有的mysql对象指针都通过上面的getMysqlConnect()获取,看似没有问题,但是没有人敢保证一个项目中所有使用这个方法的人都会在使用完mysql对象后,手动将其释放。因此这里需要一个shared_ptr或者unique_ptr去包装一下指针,这样调用函数的人就不需要在外部释放对象内存,也就避免了内存泄露的问题。当然在使用shared_ptr时也一定要注意,因为可能会出现循环引用的问题,循环引用则会导致对象内存一直不被释放。
当然了,上面的例子只是为了说明这个例子而列举出来的,事实上可以直接返回对象而不是指针。
c++
class Example{
Mysql getMysqlConnect(){
Mysql mysql;
return mysql;
}
};

这里我一开始以为是会将对象复制一遍返回出来,但事实上在c++11中有了移动语义,即这种在拷贝语义和移动语义中,会优先使用移动语义来将mysql对象从方法中移出来,而不是拷贝mysql对象返回,并把方法中的对象删除。
##3 c++如何尽量避免内存泄露

后天再写啦

linux下c/c++的内存泄漏分析

使用valgrind进行内存分析

简介

官网地址:http://valgrind.org
主要提供以下工具:

Memcheck 是一个内存错误的检测工具,帮助你的程序,尤其是c/c++程序出现更少内存的问题。
Cachegrind 是一个缓存和分支预测探查工具,帮助你的程序运行的更快。
Callgrind 也是一个和缓存相关的调用图工具,和Cachegrind有一部分重叠,但也生成一些Cachegrind不提供的信息。
Helgrind 是一个线程错误的检测工具,在多线程场景下能派上用场。
DRD 也是一个线程错误的检测工具,与Helgrind功能一样,但是使用了不同的分析技术,可能会发现不同的问题。
Massif 是一个堆分析工具,帮助程序使用更少的内存。
DHAT 是不同与Massif的堆分析工具,帮助你理解块的生命周期, 块的利用率, 以及layout的低效。
SGcheck 是一个实验性的工具,帮助检测栈的超支和全局数组。是Memcheck的功能方面的补充:可以检测出Memcheck无法发现的问题,反之亦然。
BBV 是一个实验SimPoint基本块向量生成器。对于进行计算机体系结构研究和开发的人来说,这是有用的。

这里记录的只是Memcheck的使用,其他的使用可以参考上述的官网的网址。

ubuntu下的安装

sudo apt-get install valgrind

使用方法

valgrind [valgrind-options] your-prog [your-prog-options]

例如,对ls -l进行分析:

valgrind –tool=memcheck ls -l

valgrind的默认工具就是memcheck,所以使用memcheck工具时可以省略–tool参数

注意事项:

1、valgrind工具会减慢程序的运行速度
2、程序编译时需要开启-g,帮助valgrind可以更精准的定位到错误
3、程序编译时最好关闭优化,否则可能产生不正确的未初始化错误信息,以及遗漏未初始化错误。
4、程序编译时最好使用-Wall,帮助valgrind在高优化等级的程序中精准识别一些甚至是全部的问题

具体使用说明

Valgrind会记录下一些注释,文本流,具体的错误报告以及其他的重要的事件。类似与以下的格式:

==12345== some-message-from-Valgrind

12345代表进程Id,这个格式方便区分程序输出和Valgrind的注释输出,以及区分多个进程的输出。Valgrind只会输出最重要的信息,如果需要一些次要的信息,可以使用-v参数。

你可以使用三种方式去导出这些错误

1、默认情况:会直接在控制台打印出来
2、使用文件记录,这个时候需要使用参数–log-file=filename,filename代表存储的文件名
3、通过socket发送:使用参数–log-socket=192.168.0.1:12345,不加端口号会使用默认的1500端口,Valgrind提供了一个叫Valgrind-listener的工具去监听这个网络流。

读懂memcheck工具产生的错误信息

1.非法读/非法写的错误(Illegal read/Illegal write errors)

例如:

Invalid read of size 4
at 0x40F6BBCC: (within /usr/lib/libpng.so.2.1.0.9)
by 0x40F6B804: (within /usr/lib/libpng.so.2.1.0.9)
by 0x40B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326)
by 0x40AC751B: QImageIO::read() (kernel/qimage.cpp:3621)
Address 0xBFFFF0E0 is not stack’d, malloc’d or free’d

出现这个错误是因为程序读或写了Valgrind认为不应该读写的内存区域

2.使用了为初始化的值

例如:

Conditional jump or move depends on uninitialised value(s)
at 0x402DFA94: _IO_vfprintf (_itoa.h:49)
by 0x402E8476: _IO_printf (printf.c:36)
by 0x8048472: main (tests/manuel1.c:8)
这样一段错误可能就是由以下的代码产生

int main()
{
int x;
printf ("x = %d\n", x);
}

Valgrind会跟踪变量x,直到x被使用时才会报错。在这里x被传入了printf,进而进入_IO_printf,但是这些都不会报错,只有当x被传递到_IO_vfprintf,_IO_vfprintf开始检查x是否可以被转换为ASCII码时才报错。

未初始化值一般有两种情况:

  • 1、局部变量没有被初始化,就像上面一样。
  • 2、The contents of heap blocks (allocated with malloc, new, or a similar function) before you (or a constructor) write something there.

为了找到未初始化变量一开始的位置,可以使用–track-origins=yes参数。当然这会减慢Valgrind的使用速度。

3.在系统调用中使用了未初始化或者不可寻址的值

Valgrind会检查所有系统调用的参数,一般有以下3类:

  • 1、检查所有直接调用的参数,即使已经初始化了。
  • 2、如果系统调用需要你的程序申请的缓冲区,Valgrind会检查所有的缓冲区内容,看它是否可寻址,内容是否初始化了。
  • 3、如果系统调用需要写入用户提供的缓冲,Valgrind会检查是否可寻址。

下面是两个使用了无效参数的系统调用的例子:

#include
#include
int main( void )
{
char* arr = malloc(10);
int* arr2 = malloc(sizeof(int));
write( 1 /* stdout */, arr, 10 );
exit(arr2[0]);
}

得到这样的错误信息:

Syscall param write(buf) points to uninitialised byte(s)
at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so)
by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so)
by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out)
Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc’d
at 0x259852B0: malloc (vg_replace_malloc.c:130)
by 0x80483F1: main (a.c:5)

Syscall param exit(error_code) contains uninitialised byte(s)
at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so)
by 0x8048426: main (a.c:8)

write(a)和exit(b)都是错误的,a从堆中向标准输出中写入了未初始化的arr。b向exit传递了为初始化的值。注意a的错误在于arr指向的内存区域,而b的错误直接是arr2[0]。

4.非法的释放(Illegal frees)

例如:

Invalid free()
at 0x4004FFDF: free (vg_clientmalloc.c:577)
by 0x80484C7: main (tests/doublefree.c:10)
Address 0x3807F7B4 is 0 bytes inside a block of size 177 free’d
at 0x4004FFDF: free (vg_clientmalloc.c:577)
by 0x80484C7: main (tests/doublefree.c:10)

这个例子中,一块区域被free了两次,所以出现Illegal frees的错误。

5.使用不合适的释放函数去释放堆区域的内存

例如:

Mismatched free() / delete / delete []
at 0x40043249: free (vg_clientfuncs.c:171)
by 0x4102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149)
by 0x4C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60)
by 0x4C261F0E: PptXml::~PptXml(void) (pptxml.cc:44)
Address 0x4BB292A8 is 0 bytes inside a block of size 64 alloc’d
at 0x4004318C: operator new[](unsigned int) (vg_clientfuncs.c:152)
by 0x4C21BC15: KLaola::readSBStream(int) const (klaola.cc:314)
by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416)
by 0x4C21788F: OLEFilter::convert(QCString const &) (olefilter.cc:272)

这个错误是因为使用new[]开辟内存空间,却使用了free去释放内存。
使用malloc, calloc, realloc, valloc or memalign,必须使用free释放内存。
使用new, 必须使用delete释放内存。
使用new[],必须使用delete[]释放内存。

6.源内存区域和目标内存区域重叠

memcpy, strcpy, strncpy, strcat, strncat这些函数可以从源内存区域复制内容到目标内存区域。这两块区域是不可以重叠的。POSIX标准规定这种行为是未定义的。
例如

==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21)
==27492== at 0x40026CDC: memcpy (mc_replace_strmem.c:71)
==27492== by 0x804865A: main (overlap.c:40)

7.Fishy argument values

所以的内存分配函数都指定了分配的内存大小,这个大小必定为正数,或者一般情况下不会极度的大。例如在64位的机器上,不会申请分配2^63大小的内存。这种为负数的或者过于大的参数被成为Fishy argument。
例如:

==32233== Argument ‘size’ of function malloc has a fishy (possibly negative) value: -3
==32233== at 0x4C2CFA7: malloc (vg_replace_malloc.c:298)
==32233== by 0x400555: foo (fishy.c:15)
==32233== by 0x400583: main (fishy.c:23)

8.内存泄漏检测

Valgrind会跟踪所有由malloc或new申请的内存,所以当程序退出时,Valgrind知道哪些内存没有被主动释放。
如果–leak-check参数设置得当,对于每一个未被释放的内存块,Valgrind判断从root-set中的指针是否能到达这些内存块。root-set包含(a)普通的所有线程使用的寄存器,(b)初始化的, 对齐的, 指针大小的数据块,包括栈。一个数据块有两种方式可到达,第一种是“start-pointer”,即指针在数据块的开头;第二种是“interior-pointer”,即指针在数据块的中间。“interior-pointer”有多种方式出现:

  • 指针一开始是“start-pointer”,被程序有意或无意的移动到中间。

  • 可能只是巧合。

  • std::string中的char的指针。

  • 有些代码分配块内存,使用前8个去存储作为64位的数。例如sqlite3MemMalloc就是这样做的。

  • 可能是一个c++对象(具有析构函数)数组的指针,由new[]来分配内存。这种情况下,有些编译器存储一个“magic cookie”,包含数组长度存储在分配的块开头。

  • 可能是一个多重继承产生的c++对象的内部部分的指针。

在使用了启发式(heuristics)的情况下,stdstring, length64, newarray and multipleinheritance情况下的“interior-pointer”会被当成“start-pointer”对待。

考虑下面这九种情况:

Pointer chain AAA Leak Case BBB Leak Case


(1) RRR ————> BBB DR
(2) RRR —> AAA —> BBB DR IR
(3) RRR BBB DL
(4) RRR AAA —> BBB DL IL
(5) RRR ——?—–> BBB (y)DR, (n)DL
(6) RRR —> AAA -?-> BBB DR (y)IR, (n)DL
(7) RRR -?-> AAA —> BBB (y)DR, (n)DL (y)IR, (n)IL
(8) RRR -?-> AAA -?-> BBB (y)DR, (n)DL (y,y)IR, (n,y)IL, (_,n)DL
(9) RRR AAA -?-> BBB DL (y)IL, (n)DL

Pointer chain legend:
– RRR: a root set node or DR block(一个root set或者直接可达的块)
– AAA, BBB: heap blocks(堆块)
– —>: a start-pointer (头指针)
– -?->: an interior-pointer (内部指针)

Leak Case legend:
– DR: Directly reachable (直接可达)
– IR: Indirectly reachable (不直接可达)
– DL: Directly lost (直接丢失)
– IL: Indirectly lost (不直接丢失)
– (y)XY: it’s XY if the interior-pointer is a real pointer (内部指针是一个真实的指针)
– (n)XY: it’s XY if the interior-pointer is not a real pointer (内部指针不是一个真实的指针)
– (_)XY: it’s XY in either case (任意一个情况)

任意一种情况都可以被归为上述9种情况之一,Valgrind合并其中一些情况,得出4种可能

  • “Still reachable(依然可达)”。 这包含情况 1 和 2 (for the BBB blocks) 。 一个内存块的头指针的或者头指针的链被发现,程序员至少在原理上释放了这块内存在程序退出之前。这是一个非常普遍并且不算是一个问题,Valgrind默认不报告这个问题。

  • “Definitely lost(绝对丢失)”。 这包含情况3 (for the BBB blocks) 。这意味着这个数据块没有指针可达。数据块被归为丢失,因为程序员在程序结束时不能主动释放它,原因是没有指针指向这块内存。 这可能是在较早之前丢失了指向内存区域的指针。

  • “Indirectly lost(非直接丢失)”。这包含情况4和9 (for the BBB blocks)。这意味着数据块丢失不是因为没有指针指向它,而是因为所有的指向数据块的指针自己丢失了。 举例来说,如果你有一个二叉树,根节点丢失,所有的他的子节点都变成非直接丢失。因为根节点的直接丢失问题被解决,子节点的非直接丢失问就会消失。Valgrind默认不报告这个问题。

  • “Possibly lost(可能丢失)”。 这包含情况5、6、7、8 (for the BBB blocks) 。 这意味着一个或多个数据块指针被发现,但是至少一个指针是内部指针。这可能只是一个内存中的随机值,刚好指向一个数据块,所以你不需要考虑这个情况除非你知道你的代码中出现了内部指针。

下面是一个内存泄漏的总结的例子

LEAK SUMMARY:
definitely lost: 48 bytes in 3 blocks.
indirectly lost: 32 bytes in 2 blocks.
possibly lost: 96 bytes in 6 blocks.
still reachable: 64 bytes in 4 blocks.
suppressed: 0 bytes in 0 blocks.

如果开启的启发式的选项,类似于以下输出

LEAK SUMMARY:
definitely lost: 4 bytes in 1 blocks
indirectly lost: 0 bytes in 0 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 95 bytes in 6 blocks
of which reachable via heuristic:
stdstring : 56 bytes in 2 blocks
length64 : 16 bytes in 1 blocks
newarray : 7 bytes in 1 blocks
multipleinheritance: 8 bytes in 1 blocks
suppressed: 0 bytes in 0 blocks

如果 –leak-check=full 被指定, Memcheck 会给出每一个绝对丢失或可能丢失块的详细情况,包括他们在哪里被分配。它不能告诉你何时、如何、为何指向一个泄露内存块的指针丢失了;这个需要自己解决。通常,你需要保证在程序退出时,你的程序没有任何的绝对丢失或者可能丢失的内存块。

例如

8 bytes in 1 blocks are definitely lost in loss record 1 of 14
at 0x……..: malloc (vg_replace_malloc.c:…)
by 0x……..: mk (leak-tree.c:11)
by 0x……..: main (leak-tree.c:39)

88 (8 direct, 80 indirect) bytes in 1 blocks are definitely lost in loss record 13 of 14
at 0x……..: malloc (vg_replace_malloc.c:…)
by 0x……..: mk (leak-tree.c:11)
by 0x……..: main (leak-tree.c:25)

第一条信息描述了一种简单的情况,一个8byte的内存块绝对丢失了。第二种情况描述了另外一个8byte内存块绝对丢失;不同在于第二种情况会引起在另外内存块中的更多的80bytes内存非直接丢失了。loss number没有任何特殊的含义。这个loss number可以在Valgrind gdbserver中用来列出泄漏内存块的地址,或者给出更多的信息关于为何一个内存块仍然可达。

当 –leak-check=full 被指定时,选项–show-leak-kinds= 控制显示的泄漏类型。

有下面几种类型:

  • 单独指定一或多个: definite indirect possible reachable。
  • all代表所有。
  • none 代表空集合。

例如使用 –show-leak-kinds=definite,possible 来只显示绝对或者可能的内存丢失。

注意事项

在调试php时,因为php自己实现了内存管理机制,所有使用valgrind时,会检测出很多php上的内存泄露,我们可以通过export USE_ZEND_ALLOC=0来让php直接向内存申请内存,这样有助于发现问题

unable to make backup link of `./usr/bin/chattr’ before installing new version: Operation not permitted

在公司服务器上使用apt-get upgrade遇到这个问题。通过查阅资料,发现问题的关键在于,chattr需要升级但是chatter无法被删除。

使用:

lsattr /usr/bin/chattr

发现chattr的属性包括i和a,i代表immutable,不可更改,a代表append only,只能增加。这样问题就清楚了,chattr不可更改导致无法升级,至于出现这个情况的原因也不清楚。于是使用chattr更改自己的i和a属性

chattr -i /usr/bin/chattr
chattr -a /usr/bin/chattr

没有任何效果,而且提示我chattr的用法。出现这种提示的原因往往都是用错了指令,我反复确认指令都没有错。

于是在本地机器上验证chattr的属性,发现是没有i和a属性的,而且上述指令也可以正常工作。

实在没有办法,使用sftp将本地的chattr传到服务器上,命名为chattr_new,再用传上去的chattr_new更改chattr的属性

chattr_new -i /usr/bin/chattr
chattr_new -a /usr/bin/chattr

然后在执行apt-get upgrade,没有任何报错。

注:
chattr是用来防止误删操作的,即使是root用户,在chattr为文件添加了i属性后,root用户也无法删除。
lsattr则是用来查看文件的这方面的属性的。