直接内存、java虚拟机栈、本地方法栈、程序计数器、方法执行

直接内存、java虚拟机栈、本地方法栈、程序计数器、方法执行
直接内存
概述

直接内存又叫堆外内存。它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

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

直接内存与堆内存比较
  1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
  2. 直接内存与IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
  3. 本机直接内存的分配不会受到java堆大小的限制,受到本机总内存大小限制
  4. 配置虚拟机参数时,不要忽略直接内存防止出现OutOfMemoryError异常
直接内存的实现

Java中分配堆外内存的方式有俩种:

  • 一是通过ByteBuffer.java#allocateDirect得到以一个DirectByteBuffer对象
  • 二是直接调用Unsafe.java#allocateMemory分配内存,但Unsafe只能在JDK的代码中调用,一般不会直接使用该方法分配内存。

其中DirectByteBuffer也是用Unsafe去实现内存分配的,对堆内存的分配、读写、回收都做了封装。

ByteBuffer源码
ge java.nio;
public abstract class ByteBuffer
 extends Buffer
 implements Comparable<ByteBuffer> {
/**
 * Allocates a new direct byte buffer.
 *
 * <p> The new buffer's position will be zero, its
limit will be its
 * capacity, its mark will be undefined, and each of
its elements will be
 * initialized to zero. Whether or not it has a
 * {@link #hasArray backing array} is unspecified.
 *
 * @param capacity
 * The new buffer's capacity, in bytes
 *
 * @return The new byte buffer
 *
 * @throws IllegalArgumentException
 * If the <tt>capacity</tt> is a negative
integer
 */
 public static ByteBuffer allocateDirect(int capacity) {
 return new DirectByteBuffer(capacity);
 }

ByteBuffer#allocateDirect中仅仅是创建了一个DirectByteBuffer对象,重点在DirectByteBuffer的构造方法中。

DirectByteBuffer(int cap) { // packageprivate
 //主要是调⽤ByteBuffer的构造⽅法,为字段赋值
 super(-1, 0, cap, cap);
 //如果是按⻚对齐,则还要加⼀个Page的⼤⼩;我们分析只pa为false的
情况就好了
 boolean pa = VM.isDirectMemoryPageAligned();
 int ps = Bits.pageSize();
 long size = Math.max(1L, (long)cap + (pa ? ps : 0));
 //预分配内存
 Bits.reserveMemory(size, cap);
 long base = 0;
 try {
 //分配内存
 base = unsafe.allocateMemory(size);
 } catch (OutOfMemoryError x) {
 Bits.unreserveMemory(size, cap);
 throw x;
 }
 //将分配的内存的所有值赋值为0
 unsafe.setMemory(base, size, (byte) 0);
 //为address赋值,address就是分配内存的起始地址,之后的数据读写
都是以它作为基准
 if (pa && (base % ps != 0)) {
 // Round up to page boundary
 address = base + ps - (base & (ps - 1));
 } else {
 //pa为false的情况,address==base
 address = base;
 }
 //创建⼀个Cleaner,将this和⼀个Deallocator对象传进去

 cleaner = Cleaner.create(this, new Deallocator(base,
size, cap));
 att = null; }

DirectByeBuffer构造方法中共还做了挺多事情的,总的来说分为几个步骤:

  1. 预分配内存

  2. 分配内存

  3. 将刚分配的内存空间初始化为0

  4. 创建一个cleaner对象,Cleaner对象的作用是当DirectByteBuffer对象被回收时,释放其对应的堆外内存。

    Java的堆外内存回收设计是这样的:当GC发现DirectByteBuffer对象变成垃圾时,会调用Cleaner#clean回收对应的堆外内存,一定程度上防止了内存泄漏。当然,也可以手动的调用该方法,对堆外内存进行提前回收。

Cleaner的实现
public class Cleaner extends PhantomReference<Object> {
 // ...
 private Cleaner(Object referent, Runnable thunk) {
 super(referent, dummyQueue);
 this.thunk = thunk;
 }
 public void clean() {
 if (remove(this)) {
 try {
 //thunk是⼀个Deallocator对象
 this.thunk.run();
 } catch (final Throwable var2) {
 ...
 }
 }
 }
}
 private static class Deallocator
 implements Runnable
 {
 private static Unsafe unsafe = Unsafe.getUnsafe();
 private long address;
 private long size;
 private int capacity;
 private Deallocator(long address, long size, int
capacity) {
 assert (address != 0);
 this.address = address;
 this.size = size;
 this.capacity = capacity;
 }
 public void run() {
 if (address == 0) {
 // Paranoia
 return;
 }
 unsafe.freeMemory(address);
 address = 0;
 Bits.unreserveMemory(size, capacity);
 }

Cleaner继承自PhantomReference

简单来说,就是当字段referent(也就是DirectByteBuffer对象)被回收时,会调用到Cleaner#clean方法最终会调用到Deallocator#run进行堆外内存回收

Cleaner是虚引用在JDK中的一个典型应用场景。

预分配内存源码
c void reserveMemory(long size, int cap) {
 //maxMemory代表最⼤堆外内存,也就是-
XX:MaxDirectMemorySize指定的值
 if (!memoryLimitSet && VM.isBooted()) {
 maxMemory = VM.maxDirectMemory();
 memoryLimitSet = true;
 }
 //1.如果堆外内存还有空间,则直接返回
 if (tryReserveMemory(size, cap)) {
 return;
 }
 //走到这⾥说明堆外内存剩余空间已经不⾜了
 final JavaLangRefAccess jlra =
SharedSecrets.getJavaLangRefAccess();
 //2.堆外内存进⾏回收,最终会调⽤到Cleaner#clean的⽅法。如
果⽬前没有堆外内存可以回收则跳过该循环
 while (jlra.tryHandlePendingReference()) {
 //如果空闲的内存⾜够了,则return
 if (tryReserveMemory(size, cap)) {
 return;
 }
 }
 //3.主动触发⼀次GC,⽬的是触发老年代GC
 System.gc();
 //4.重复上⾯的过程
 boolean interrupted = false;
 try {
 long sleepTime = 1;
 int sleeps = 0;
 while (true) {
 if (tryReserveMemory(size, cap)) {
 return;
 }
 if (sleeps >= MAX_SLEEPS) {
 break;
 }
 if (!jlra.tryHandlePendingReference()) {
 try {
 Thread.sleep(sleepTime);
 sleepTime <<= 1;
 sleeps++;
 } catch (InterruptedException e) {
 interrupted = true;
 }
 }
 }
 //5.超出指定的次数后,还是没有⾜够内存,则抛异常
 throw new OutOfMemoryError("Direct buffer
memory");
 } finally {
 if (interrupted) {
 // don't swallow interrupts
 Thread.currentThread().interrupt();
 }
 }
 }
 
 private static boolean tryReserveMemory(long size, int
cap) {
 //size和cap主要是page对齐的区别,这⾥我们把这两个值看作是
相等的
 long totalCap;
 //totalCapacity代表通过DirectByteBuffer分配的堆外内存
的⼤⼩
 //当已分配⼤⼩<=还剩下的堆外内存⼤⼩时,更新totalCapacity
的值返回true
 while (cap <= maxMemory - (totalCap =
totalCapacity.get())) {
     if (totalCapacity.compareAndSet(totalCap,
totalCap + cap)) {
 reservedMemory.addAndGet(size);
 count.incrementAndGet();
 return true;
 }
 }
 //堆外内存不⾜,返回false
 return false;
 }

在创建一个新的DirecByteBuffer时,会先确认有没有足够的内存,如果没有的话,会通过一些手段回收一部分堆外内存,直到可用内存大于需要分配的内存。具体步骤如下:

  1. 如果可用堆外内存足够,则直接返回
  2. 调用tryHandlePendingReference方法回收已经变成垃圾的DirectByteBuffer对象对应的堆外内存,直到可用内存足够,或目前没有垃圾DirectByteBuffer对象
  3. 触发了异常full gc,其主要目的是为了防止’冰山现象‘:一个DirectByteBuffer对象本身占用的内存很小,但是它可能引用了一块很大的堆外内存。如果DirectByteBuffer对象进入了老年代之后变成了垃圾,因为老年代GC一直没有触发,导致这块堆外内存也一直没有被回收。需要注意的是如果使用参数-XX:+DisableExplicitGC,那么System.gc();是无效的
  4. 重复1,2步骤的流程,直到可用内存大于需要分配的内存
  5. 如果超出指定次数还没有回收到足够内存,则OOM
static boolean tryHandlePending(boolean waitForNotify) {
 Reference<Object> r;
 Cleaner c;
 try {
 synchronized (lock) {
 //pending由jvm gc时设置
 if (pending != null) {
 r = pending;
 // 如果是cleaner对象,则记录下来
 c = r instanceof Cleaner ? (Cleaner) r
: null;
 // unlink 'r' from 'pending' chain
 pending = r.discovered;
 r.discovered = null;
 } else {
 // waitForNotify传入的值为false
 if (waitForNotify) {
 lock.wait();
 }
 // 如果没有待回收的Reference对象,则返回
false
 return waitForNotify;
 }
 }
 } catch (OutOfMemoryError x) {
 ...
 } catch (InterruptedException x) {
 ...
 }
 // Fast path for cleaners
 if (c != null) {
 //调⽤clean⽅法
 c.clean();
 return true;
 }
 ...
 return true; }

可以看到,tryHandlePendingReference的最终效果就是:如果有垃圾DirectByteBuffer对象,则调用对应的Cleaner#clean方法进行回收。clean方法在上面已经分析过了。

堆外内存的读写API
public ByteBuffer put(byte x) {
 unsafe.putByte(ix(nextPutIndex()), ((x)));
 return this; }
final int nextPutIndex() { 
 if (position >= limit)
 throw new BufferOverflowException();
 return position++; }
private long ix(int i) {
 return address + ((long)i << 0);
}public byte get() {
 return ((unsafe.getByte(ix(nextGetIndex()))));
}
final int nextGetIndex() { //
package-private
 if (position >= limit)
 throw new BufferUnderflowException();
 return position++; }

读写的逻辑也比较简单,address就是构造方法中分配的native内存的起始地址。Unsafe的putByte/geByte都是native方法,就是写入值到某个地址/获取某个地址的值

结论:

  1. 在数据量提升时,直接内存相比非直接内存,有很严重的性能问题
  2. 直接内存在直接的IO操作上,在频繁的读写时 会有显著的性能提升

堆内存作用链:

本地IOP -> 直接内存->堆内存->直接内存->本地IO

直接内存作用链:

本地IO->直接内存->本地IO

直接内存使用场景

有很大的数据需要存储,它的生命周期很长

  • 适合频繁的IO操作,例如网络并发场景
  • 适合长期存在或能复用的场景
    堆外内存分配回收也是有开销的,所以适合长期存在的对象
  • 适合注重稳定的场景
    堆外内存能有效避免因GC导致的暂停问题。
  • 适合见的对象的促成你
  • 因为堆外内存只能存储字节数组,所以对于复杂的DTO对象,每次存储/读取都需要序列号/反序列化
  • 适合注重IO效率的场景
    用堆外内存读写文件性能更好
文件IO
BIO

BIO的文件写FileOutputStream#write最终会调用到native层的io_util.c#writeBytes方法


writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
 jint off, jint len, jboolean append, jfieldID
fid)
{
 jint n;
 char stackBuf[BUF_SIZE];
 char *buf = NULL;
 FD fd;
 ...
 // 如果写入⻓度为0,直接返回0
 if (len == 0) {
 return;
 } else if (len > BUF_SIZE) {
 // 如果写入⻓度⼤于BUF_SIZE(8192),⽆法使⽤栈空间
buffer
 // 需要调⽤malloc在堆空间申请buffer
 buf = malloc(len);
 if (buf == NULL) {
 JNU_ThrowOutOfMemoryError(env, NULL);
 return;
 }
 } else {
 buf = stackBuf;
 }
 // 复制Java传入的byte数组数据到C空间的buffer中
 (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);
 
 if (!(*env)->ExceptionOccurred(env)) {
 off = 0;
 while (len > 0) {
 fd = GET_FD(this, fid);
 if (fd == -1) {
 JNU_ThrowIOException(env, "Stream
Closed");
 break;
 }
 //写入到⽂件,这⾥传递的数组是我们新创建的buf
 if (append == JNI_TRUE) {
 n = (jint)IO_Append(fd, buf+off, len);
 } else {
 n = (jint)IO_Write(fd, buf+off, len);
 }
 if (n == JVM_IO_ERR) {
 JNU_ThrowIOExceptionWithLastError(env,
"Write error");
 break;
 } else if (n == JVM_IO_INTR) {
 JNU_ThrowByName(env,
"java/io/InterruptedIOException", NULL);
 break;
 }
 off += n;
 len -= n;
 }
 }
}

GetByteArrayRegion其实就是对数组进行了一份拷贝,该函数的实现在jni.cpp宏定义中

//jni.cpp
JNI_ENTRY(void, \
jni_Get##Result##ArrayRegion(JNIEnv *env,
ElementType##Array array, jsize start, \
 jsize len, ElementType *buf)) \
 ...
 int sc = TypeArrayKlass::cast(src->klass())- >log2_element_size(); \
 //内存拷⻉
 memcpy((u_char*) buf, \
 (u_char*) src->Tag##_at_addr(start), \
 len << sc); \
...
 } \
JNI_END
1.底层通过write、read、pwrite,pread函数进⾏系统调⽤时,需要传入
buffer的起始地址和buffer count作为参数。如果使⽤java heap的话,我
们知道jvm中buffer往往以byte[] 的形式存在,这是⼀个特殊的对象,由于
java heap GC的存在,这⾥对象在堆中的位置往往会发⽣移动,移动后我们传
入系统函数的地址参数就不是真正的buffer地址了,这样的话⽆论读写都会发
⽣出错。⽽C Heap仅仅受Full GC的影响,相对来说地址稳定。
2.JVM规范中没有要求Java的byte[]必须是连续的内存空间,它往往受宿主语
⾔的类型约束;⽽C Heap中我们分配的虚拟地址空间是可以连续的,⽽上述的
系统调⽤要求我们使⽤连续的地址空间作为buffer。
NIO

NIO的文件写最终会调用到IOUtil#write

tatic int write(FileDescriptor fd, ByteBuffer src, long
position,
 NativeDispatcher nd, Object lock)
 throws IOException
 {
 //如果是堆外内存,则直接写
 if (src instanceof DirectBuffer)
 return writeFromNativeBuffer(fd, src,
position, nd, lock);
 // Substitute a native buffer
 int pos = src.position();
 int lim = src.limit();
 assert (pos <= lim);
 int rem = (pos <= lim ? lim - pos : 0);
 //创建⼀块堆外内存,并将数据赋值到堆外内存中去
 ByteBuffer bb =
Util.getTemporaryDirectBuffer(rem);
 try {
 bb.put(src);
 bb.flip();
 // Do not update src until we see how many
bytes were written
 src.position(pos);
 int n = writeFromNativeBuffer(fd, bb,
position, nd, lock);
 if (n > 0) {
 // now update src
 src.position(pos + n);
 }
 return n;
 } finally {
 Util.offerFirstTemporaryDirectBuffer(bb);
 }
 }
 
 /**
 * 分配⼀片堆外内存
 */
 static ByteBuffer getTemporaryDirectBuffer(int size) {
 BufferCache cache = bufferCache.get();
 ByteBuffer buf = cache.get(size);
 if (buf != null) {
 return buf;
 } else {
 // No suitable buffer in the cache so we need
to allocate a new
 // one. To avoid the cache growing then we
remove the first
 // buffer from the cache and free it.
 if (!cache.isEmpty()) {
 buf = cache.removeFirst();
 free(buf);
 }
 return ByteBuffer.allocateDirect(size);
 }
 }

NIO的文件写,对于堆内内存来说是会有额外的一次内存拷贝的。

程序计数器
示例
public class PCRegisterTest {
 public static void main(String[] args){
 int i = 100;
 int j = 200;
 int m = i + j;
 String str = "a";
 System.out.println(m);
 System.out.println(str);
 }
}
特点
  • 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。
  • 在JVM规范中,每个线程都有它的程序计数器,是线程私有的,它的生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法JVM指令地址;或者,如果是执行native方法,则是未指定值(undefined)
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来读取下一条需要执行的字节码指令。
  • 它是唯一一个在java虚拟机规范中没有被规定任何OutOfMemoryError(OOM)情况的区域。
JAVA虚拟机栈(Java方法)
如何设置栈的大小

使用参数-Xss选项来设置线程的最大栈空就,栈的大小直接决定了函数调用的最大可达深度。JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K.根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,值在3000~5000左右。

虚拟机栈存储哪些数据
栈帧是什么

栈帧(Stack Frame)是用于支持虚拟机进行方法执行的数据结果。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
当前栈帧

一个线程中方法的调用链可能会很长,所以会有很多栈帧。只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法,定义这个方法的类叫做当前类

​ 执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。

什么时候创建栈帧

调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧。

局部变量表

句柄变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。

一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。

局部变量表中的存储顺序:

  • this引用(实例对象都需要维护的一个变量,而且在局部变量表中始终处于第一个位置,也就是下标为0的位置)
  • 方法参数
  • 方法内声明的变量
存储容量

局部变量表的容量以变量槽为最新单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大slot数量)

double\long这种8字节类型的数据,都需要来个slot来存储。

其他

虚拟机通过所以定位的方法查找相应的局部变量,所以的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型)时,会连续使用俩个连续的S lot来促存储。

操作数栈

作用

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。

JVM的解释引擎是基于栈(操作数栈)的方式执行的。(另外还有一种是基于寄存器的方式)

当一个方法刚刚开始执行时,其操作栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段(成员变量)中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者。也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

存储内容

操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型栈2个栈容量。

存储容量

同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

结论

一个线程的执行过程中,需要进行来个栈的入栈出栈操作,一个是JVM栈(栈帧的出栈和入栈),一个是操作数栈(参与计算的值进行出栈和入栈)

动态链接

在一个class文件中,一个方法要调用其他方法,需要将这些方法的符合引用转化位其在内存地址中的直接引用,而符合引用存在于方法区中的运行时常量池。

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属的方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接,这些符合引用一部分会在类加载阶段或者第一次使用时就直接转化位直接引用,这类转化称为静态解析。另一部分将在每次运行期间转换为直接引用,这类转换称为动态连接。

方法返回

当一个方法开始执行时,可能有俩种方式退出该方法:

  • 正常完成出口
  • 异常完成出口

真诚完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时同那个throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者,或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。

​ 异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。

​ 无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他回复它的上层方法执行状态。

​ 方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。

​ 一般来说,方法正常退出时,调用者的PC技术值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

栈异常
虚拟机栈的深度,在编译的时候,就已经确定了

Java虚拟机规范中,对该区域规定了这俩种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常(-Xss);
  2. 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈(本地方法)

什么是本地方法

本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务。

简单来说,一个Native Method就是一个java调用非java代码的接口。一个NativeMethod是这样一个java的方法:该方法的实现由非java语言实现比如C。

在定义一个native method时,并不提供实现体(有些定义一个java interface),因为其实现体是由非java语言在外面实现的。下面给了一个示例:

public class IHaveNatives
 {
 native public void Native1( int x ) ;
 native static public long Native2() ;
 native synchronized private float Native3( Object o
) ;
 native void Native4( int[] ary ) throws Exception ;
 }

本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。

为什么使用本地方法

java使用起来非常方便,然而有些层次的认为用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

本地方法非常有用,因为它有效地扩充了jvm

事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做到任何应用层次的任务。

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。

本地方法正是这样一种交流机制;它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐细节。

java使用起来非常方便,然而有效层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

与java环境外交互:

​ 有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无线去了解java应用之外的繁琐细节。

与操作系统交互:

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的。还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority()。这个本地方法是用C实现的,并被植入JVM内部,在Windows95的平台上,这个本地方法最终将调用win32 SetPriority()API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库提供,然后被JVM调用。

package java.lang;
public class Object {
 private static native void registerNatives();
 static {
 registerNatives();
 }
 
 public final native Class<?> getClass();
 ..........
 }
 package java.lang;
public
class Thread implements Runnable {
............
 /* Some private helper methods */
 private native void setPriority0(int newPriority);
 private native void stop0(Object o);
 private native void suspend0();
 private native void resume0();
 private native void interrupt0();
 private native void setNativeName(String name);
}

现状

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过java程序驱动打印机或者java系统该管理生产设备,在企业应用中已经教师见。因为现状的异构领域间的通信很发达,比如可以使用socket通信,也可以使用web service等等。

本地方法栈的使用流程

当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。

一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。

本地方法栈的理解

本地方法栈(Native Method Stack)

java虚拟机用于管理java方法的调用,而本地方法栈用于管理本地方法的调用。

本地方法栈也是线程私用的。

本地方法是使用C语言实现的。

当某个线程调用一个本地方法时,它就进入了一个全新的并且不受虚拟机限制的世界。它和虚拟机拥有同样的权限

  • 本地方法可以通过本地接口来访问虚拟机内部的运行时数据区。

  • 它甚至可以直接使用本地处理器中的寄存器

  • 直接从本地内存的堆中分配任意数量的内存

    运行本地方法栈的大小是固定的或者可动态扩展的内存大小。

  • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量Java虚拟机将会抛出一个StackOverflowError异常

  • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那java虚拟机将会抛出一个OutOfMemoryError异常。

    并不是所以的JVM都支持本地方法。因为Java虚拟机贵伐并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不支持native方法,也可以无需实现本地方法栈。

在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

方法执行

字节码指令集(字典)

Java虚拟机的指令由一个字节长度的

  • 代表着某种特定操作含义的数组(称为操作码,Opcode)
  • 跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

Opcode+操作数

  • iconst_()操作码
  • bipush 10 操作码+操作数

基本数据类型

  1. 除了long和double类型外,每个变量都占局部变量区中的一个变量槽(slot),而long及double会占用俩个连续的变量槽。
  2. 大多数对于double,byte,short和char类型数据的操作,都使用相应的int类型作为运算类型。

加载和存储指令

  1. 将一个【局部变量表】加载到【操作数栈】:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、
dload、dload_<n>、aload、aload_<n>

2.将一个数值从【操作数栈】存储到【局部变量表】:

istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n >、dstore、dstore_<n>、astore、astore_<n>

3.将一个【常量】加载到操作数栈:

bipush、sipush、
ldc、ldc_w、ldc2_w、
aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_< f>、dconst_<d>

4.补充局部变量表的访问索引的指令:

wide_<n>:_0、_1、_2、_3,

存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。

cosnt系列(小数值)

该系列命令主要复制把【简单的数值类型】送到【操作数栈栈顶】。该系列命令不带参数。主要只把简单的数值类型送到栈顶时,才使用如下的命令。

-1,0,2,3,4,5(分别采用iconst_m1,iconst_0,iconst_2,iconst_3,iconst4,iconst_5)送到栈顶

对于int型,其他的数值请使用push系列命令(比如bipush)。

push系列(中数值)

该系列命令负责把一个【整形数字(长度比较小)】送到【操作数栈栈顶】。该系列命令【有一个参数】,用于指定要送到栈顶的数字。

注意:该系列命令只能操作一定范围内的整形数值,超出该范围的使用将使用【ldc命令】系列。

ldc系列(大数字或字符串常量)

该系列命令负责把【长度较长的数值常量】或【String常量值】从【常量池中】推送至【操作数栈栈顶】。该命令后面需要给一个表示常量在常量池中位置(编号)的参数。

哪些常量是放在常量池呢?比如:

final static int id=32768;
final static float double = 6.5

对于const系列命令和push系列命令操作范围之外的数值类型常量,都放在常量池中。

另外,所有·不是通过new创建的String都是放在常量池中的。

laod系列
load系列A

该系列命令负责把【本地变量表中的值】送到操作数栈栈顶。这里的本地变量不仅可以是数值类型,还可以是引用类型。

  • 对于前四个本地变量可以采用iload_0,iload_1,iload_2,iload_3(它们分别表示第0,1,2,3个整形变量)这种不带参数的简化命令形式。
  • 对于第4以上的本地变量将使用iload命令这种形式,在它后面给一参数,以表示是堆第几个(从0开始)本类型的本地变量进行操作。对本地变量所进行的编号,是对所有类型的本地变量进行的(并不安装类型分类)。
  • 对于非静态函数(虚方法,实例方法),第一变量是this,即其对应的操作是aload_0.
  • 还有函数传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
load系列B

该系列命令负责把数值的某项送到栈顶。该命令根据栈里内容来确定对哪个数值的哪项进行操作。

比如,如果有成员变量:

final String name[] = {"robin","hb"};

那么这句话:

String str = names[0];

对应的指令为

17: aload_0 //将this引⽤推送⾄栈顶,即压入栈。
 18: getfield #5; //Field names:[Ljava/lang/String;
 //将栈顶的指定的对象的第5个实例域(Field)的值(这个值可能是
引⽤,这⾥就是引⽤)压入栈顶
 21: iconst_0 //数组的索引值(下标)推⾄栈顶,即压入栈
 22: aaload //根据栈⾥内容来把name数组的第⼀项的值推⾄栈顶
 23: astore 5 //把栈顶的值存到str变量⾥。因为str在我的程序中
是其所在非静态函数的第5个变量(从0开始计数),
指令码 助记符 说明
0x2e iaload 将int型数组指定索引的
值推送⾄栈顶
0x2f laload 将long型数组指定索引
的值推送⾄栈顶
0x30 faload 将float型数组指定索引
的值推送⾄栈顶
0x31 daload 将double型数组指定索
引的值推送⾄栈顶
0x32 aaload 将引⽤型数组指定索引的
值推送⾄栈顶
0x33 baload 将boolean或byte型数
组指定索引的值推送⾄栈顶
0x34 caload 将char型数组指定索引
的值推送⾄栈顶
0x35 saload 将short型数组指定索引
的值推送⾄栈顶
store

系列

store系列A

该系列命令负责把【操作数栈栈顶的值】存入【本地变量表】。这里的本地变量不仅可以是数值类型,还可以是引用类型。

  • 如果是把栈顶的值存入到前四个本地变量的话,采用的是istore_0,istore_1,istore_2,istore_3(它们分别表示第0,1,2,3个本地整形变量)这种不带参数的简化命令形式。
  • 如果是把栈顶的值存入到第四个以上本地变量的话,将使用istore命令这种形式,在它后面给一个参数,以表示是把栈顶的值存入到第几个(从0开始)本地变量中。对本地变量所进行的编号,是对所以类型的本地变量进行的(并不按照类型分类)。
  • 对于非静态函数,第一个变量是this,它是只读的。
  • 还有函数传入参数也算本地变量,在进行编号时,它是先于函数体的本地变量的。
store系列B

该系列命令负责把栈顶项的值存到数组里。该命令根据栈里内容来确定对哪个数组的哪项进行操作。

比如,如下代码:

int moneys[] = new int[5];
moneys[1] = 100;

其对应的指令为

 49: iconst_5
 50: newarray int
 52: astore 11
 54: aload 11
 56: iconst_1
 57: bipush 100
 59: iastore
 60: lload 6 //因为str在我的程序中是其所非静态在函数的第6
个变量(从0开始计数)
指令码 助记符 说明
0x4f iastore 将栈顶int型数值存入指定数组
的指定索引位置
0x50 lastore 将栈顶long型数值存入指定数组
的指定索引位置
0x51 fastore 将栈顶float型数值存入指定数
组的指定索引位置
0x52 dastore 将栈顶double型数值存入指定数
组的指定索引位置
0x53 aastore 将栈顶引⽤型数值存入指定数组的
指定索引位置
0x54 bastore 将栈顶boolean或byte型数值存
入指定数组的指定索引位置
0x55 castore 将栈顶char型数值存入指定数组的
指定索引位置
0x56 sastore 将栈顶short型数值存入指定数组
的指定索引位置
pop系列

该系列命令似乎只是简单对栈顶进行操作

栈顶元素数字操作及移位操作系列

该系列命令用于对栈顶元素行数学操作,和对数组进行移位操作。移位操作的数据操作数和要移位的数都是从栈里取得。

运算指令

  1. 运算或算术指令用于对俩个操作数栈上的值进行某种特定运算,并把结构重新存入到操作栈顶。
  2. 算术指令分为俩种:整型运算的指令和浮点型运算的指令。
  3. 无论哪种算术指令,都使用java虚拟机的数据类型,由于没有直接支持byte,short,char和boolean类型的算术指令,使用操作int类型的指令代替。
加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量⾃增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
类型转换指令
  1. 类型转换指令可以将俩种不同的数值类型进行相互转换。

  2. 这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理字节码指令集中数据类型相关指令方法与数据类型一一对应的问题。

    宽化类型转换

    int类型到long,float或者double类型。

    long类型到float,double类型。

    float类型到double类型

    i2l,f2b,12f,12d,f2d.
    
    窄化类型
    i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
    
对象创建与访问指令

创建类实例的指令:new.

创建数组的指令:newarray,anewarray,multianewarray.

访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield,putfield,getstatic,putstatic.

把一个数组元素加载到操作数栈的指令:baload,caload,saload,iaload,laload,faload,daload,aaload.

将一个操作数栈的值存储到数组元素中的指令:bastore,castore,sastore,iastore,fastore,dastore,aastore.

取数组长度的指令:arraylength.

检查类实例类型的指令:instanceof,checkcast.

操作数栈管理指令

直接操作操作数栈的指令:

将操作数栈的栈顶一个或俩个元素出栈:pop,pop2

复制栈顶一个或俩个数组并将复制值或双份的复制值重新压入栈顶:dup,dup2,dub_x1,dup2_x1,dup_x2,dup2_x2.

将栈最顶端的俩个数值互换:swap.

控制转移指令
  1. 控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。
  2. 从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。

条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeg,if_icmpne,if_icmplt,if_icmpat,if_icmple,if_icmpge,if_acmpeg和if_acmpne.

复合条件分支:tableswitch,lookupswitch.

无条件分支:goto,goto-w,jsr,jsr_w,ret.

在java虚拟机中有专门的指令集用来处理int和reverence类型的条件分支比较操作,为了可以无需明显标识一个实体值是否为null,也有专门的指令用来检测null值。

JVM程序执行流程
执行流程图

Java编译成字节码,动态编译和解释为机器码的过程分析:

  • java编译环境、
    • Java源代码(java文件)->Java编译器->Java字节码(class文件)->字节码本地或网络-》类加载器字节码的验证(-》Java类库)=>java解释器和即使编译器-》运行期系统-》操作系统-》硬件

在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器,下文统称JIT编译器。

由于Java虚拟机规范并没有具体的约束规则区现在即时编译器应该如何实现,所以这部分功能完全是与虚拟机具体实现相关的内容,如无特殊说明,我们提到的编译器,即时编译器都素指Hotspot虚拟机内的即使编译器,虚拟机也是特指HotSpot虚拟机。

我们的JIT是属于动态编译方式的,动态编译指的是在运行时进行编译,与之相对应的是事前编译,也叫静态编译。

JIT编译侠义来说是当某段代码即将第一次被执行时进行编译,因而叫即时编译。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要主要广义与狭义的JIT编译所指的区别。

热点代码

程序中的代码只有是热点代码时,才会编译为本地代码,那么什么是热点代码呢?

运行过程中会被即使编译器编译的热点代码有俩类:

  1. 被多次调用的方法
  2. 被多次执行的循环体。

俩种情况,编译器都是以整个方法作为编译对象。这种编译方法因为编译发生在方法执行过程之中,因此形象的称之为栈上替换,即方法栈还在栈上,方法就被替换了。

热点检测方式

要知道方法或一段代码是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。

目前主要的热点探测方式有一些俩种:

  • 基于采样的热点探测
    采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这个方法就是“热点方法”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。

  • 基于计数器的热点探测—采用这种
    采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值,就任认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法调用关系,但是它的统计结构相对更加精确严谨。

在HotSpot虚拟机中使用的是第二种------基于计数器的热点探测方法,因此它为每个方法准备了俩个计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这俩个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译.

方法调用计数器

这个计数器用于统计方法被调用的次数。

在JVM client模式下的阈值是1500次,Server是10000次,可以通过虚拟机参数:-XX:CompileThreshold设置。但是JVM还存在热度衰减,时间段内调用方法的次数较少,计数器就减小。

  • Java方法入口
    • 是否存在已编译版本
      • 是-》执行编译后的本地代码版本
      • 否-》方法调用计数器值加1-》俩计数器值之和是否超过阈值
        • 是->向编译器提交编译请求->以解释方式执行方法->java方法返回
        • 否->以解释方式执行方法->Java方法返回
回边计数器

它的作用就是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”

解释器方法执行
public class Math{
 public static void main(String[] args){
 int a = 1 ;
 int b = 2;
 int c = (a+b)*10;
 }
}

开始执行方法之前,PC寄存器存储的指针是第一条指令的地址,局部变量区和操作栈都没有数据。从第一条到第4条指令分别将a,b俩个本地变量赋值,对应到局部变量区就是1和2分别存储常数1和2

第5条和第6条指令分别是将俩个局部变量入栈,然后相加

JIT使用
为何HotSpot需要使用解释器和编译器并存的架构?

尽管并不是所以的Java虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机(如HotSpot),都同时包含解释器和编译器。

解释器与编译器特点

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行.在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码指挥,可以获取更高的执行效率。
  • 当程序运行环境中 内存资源限制较大,可以使用解释器执行节约内存,反之可以使用编译执行来提升效率
编译的时间开销

解释器的执行,抽象的看是这样的:

输入的代码->>[解释器解释执行]->>执行结果

而要JIT编译然后在执行的化,抽象的看则是:

输入的代码->>[编译器 编译]->>编译后的代码->>[执行]->>执行结果

说JIT比解释看,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作块。JIT编译再怎么快,至少也比解释执行略慢一些,而要得到最后的执行结果还得再技工一个“执行编译后的代码”的过程,所以,对“只执行一次”的代码而言,解释执行其实比JIT编译执行要快。

怎么算是“只执行一次的代码”呢?

  1. 只被调用一次,例如类的构造器
  2. 没有循环

对只执行一次的代码做JIT编译再执行,可以说是得不偿失。

对只执行少量次数的代码,JIT编译代理的执行速度的提升也未必能抵消掉最初编译带来的开销。

只有对频繁执行的代码,JIT编译才能保证有正面的收益

编译的空间开销

对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10x是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著证据代码所占空间,导致“代码爆炸”。

这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。

为何要实现俩个不同的即时编译器

HotSpot虚拟机中内置了来个即时编译器:Client Complier和Server Complier,简称C1,C2编译器,分别用再客户端和服务端。

目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工资。程序使用哪个编译器,取决于虚拟机运行模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行再Client模式或Server模式。

用Client Complier获取更高的百衲衣速度,用Server Complier来获取更好的编译质量。为什么提供多个即时编译器与为什么提供多个垃圾收集器类似,都是为了使用不同的应用场景。

如何编译为本地代码?

ServerCompiler和Client Compiler俩个编译器的编译过程是不一样的。

对Client Compiler来说,它是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时比较长的全局优化手段。

而ServerCompiler则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化的高级编译器。

JIT优化

HotSpot虚拟机使用了很多种优化技术,这里只简单介绍其中的几种。

公共子表达式的消除

公共表达式消除是一个普遍应用于各种编译器的经典优化技术,他的含义是:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所以变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对他进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。

如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除

如果这种优化范围涵盖多个基本块,那就称为全局公共子表达式消除

int d = (c*b)*12+a+(a+b*c);

当这点代码进入到虚拟机即时编译器后,他将进行如下优化:编译器检查到cb与bc是一样的表达式,而且在计算器间b与c的值是不变的。因此,这条表达式就可能被视为:

int d = E * 12+a+(a+E);

这时,编译器还可能(取决于哪种虚拟机的编译器以及编译器以及具体的上下文而定)进行另外一种优化:代数化简,把表达式变为:

int d = E*13+a*2;

表达式进行变换之后,再计算起来就可以节省一些时间了。

方法内联

再使用JIT进行即时编译时,将方法调用直接使用方法体中的代码进行替换,这就是方法内联,减少了方法调用过程中压栈与入栈的开销。同时为之后的一些优化收到提供条件。如果JVM检测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身。

比如说下面这个:

 private int add4(int x1, int x2, int x3, int x4) {
 return add2(x1, x2) + add2(x3, x4);
 }
 private int add2(int x1, int x2) {
 return x1 + x2;
 }

可以肯定的是运行一段时间后JVM把add2方法去掉,并把你的代码翻译成

private int add4(int x1, int x2, int x3, int x4) {
 return x1 + x2 + x3 + x4;
 }
方法逃逸分析

逃逸分析是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象再方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

逃逸分析包括:

  • 全局变量赋值逃逸
  • 方法返回值逃逸
  • 实例引用发生逃逸
  • 线程逃逸:赋值给类变量或可以再其他线程中访问的实例变量
blic class EscapeAnalysis {
 //全局变量
 public static Object object;
 
 //public Object object;
 
 public void globalVariableEscape(){//全局变量赋值逃逸 
 object = new Object(); 
 } 
 
 public Object methodEscape(){ //⽅法返回值逃逸
 return new Object();
 }
 
 public void instancePassEscape(){ //实例引⽤发⽣逃逸
 EscapeAnalysis escapeAnalysis = null;
 this.speak(escapeAnalysis);
 //下⾯就可以获取escapeAnalysis的引⽤
 }
 
 public void speak(EscapeAnalysis escapeAnalysis){
 escapeAnalysis = new Object();
 System.out.println("Escape Hello");
 }
}

使用方法逃逸的案例进行分析:

public static StringBuffer craeteStringBuffer(String s1,
String s2) {
 StringBuffer sb = new StringBuffer();
 sb.append(s1);
 sb.append(s2);
 return sb; }

StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是再方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以再其他线程中访问的实例变量称为线程逃逸。

如果想要上述代码StringBuffer sb不逃出去,可以这样写

public static String createStringBuffer(String s1, String
s2) {
 StringBuffer sb = new StringBuffer();
 sb.append(s1);
 sb.append(s2);
 return sb.toString();
}

不能直接返回StringBuffer,那么StringBuffer将不会逃逸出方法。

使用逃逸分析,编译器可以对代码做如下优化:

⼀、同步省略。如果⼀个对象被发现只能从⼀个线程被访问到,那么对于这个对
象的操作可以不考虑同步。
⼆、将堆分配转化为栈分配。如果⼀个对象在⼦程序中被分配,要使指向该对象
的指针永远不会逃逸,对象可能是栈分配的候选,⽽不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为⼀个连续的内存结构存在也
可以被访问到,那么对象的部分(或全部)可以不存储在内存,⽽是存储在CPU
寄存器中。

再Java代码运行时,通过JVM参数可指定是否开启逃逸分析

-XX:+DoEscapeAnalysis :表示开启逃逸分析
-XX:-DoEscapeAnalysis  :表示关闭逃逸分析

从jdk1.7开始以及默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalywsis

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值