JVM内存区域的划分,哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有的区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

  • 程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址
  • JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代
  • 方法区。方法区主要存放类的结构信息,比如静态属性和方法等等
  • 运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息

其实除了程序计数器,其他的部分都会发生 OOM

  • 堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏
  • JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的
  • 方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的

GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:

  • 引用计数法:当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。

回收垃圾的话有这么四种方法:

  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的[碎片化]。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要[两倍的内存]。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会[将存活的对象压缩],避免了内存的碎片化。
  • 分代算法:将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率

新生代对象分为三个区域:Eden 区和两个 Survivor 区。

新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法

介绍下Android应用程序启动过程

一. :Launcher通过Binder进程间通信机制通知ActivityManagerService,它要启动一个Activity;

二.:ActivityManagerService通过Binder进程间通信机制通知Launcher进入Paused状态;

三.:Launcher通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就创建一个新的进程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行;

四. :ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给ActivityManagerService,以便以后ActivityManagerService能够通过这个Binder对象和它进行通信;

五 :ActivityManagerService通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。

Android PorterDuffXfermode

PorterDuffXfermod模式的混合效果如图:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    drawSrc(canvas, mPaint);
}

private void drawDst(Canvas canvas, Paint p) {
    //画黄色圆形
    p.setColor(0xFFFFCC44);
    canvas.drawOval(new RectF(0, 0, W * 3 / 4, H * 3 / 4), p);
}

private void drawSrc(Canvas canvas, Paint p) {
    //画蓝色矩形
    p.setColor(0xFF66AAFF);
    canvas.drawRect(W / 3, H / 3, W * 19 / 20, H * 19 / 20, p);
}

代码很简单,就是在onDraw()中先画了一个黄色圆形dst,然后再画了一个蓝色矩形src。

结果显示后绘制的蓝色矩形叠加在黄色圆形上了,这是正常的混合模式。

使用PorterDuffXfermod的CLEAR模式。修改onDraw方法如下:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    //设置xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    drawSrc(canvas, mPaint);
    //还原
    mPaint.setXfermode(null);
}

在绘制蓝色矩形src时给paint设置了Xfermode为CLEAR模式;

发现绘制的蓝色矩形src结果成了白色矩形

这是为什么呢?

先看如下代码:

@Override
protected void onDraw(Canvas canvas) {
    //背景色
    canvas.drawARGB(255, 255, 156, 161);
    int sc = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
    //先绘制的是dst,后绘制的是src
    drawDst(canvas, mPaint);
    //设置xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    drawSrc(canvas, mPaint);
    //还原
    mPaint.setXfermode(null);
    canvas.restoreToCount(sc);
}

这次我们发现绘制的这个蓝色矩形src好像“消失了”。并且发现dst与src两个图形的相交处显示的是背景色。

比较这两处代码不同之处在于这次绘制图形是放在canvas.saveLayer与canvas.restoreToCount中实现的。

关于savelayout的作用查一下可知:

  1. canvas是支持图层layer渲染这种技术的,canvas默认就有一个layer,当我们平时调用canvas的各种drawXXX()方法时,其实是把所有的东西都绘制到canvas这个默认的layer上面。
  2. 我们还可以通过canvas.saveLayer()新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。
  3. 用canvas.saveLayer()方法产生的layer所有像素的ARGB值都是(0,0,0,0),即canvas.saveLayer()方法产生的layer初始时时完全透明的。
  4. canvas.saveLayer()方法会返回一个int值,用于表示layer的ID,在我们对这个新layer绘制完成后可以通过调用canvas.restoreToCount(layer)或者canvas.restore()把这个layer绘制到canvas默认的layer上去,这样就完成了一个layer的绘制工作。

分析一下原因:

  1. 我们先将画布设置背景色,此时画布的ARGB值就是背景色值。
  2. 我们通过saveLayer新建一个layer,接下来的绘制都是在这个新建的layer上进行绘制的了。
  3. 我们先绘制了一个黄色的圆形dst。
  4. 设置画笔Xfermode为CLEAR模式画蓝色矩形src,由于CLEAR模式蓝色矩形区域(包括与圆形相交部分)的ARGB值被设置为(0,0,0,0)也就是透明。
  5. 这样新建的layer上就只有一个3/4黄色圆形dst是不透明的(1/4与src叠加部分被置为透明),最后通过restoreToCount将layer绘制到canvas上,绘制的结果就是:透明部分显示canvas背景颜色,不透明部分显示黄色圆形dst部分。也就是上图这种现象。

为什么第一次会出现白色矩形呢?”这个问题也就很好回答了,因为我们一开始是在canvas的默认layer上绘制的,当我们的矩形区域src被CLEAR模式绘制后,该区域变为透明。canvas该部分就成为透明了,但由于Activity背景本身是白色的所以最终显示就为白色了。

通过上面的实验和分析可以得出,CLEAR模式可以使两种图片相交处设置为透明。但我们把API Demo那张图拿出来后一看:“不对啊,不应该是全部透明么?”这里就是那张图给我们挖的坑了。我们仔细看下官方示例的代码是如何实现的:

// create a bitmap with a circle, used for the "dst" image
static Bitmap makeDst(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0xFFFFCC44);
    c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);
    return bm;
}

// create a bitmap with a rect, used for the "src" image
static Bitmap makeSrc(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0xFF66AAFF);
    c.drawRect(w/3, h/3, w*19/20, h*19/20, p);
    return bm;
}

这是其实现代码,我们可以看出这两个方法的目的就是创建两个图形用来演示PorterDuffXfermod的不同效果。但我们仔细一看就能发现有点问题了:

  1. 创建的bitmap的宽高是w,h;但绘制的图形大小只是其中一部分(注意drawXX中的代码),这样bitmap其余部分是透明的。
  2. 这就可以知道最终得到的dst与src是两张大小一致(都为w,h)的bitmap,只是可见区域(绘制区域)导致我们误解是两个不同尺寸的bitmap。
  3. 这样的dst与src叠加就不再是一个黄色圆形与蓝色矩形叠加了,而是两张宽高一致的bitmap叠加了。
  4. 这样的src(蓝色矩形)设置为CLEAR后,其实是绘制的整个bitmap设置为CLEAR。dst与src相交区域也就是整个bitmap区域都会被设置为透明,这也就是为什么我们的实验与官方demo不一致的地方了。

原来如此!所以在开发中万不可看着API Demo图无脑来选择一种自己想要的模式,这样会使你得不到自己想要的结果时摸不着头脑。接下来我们按照图形实际大小修改后可以得出另一张图:

最后

1、关于硬件加速 

在sdkversion>=11时,需要关闭硬件加速,否则 Mode.CLEAR 、 Mode.DARKEN 、 Mode.LIGHTEN 三种模式下绘制效果不正常。

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
//View从API Level 11才加入setLayerType方法
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

2、关于saveLayer 

savelayer不是必须的,我们已经知道使用他的目的了;其实我们可以新建一个bitmap并在这上面进行绘制,最后将bitmap绘制到canvas上,其实原理是一致的。

Https相关-HostnameVerifier

[强制]在实现的HostnameVerifier子类中,需要使用verify函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。

说明:
在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接,如果回调内实现不恰当,默认接受所有域名,则有安全风险。

反例:
HostnameVerifier hnv=new HosernameVerifier(){
  @Override
  public boolean verify(String hostname,SSLSession session){
      return true;
  }
}
正例:
HostnameVerifier hnv=new HosernameVerifier(){
@Override
public boolean verify(String hostname,SSLSession session){
    if("youhostname".equals(hostname)){
        return true;
    }else{
          HostnameVerifier        hv=HttpsURLConnection.getDefaultHostnameVerifier();
         return hv.verify(hostname,session);
          }
  }
}