深入探究 JVM | Safepoint 及 GC 的触发条件

最近看JVM看得火热。。在这里我们来总结一下HotSpot JVM里GC Safepoint以及触发GC的条件相关的一些知识。

GC Safepoint

如果要触发一次GC,那么JVM中所有Java线程都必须到达GC Safepoint。

JVM只会在特定位置放置safepoint,比如:

  • 内存分配的地方(allocation,即new一个新对象的时候)
  • 长时间执行区块结束的时刻(如方法调用,循环跳转等)

之所以只在特定的位置放置safepoint,是因为OopMap要占用空间,如果设太多safepoint那么占用空间会太大;再者,safepoint会影响优化,如果某个无用的值处设置了safepoint,那么JIT就无法优化掉这些无用变量,这会影响性能。

HotSpot JVM在通过JIT编译时,会在所有方法返回之前以及循环跳转、异常跳转之前放置Safepoint,并且在每个Safepoint都生成一些信息存储哪些地方是引用(OopMap),以便JVM能找到需要的引用。

那么如何确保GC时所有线程都到达GC Safepoint呢?有两种方法:抢占式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

抢占式中断不需要线程的执行代码去主动配合,当触发GC时,JVM会中断所有线程,然后依次检查每个线程中断的位置是否为Safepoint,如果不是则恢复线程,让它执行至Safepoint再进行终端。

大部分JVM实现(如HotSpot JVM)都是采用主动式中断,即GC需要中断线程的时候,它仅仅简单地设个标志,执行线程会主动轮询这个标志位,如果标志位就绪的话就自行中断。Polling Point与Safepoint是重合的。主动式中断的思想是一种hand-shacking protocol的思想。

通过对Safepoint的研究,我们了解到了GC触发的时刻主要是在new一个新的对象或者在循环跳转或方法返回之前

【扩展】Safepoint有好几种,比如还有deoptimization safepoint之类的,作用不同。

Safe-Region

只有GC Safepoint是不足的,因为我们发现,有一种情况,线程无法响应JVM的中断请求,也无法去轮询标志位:

  • 线程处于阻塞或等待状态

对于这种情况,引入了safe-region的概念。

Safe-Region是指在代码片段中,引用关系不会发生变化,因此GC可以随心所欲地在任何地方执行。在线程执行到Safe Region里面的代码时,首先标识自己已经进入了Safe Region,那样当这段时间里JVM要发起GC,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

另外,当一个线程在执行native方法时,由于此时该线程在执行JVM管理之外的代码,不能对JVM的执行状态做任何修改,因而JVM要进入safepoint不需要关心它。所以也可以把正在执行native函数的线程看作“已经进入了safepoint”,或者把这种情况叫做“在safe-region里”。


参考资料

深入探究 JVM | 初探 GC 算法

GC算法的思想主要有三种:

  • Mark-Sweep
  • Mark-Compact
  • Copying

另外,当前JVM的GC一般都是分代收集,几种垃圾回收算法进行组合。

分代收集

根据分代收集的模型,一般将内存区域分为新生代(Young Generation)老年代(Old Generation)

新生代对应那些新产生的,存活时间较短的对象。如果一个对象在新生代内经历了一定次数(默认15)的收集后,它就会晋升至老年代(大对象也可以直接进入老年代,可以调参数)。一般会把新生代划分为Eden区和Suvivor区,在HotSpot JVM中E:2S=8:2。后面会说到,新生代一般使用基于复制的GC算法。新生代对应Minor GC。

老年代对应那些存活时间较长,容量较大的对象。老年代GC对应Full GC,此时需要STW。

JDK1.8之前还存在永久代(PermGen),它用于存放类的元数据和常量,这里偶尔也会发生GC(回收无用的类和常量等等的)。由于永久代经常会OOM,JDK1.8移除了永久代,用Metaspace代替PermGen。具体可以看我之前总结的深入探究JVM | 探秘Metaspace

基于标记-清理的GC

基于标记-清理(Mark-Sweep)的GC是比较基础的一种实现,它的思想比较简单,首先根据可达性分析对不可达对象进行标记,标记完成后统一清理这些对象。它的缺点有两个:

  • 标记和清理的效率都不算高
  • 会产生大量的内存碎片,如果这时候有大对象需要连续的内存空间进行分配,很可能会因为没有足够的连续内存空间而又触发一次GC

基于Mark-Sweep的GC多用于老年代。

基于标记-压缩的GC

基于标记-压缩(Mark-Compact)的GC可以解决内存碎片的问题。它的思想是,在标记好待回收对象后,将存活的对象移至一端(reallocate)然后对剩余的部分进行回收。这个过程需要进行remapping,即修复线程与对象之间的引用映射关系。

基于Mark-Compact的GC多用于老年代。

基于复制的GC

基于复制(Copy)的GC比较高效,它的思路是,将内存容量划分为相同的两份,每次只用一块。当这一块内存用完了,就把还存活的对象移到另一块内存,然后对这一块内存(整个半区)进行清理操作。这样内存分配时也就不用考虑内存碎片了,只需要移动指针,按顺序分配即可。但是这种算法是拿空间换时间,而且一下子就是50%的内存空间,一般受不了。并且这种算法需要频繁GC。而新生代的对象一般是存活时间较短的对象,GC频率较高,占内存较少,因此新生代一般都采用基于复制的GC。

HotSpot JVM将新生代划分为一个Eden区和两个Survivor区,默认比例为8:2,其中对象可使用1E+1S,留出空闲的1S。每次进行GC的时候收集器就会将存活对象移至那个空闲S区,然后将其余的部分进行回收,这样默认空间利用率可达90%。当然也有很多时候一个S区无法容纳所有的存货对象,那么某些对象就需要通过分配担保机制(Handle Promotion)直接进入老年代。

当前商用实现

这是现有的商用GC对应的算法:


参考资料

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明 著
  • Garbage Collection Understanding Java, Azul

Java 中的几种引用 | 弱引用、软引用、虚引用

Java中存在四种引用:强引用、软引用、弱引用、虚引用,这里来分别分析一下。

StrongReference

StrongReference(强引用)是最普通的引用类型,只要强引用存在,GC就不会进行垃圾回收。

SoftReference

SoftReference(软引用)用来描述一些有用但是非必需的对象。如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM就会把这个软引用加入到与之关联的引用队列中。软引用可用来实现内存敏感的高速缓存。

WeakReference

WeakReference(弱引用)是一种生命周期比软引用更短的引用。当GC扫描启动时,只要扫描到只具有弱引用的对象,无论内存是否够用都会执行GC,但由于GC线程优先级很低,因此并不一定能迅速发现这些弱引用对象。弱引用也可以和一个引用队列联合使用。
WeakReference在Android中用的挺多。

PhantomReference

PhantomReference(虚引用)不同于其余三种引用,虚引用不会影响对象的生命周期,也无法通过虚引用获得对象的一个实例;如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,它必须和引用队列联合使用。

【2015-7-16补充】逛InfoQ的时候又发现了一种FinalReference。。用的不多就不总结了,文章:JVM源码分析之FinalReference完全解读

深入探究 JVM | 初探 GC - 引用计数 VS 可达分析

主流的垃圾回收主要分两大类:引用计数和可达性分析。

引用计数法

引用计数的实现很简单,它的思想就是给一个对象增加一个引用计数器,每当一个新的对象引用它时就给计数器加1,不引用了就减1,当计数器为0时即可认为对象无用了,可进行回收。

这个思想很简单,而且很多语言的底层(如Swift,Python)都是基于引用计数法进行回收的。但是引用计数法有一个很大的缺陷:它无法解决循环引用的问题,比如A引用B,B引用A,这样两对象的引用计数器永远不为0,两对象不能被回收,从而造成内存不能被及时清理。

JVM没有使用引用计数法,而是使用了可达性分析来进行GC。

题外话:C++ 11的智能指针就是基于引用计数的,它提供了weak_ptr来解决循环引用的问题。(类似于Java中的WeakReference,作用类似)

可达性分析

可达性分析是基于图论的分析方法,它会找一组对象作为GC Root(根结点),并从根结点进行遍历,遍历结束后如果发现某个对象是不可达的(即从GC Root到此对象没有路径),那么它就会被标记为不可达对象,等待GC。比如,假设下图中obj1位GC Root,那么obj5和obj6就是不可达的:

哪些对象可以作为GC Root

能作为GC Root的对象必定为可以存活的对象,比如全局性的引用(静态变量和常量)以及某些方法的局部变量(栈帧中的本地变量表)。

以下对象通常可以作为GC Root:

  • 存活的线程
  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 方法区中的类静态属性以及常量引用的对象
  • 本地方法栈中JNI引用的局部变量以及全局变量

参考资料

深入探究 JVM | 探秘 Metaspace

Java 8 彻底将永久代 (PermGen) 移除出了 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Metaspace。这一篇文章我们来总结一下Metaspace(元空间)的特性。如有错误,敬请指出,谢谢~

引言:永久代为什么被移出HotSpot JVM了?

在 HotSpot JVM 中,永久代中用于存放类和方法的元数据以及常量池,比如ClassMethod。每当一个类初次被加载的时候,它的元数据都会放到永久代中。

永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen ,为此我们不得不对虚拟机做调优。

那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?我总结了两个主要原因(详见:JEP 122: Remove the Permanent Generation):

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量移至 Java Heap

探秘元空间

由于 Metaspace 的资料比较少,这里主要是依据Oracle官方的Java虚拟机规范及Oracle Blog里的几篇文章来总结的。

首先,Metaspace(元空间)是哪一块区域?官方的解释是:

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

也就是说,JDK 8 开始把类的元数据放到本地堆内存(native heap)中,这一块区域就叫 Metaspace,中文名叫元空间。

优点

使用本地内存有什么好处呢?最直接的表现就是OOM问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大(貌似容量还与操作系统的虚拟内存有关?这里不太清楚),这解决了空间不足的问题。不过,让 Metaspace 变得无限大显然是不现实的,因此我们也要限制 Metaspace 的大小:使用 -XX:MaxMetaspaceSize 参数来指定 Metaspace 区域的大小。JVM 默认在运行时根据需要动态地设置 MaxMetaspaceSize 的大小。

除此之外,它还有以下优点:

  • Take advantage of Java Language Specification property : Classes and associated metadata lifetimes match class loader’s
  • Linear allocation only
  • No individual reclamation (except for RedefineClasses and class loading failure)
  • No GC scan or compaction
  • No relocation for metaspace objects

GC

如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。根据JDK 8的特性,G1和CMS都会很好地收集Metaspace区(一般都伴随着Full GC)。

为了减少垃圾回收的频率及时间,控制吞吐量,对Metaspace进行适当的监控和调优是非常有必要的。如果在Metaspace区发生了频繁的Full GC,那么可能表示存在内存泄露或Metaspace区的空间太小了。

新增的 JVM 参数

  • -XX:MetaspaceSize 是分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark ),此值为估计值。MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
  • -XX:MaxMetaspaceSize 是分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
  • -XX:MinMetaspaceFreeRatio 表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
  • -XX:MaxMetaspaceFreeRatio 表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最大比例,不够就会导致垃圾回收。

监控与调优(待补充)

VisualVMjstatjstack 可以监测 Metaspace 的动态,后续将更新这里。


参考资料

深入解析Java反射(2) - invoke方法

上篇文章中回顾了一下Java反射相关的基础内容。这一节我们来深入研究Method类中的invoke方法,探寻它的奥秘。
注:本篇文章的所有源码都基于OpenJDK 1.8。


引入

即使没有学过反射,大家也一定会见过invoke方法。因为很多方法调用都是靠invoke方法,所以很多异常的抛出都会定位到invoke方法,比如下面的情形大家会很熟悉:

1
2
3
4
5
6
java.lang.NullPointerException
at ......
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)

大家在看到异常抛出时,除了想要排除Bug,是不是同时也对这个神秘的invoke乃至invoke0方法有一些好奇呢?我们下面就来揭开它神秘的面纱,探寻底层的机制。

浅析invoke过程

上一篇文章我们讲过,invoke方法用来在运行时动态地调用某个实例的方法。它的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

我们根据invoke方法的实现,将其分为以下几步:

1、权限检查

invoke方法会首先检查AccessibleObject的override属性的值。AccessibleObject 类是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。
override的值默认是false,表示需要权限调用规则,调用方法时需要检查权限;我们也可以用setAccessible方法设置为true,若override的值为true,表示忽略权限规则,调用方法时无需检查权限(也就是说可以调用任意的private方法,违反了封装)。
如果override属性为默认值false,则进行进一步的权限检查:
(1)首先用Reflection.quickCheckMemberAccess(clazz, modifiers)方法检查方法是否为public,如果是的话跳出本步;如果不是public方法,那么用Reflection.getCallerClass()方法获取调用这个方法的Class对象,这是一个native方法:

1
2
@CallerSensitive
public static native Class<?> getCallerClass();

在OpenJDK的源码中找到此方法的JNI入口(Reflection.c):

1
2
3
4
5
JNIEXPORT jclass JNICALL Java_sun_reflect_Reflection_getCallerClass__
(JNIEnv *env, jclass unused)
{
return JVM_GetCallerClass(env, JVM_CALLER_DEPTH);
}

其中JVM_GetCallerClass的源码如下,有兴趣的可以研究一下(位于jvm.cpp):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
JVM_ENTRY(jclass, JVM_GetCallerClass(JNIEnv* env, int depth))
JVMWrapper("JVM_GetCallerClass");
// Pre-JDK 8 and early builds of JDK 8 don't have a CallerSensitive annotation; or
// sun.reflect.Reflection.getCallerClass with a depth parameter is provided
// temporarily for existing code to use until a replacement API is defined.
if (SystemDictionary::reflect_CallerSensitive_klass() == NULL || depth != JVM_CALLER_DEPTH) {
Klass* k = thread->security_get_caller_class(depth);
return (k == NULL) ? NULL : (jclass) JNIHandles::make_local(env, k->java_mirror());
}
// Getting the class of the caller frame.
//
// The call stack at this point looks something like this:
//
// [0] [ @CallerSensitive public sun.reflect.Reflection.getCallerClass ]
// [1] [ @CallerSensitive API.method ]
// [.] [ (skipped intermediate frames) ]
// [n] [ caller ]
vframeStream vfst(thread);
// Cf. LibraryCallKit::inline_native_Reflection_getCallerClass
for (int n = 0; !vfst.at_end(); vfst.security_next(), n++) {
Method* m = vfst.method();
assert(m != NULL, "sanity");
switch (n) {
case 0:
// This must only be called from Reflection.getCallerClass
if (m->intrinsic_id() != vmIntrinsics::_getCallerClass) {
THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), "JVM_GetCallerClass must only be called from Reflection.getCallerClass");
}
// fall-through
case 1:
// Frame 0 and 1 must be caller sensitive.
if (!m->caller_sensitive()) {
THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), err_msg("CallerSensitive annotation expected at frame %d", n));
}
break;
default:
if (!m->is_ignored_by_security_stack_walk()) {
// We have reached the desired frame; return the holder class.
return (jclass) JNIHandles::make_local(env, m->method_holder()->java_mirror());
}
break;
}
}
return NULL;
JVM_END

获取了这个Class对象caller后用checkAccess方法做一次快速的权限校验,其实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
volatile Object securityCheckCache;
void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
throws IllegalAccessException
{
if (caller == clazz) { // 快速校验
return; // 权限通过校验
}
Object cache = securityCheckCache; // read volatile
Class<?> targetClass = clazz;
if (obj != null
&& Modifier.isProtected(modifiers)
&& ((targetClass = obj.getClass()) != clazz)) {
// Must match a 2-list of { caller, targetClass }.
if (cache instanceof Class[]) {
Class<?>[] cache2 = (Class<?>[]) cache;
if (cache2[1] == targetClass &&
cache2[0] == caller) {
return; // ACCESS IS OK
}
// (Test cache[1] first since range check for [1]
// subsumes range check for [0].)
}
} else if (cache == caller) {
// Non-protected case (or obj.class == this.clazz).
return; // ACCESS IS OK
}
// If no return, fall through to the slow path.
slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
}

首先先执行一次快速校验,一旦调用方法的Class正确则权限检查通过。
若未通过,则创建一个缓存,中间再进行一堆检查(比如检验是否为protected属性)。
如果上面的所有权限检查都未通过,那么将执行更详细的检查,其实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Keep all this slow stuff out of line:
void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,
Class<?> targetClass)
throws IllegalAccessException
{
Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
// Success: Update the cache.
Object cache = ((targetClass == clazz)
? caller
: new Class<?>[] { caller, targetClass });
// Note: The two cache elements are not volatile,
// but they are effectively final. The Java memory model
// guarantees that the initializing stores for the cache
// elements will occur before the volatile write.
securityCheckCache = cache; // write volatile
}

大体意思就是,用Reflection.ensureMemberAccess方法继续检查权限,若检查通过就更新缓存,这样下一次同一个类调用同一个方法时就不用执行权限检查了,这是一种简单的缓存机制。由于JMM的happens-before规则能够保证缓存初始化能够在写缓存之前发生,因此两个cache不需要声明为volatile。
到这里,前期的权限检查工作就结束了。如果没有通过检查则会抛出异常,如果通过了检查则会到下一步。

2、调用MethodAccessor的invoke方法

Method.invoke()实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor来处理。
首先要了解Method对象的基本构成,每个Java方法有且只有一个Method对象作为root,它相当于根对象,对用户不可见。当我们创建Method对象时,我们代码中获得的Method对象都相当于它的副本(或引用)。root对象持有一个MethodAccessor对象,所以所有获取到的Method对象都共享这一个MethodAccessor对象,因此必须保证它在内存中的可见性。root对象其声明及注释为:

1
2
3
4
5
6
7
8
private volatile MethodAccessor methodAccessor;
// For sharing of MethodAccessors. This branching structure is
// currently only two levels deep (i.e., one root Method and
// potentially many Method objects pointing to it.)
//
// If this branching structure would ever contain cycles, deadlocks can
// occur in annotation code.
private Method root;

那么MethodAccessor到底是个啥玩意呢?

1
2
3
4
5
6
7
8
9
10
/** This interface provides the declaration for
java.lang.reflect.Method.invoke(). Each Method object is
configured with a (possibly dynamically-generated) class which
implements this interface.
*/
public interface MethodAccessor {
/** Matches specification in {@link java.lang.reflect.Method} */
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException;
}

可以看到MethodAccessor是一个接口,定义了invoke方法。分析其Usage可得它的具体实现类有:

  • sun.reflect.DelegatingMethodAccessorImpl
  • sun.reflect.MethodAccessorImpl
  • sun.reflect.NativeMethodAccessorImpl

第一次调用一个Java方法对应的Method对象的invoke()方法之前,实现调用逻辑的MethodAccessor对象还没有创建;等第一次调用时才新创建MethodAccessor并更新给root,然后调用MethodAccessor.invoke()完成反射调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NOTE that there is no synchronization used here. It is correct
// (though not efficient) to generate more than one MethodAccessor
// for a given Method. However, avoiding synchronization will
// probably make the implementation more scalable.
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}

可以看到methodAccessor实例由reflectionFactory对象操控生成,它在AccessibleObject下的声明如下:

1
2
3
4
5
6
// Reflection factory used by subclasses for creating field,
// method, and constructor accessors. Note that this is called
// very early in the bootstrapping process.
static final ReflectionFactory reflectionFactory =
AccessController.doPrivileged(
new sun.reflect.ReflectionFactory.GetReflectionFactoryAction());

再研究一下sun.reflect.ReflectionFactory类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class ReflectionFactory {
private static boolean initted = false;
private static Permission reflectionFactoryAccessPerm
= new RuntimePermission("reflectionFactoryAccess");
private static ReflectionFactory soleInstance = new ReflectionFactory();
// Provides access to package-private mechanisms in java.lang.reflect
private static volatile LangReflectAccess langReflectAccess;
// 这里设计得非常巧妙
// "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.
//
// Package-private to be accessible to NativeMethodAccessorImpl
// and NativeConstructorAccessorImpl
private static boolean noInflation = false;
private static int inflationThreshold = 15;
//......
//这是生成MethodAccessor的方法
public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
//......
/** We have to defer full initialization of this class until after
the static initializer is run since java.lang.reflect.Method's
static initializer (more properly, that for
java.lang.reflect.AccessibleObject) causes this class's to be
run, before the system properties are set up. */
private static void checkInitted() {
if (initted) return;
AccessController.doPrivileged(
new PrivilegedAction<Void>() {
public Void run() {
// Tests to ensure the system properties table is fully
// initialized. This is needed because reflection code is
// called very early in the initialization process (before
// command-line arguments have been parsed and therefore
// these user-settable properties installed.) We assume that
// if System.out is non-null then the System class has been
// fully initialized and that the bulk of the startup code
// has been run.
if (System.out == null) {
// java.lang.System not yet fully initialized
return null;
}
String val = System.getProperty("sun.reflect.noInflation");
if (val != null && val.equals("true")) {
noInflation = true;
}
val = System.getProperty("sun.reflect.inflationThreshold");
if (val != null) {
try {
inflationThreshold = Integer.parseInt(val);
} catch (NumberFormatException e) {
throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", e);
}
}
initted = true;
return null;
}
});
}
}

观察前面的声明部分的注释,我们可以发现一些有趣的东西。就像注释里说的,实际的MethodAccessor实现有两个版本,一个是Java版本,一个是native版本,两者各有特点。初次启动时Method.invoke()和Constructor.newInstance()方法采用native方法要比Java方法快3-4倍,而启动后native方法又要消耗额外的性能而慢于Java方法。也就是说,Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

为了尽可能地减少性能损耗,HotSpot JDK采用“inflation”的技巧:让Java方法在被反射调用时,开头若干次使用native版,等反射调用次数超过阈值时则生成一个专用的MethodAccessor实现类,生成其中的invoke()方法的字节码,以后对该Java方法的反射调用就会使用Java版本。 这项优化是从JDK 1.4开始的。

研究ReflectionFactory.newMethodAccessor()生产MethodAccessor对象的逻辑,一开始(native版)会生产NativeMethodAccessorImpl和DelegatingMethodAccessorImpl两个对象。
DelegatingMethodAccessorImpl的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** Delegates its invocation to another MethodAccessorImpl and can
change its delegate at run time. */
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
void setDelegate(MethodAccessorImpl delegate) {
this.delegate = delegate;
}
}

它其实是一个中间层,方便在native版与Java版的MethodAccessor之间进行切换。
然后下面就是native版MethodAccessor的Java方面的声明:
sun.reflect.NativeMethodAccessorImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** Used only for the first few invocations of a Method; afterward,
switches to bytecode-based implementation */
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}

每次NativeMethodAccessorImpl.invoke()方法被调用时,程序调用计数器都会增加1,看看是否超过阈值;一旦超过,则调用MethodAccessorGenerator.generateMethod()来生成Java版的MethodAccessor的实现类,并且改变DelegatingMethodAccessorImpl所引用的MethodAccessor为Java版。后续经由DelegatingMethodAccessorImpl.invoke()调用到的就是Java版的实现了。
到这里,我们已经追寻到native版的invoke方法在Java一侧声明的最底层 - invoke0了,下面我们将深入到HotSpot JVM中去研究其具体实现。

寻根溯源 - 在JVM层面探究invoke0方法

invoke0方法是一个native方法,它在HotSpot JVM里调用JVM_InvokeMethod函数:

1
2
3
4
5
JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{
return JVM_InvokeMethod(env, m, obj, args);
}

openjdk/hotspot/src/share/vm/prims/jvm.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))
JVMWrapper("JVM_InvokeMethod");
Handle method_handle;
if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {
method_handle = Handle(THREAD, JNIHandles::resolve(method));
Handle receiver(THREAD, JNIHandles::resolve(obj));
objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));
oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);
jobject res = JNIHandles::make_local(env, result);
if (JvmtiExport::should_post_vm_object_alloc()) {
oop ret_type = java_lang_reflect_Method::return_type(method_handle());
assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!");
if (java_lang_Class::is_primitive(ret_type)) {
// Only for primitive type vm allocates memory for java object.
// See box() method.
JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
}
}
return res;
} else {
THROW_0(vmSymbols::java_lang_StackOverflowError());
}
JVM_END

其关键部分为Reflection::invoke_method:
openjdk/hotspot/src/share/vm/runtime/reflection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
oop mirror = java_lang_reflect_Method::clazz(method_mirror);
int slot = java_lang_reflect_Method::slot(method_mirror);
bool override = java_lang_reflect_Method::override(method_mirror) != 0;
objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));
oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
BasicType rtype;
if (java_lang_Class::is_primitive(return_type_mirror)) {
rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
} else {
rtype = T_OBJECT;
}
instanceKlassHandle klass(THREAD, java_lang_Class::as_Klass(mirror));
Method* m = klass->method_with_idnum(slot);
if (m == NULL) {
THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
}
methodHandle method(THREAD, m);
return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}

这里面又会涉及到Java的对象模型(klass和oop),以后继续补充。(留坑)

寻根溯源 - Java版的实现

Java版MethodAccessor的生成使用MethodAccessorGenerator实现,由于代码太长,这里就不贴代码了,只贴一下开头的注释:

1
2
3
4
5
6
7
/** Generator for sun.reflect.MethodAccessor and
sun.reflect.ConstructorAccessor objects using bytecodes to
implement reflection. A java.lang.reflect.Method or
java.lang.reflect.Constructor object can delegate its invoke or
newInstance method to an accessor using native code or to one
generated by this class. (Methods and Constructors were merged
together in this class to ensure maximum code sharing.) */

这里运用了asm动态生成字节码技术(sun.reflect.ClassFileAssembler),原理比较复杂,后面讲到AOP要用到asm技术的时候再深入了解一下吧。

本篇总结

简单地画了个图表示invoke方法的过程,日后再更时序图:

invoke方法的过程

番外篇

  1. MagicAccessorImpl是什么鬼?

原本Java的安全机制使得不同类之间不是任意信息都可见,但JDK里面专门设了个MagicAccessorImpl标记类开了个后门来允许不同类之间信息可以互相访问(由JVM管理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** <P> MagicAccessorImpl (named for parity with FieldAccessorImpl and
others, not because it actually implements an interface) is a
marker class in the hierarchy. All subclasses of this class are
"magically" granted access by the VM to otherwise inaccessible
fields and methods of other classes. It is used to hold the code
for dynamically-generated FieldAccessorImpl and MethodAccessorImpl
subclasses. (Use of the word "unsafe" was avoided in this class's
name to avoid confusion with {@link sun.misc.Unsafe}.) </P>
<P> The bug fix for 4486457 also necessitated disabling
verification for this class and all subclasses, as opposed to just
SerializationConstructorAccessorImpl and subclasses, to avoid
having to indicate to the VM which of these dynamically-generated
stub classes were known to be able to pass the verifier. </P>
<P> Do not change the name of this class without also changing the
VM's code. </P> */
class MagicAccessorImpl {
}
  1. @CallerSensitive注解又是什么鬼?

详见:JEP 176: Mechanical Checking of Caller-Sensitive Methods

Summary: Improve the security of the JDK’s method-handle implementation by replacing the existing hand-maintained list of caller-sensitive methods with a mechanism that accurately identifies such methods and allows their callers to be discovered reliably.

JDK 1.8才引进了这个注解,因此在老版本的反射实现里并没有这个玩意。这是它的定义:

1
2
3
4
5
6
7
8
9
10
11
/**
* A method annotated @CallerSensitive is sensitive to its calling class,
* via {@link sun.reflect.Reflection#getCallerClass Reflection.getCallerClass},
* or via some equivalent.
*
* @author John R. Rose
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface CallerSensitive {
}

简而言之,用@CallerSensitive注解修饰的方法从一开始就知道具体调用它的对象,这样就不用再经过一系列的检查才能确定具体调用它的对象了。它实际上是调用sun.reflect.Reflection.getCallerClass方法。

Reflection类位于调用栈中的0帧位置,sun.reflect.Reflection.getCallerClass()方法返回调用栈中从0帧开始的第x帧中的类实例。该方法提供的机制可用于确定调用者类,从而实现“感知调用者(Caller Sensitive)”的行为,即允许应用程序根据调用类或调用栈中的其它类来改变其自身的行为。


Reference

深入解析Java反射(1) - 基础

因为本人最近正筹备Samsara框架的开发,而其中的IOC部分非常依靠反射,因此趁这个机会来总结一下关于Java反射的一些知识。本篇为基本篇,基于JDK 1.8。


一、回顾:什么是反射?

反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。

Oracle 官方对反射的解释是:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法

重点:是运行时而不是编译时

二、反射的主要用途

很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。

反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

举一个例子,在运用 Struts 2 框架的开发中我们一般会在 struts.xml 里去配置 Action,比如:

1
2
3
4
5
6
<action name="login"
class="org.ScZyhSoft.test.action.SimpleLoginAction"
method="execute">
<result>/shop/shop-index.jsp</result>
<result name="error">login.jsp</result>
</action>

配置文件与 Action 建立了一种映射关系,当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter 拦截,然后 StrutsPrepareAndExecuteFilter 会去动态地创建 Action 实例。比如我们请求 login.action,那么 StrutsPrepareAndExecuteFilter就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。

对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。

三、反射的基本运用

上面我们提到了反射可以用于判断任意对象所属的类,获得 Class 对象,构造任意一个对象以及调用一个对象。这里我们介绍一下基本反射功能的使用和实现(反射相关的类一般都在 java.lang.relfect 包里)。

1、获得 Class 对象

方法有三种:

(1) 使用 Class 类的 forName 静态方法:

1
2
3
4
5
6
7
public static Class<?> forName(String className)
```
比如在 JDBC 开发中常用此方法加载数据库驱动:
```java
Class.forName(driver);

(2)直接获取某一个对象的 class,比如:

1
2
Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;

(3)调用某个对象的 getClass() 方法,比如:

1
2
StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();

2、判断是否为某个类的实例

一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:

1
public native boolean isInstance(Object obj);

3、创建实例

通过反射来生成对象主要有两种方式。

  • 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
1
2
Class<?> c = String.class;
Object str = c.newInstance();
  • 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
1
2
3
4
5
6
7
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);

4、获取方法

获取某个Class对象的方法集合,主要有以下几个方法:

  • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
1
public Method[] getDeclaredMethods() throws SecurityException
  • getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
1
public Method[] getMethods() throws SecurityException
  • getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
1
public Method getMethod(String name, Class<?>... parameterTypes)

只是这样描述的话可能难以理解,我们用例子来理解这三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.ScZyhSoft.common;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test1 {
public static void test() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> c = methodClass.class;
Object object = c.newInstance();
Method[] methods = c.getMethods();
Method[] declaredMethods = c.getDeclaredMethods();
//获取methodClass类的add方法
Method method = c.getMethod("add", int.class, int.class);
//getMethods()方法获取的所有方法
System.out.println("getMethods获取的方法:");
for(Method m:methods)
System.out.println(m);
//getDeclaredMethods()方法获取的所有方法
System.out.println("getDeclaredMethods获取的方法:");
for(Method m:declaredMethods)
System.out.println(m);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
getDeclaredMethods获取的方法:
public int org.ScZyhSoft.common.methodClass.add(int,int)
public int org.ScZyhSoft.common.methodClass.sub(int,int)

可以看到,通过 getMethods() 获取的方法可以获取到父类的方法,比如 java.lang.Object 下定义的各个方法。

5、获取构造器信息

获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

1
public T newInstance(Object ... initargs)

此方法可以根据传入的参数来调用对应的Constructor创建对象实例。

6、获取类的成员变量(字段)信息

主要是这几个方法,在此不再赘述:

  • getFiled:访问公有的成员变量
  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

getFiledsgetDeclaredFields 方法用法同上(参照 Method)。

7、调用方法

当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

1
2
3
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException

下面是一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}

关于 invoke 方法的详解,后面我会专门写一篇文章来深入解析 invoke 的过程。

8、利用反射创建数组

数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
public static void testArray() throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));
}

其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:

1
2
3
4
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}

newArray 方法是一个 native 方法,它在 HotSpot JVM 里的具体实现我们后边再研究,这里先把源码贴出来:

1
2
private static native Object newArray(Class<?> componentType, int length)
throws NegativeArraySizeException;

源码目录:openjdk\hotspot\src\share\vm\runtime\reflection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arrayOop Reflection::reflect_new_array(oop element_mirror, jint length, TRAPS) {
if (element_mirror == NULL) {
THROW_0(vmSymbols::java_lang_NullPointerException());
}
if (length < 0) {
THROW_0(vmSymbols::java_lang_NegativeArraySizeException());
}
if (java_lang_Class::is_primitive(element_mirror)) {
Klass* tak = basic_type_mirror_to_arrayklass(element_mirror, CHECK_NULL);
return TypeArrayKlass::cast(tak)->allocate(length, THREAD);
} else {
Klass* k = java_lang_Class::as_Klass(element_mirror);
if (k->oop_is_array() && ArrayKlass::cast(k)->dimension() >= MAX_DIM) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
return oopFactory::new_objArray(k, length, THREAD);
}
}

另外,Array 类的 setget 方法都为 native 方法,在 HotSpot JVM 里分别对应 Reflection::array_setReflection::array_get 方法,这里就不详细解析了。

四、反射的一些注意事项

由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。

另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

深入探究 JVM | Java 的内存区域解析

Java 虚拟机在执行Java程序的时候会把它管理的内存区域划为几部分,这一节我们就来解析一下Java的内存区域。


有的人把JVM管理的内存简单地分为堆内存和栈内存,这样分未免有些太肤浅了。
Java的内存区域主要分为五部分:

  • 程序计数器(PC)
  • 虚拟机栈(JVM Stack)
  • 本地方法栈(Native Method Stack)
  • Java 堆内存(Java Heap)
  • 方法区(Method Area)

Java内存区域
(图转自网络)

下面我们来解析这几个区域。

程序计数器

相信学过计算机组成原理的人都知道,CPU内部的寄存器中就包含一个程序计数器(x86下为eip寄存器,ARM下为R15寄存器),存放程序执行的下一条指令地址。在程序开始执行前,将程序指令序列的起始地址,即程序的第一条指令所在的内存单元地址送入PC,CPU按照PC的地址从内存中读取第一条指令。每一条指令执行时,CPU会自动修改PC的量至下一条指令的地址,指令之间的跳转离不开PC。JVM内存中的程序计数器也是这样的作用,它储存JVM当前执行bytecode的地址。

Java虚拟机允许多个线程同时执行指令。如果有多个线程正在执行指令,那么每个线程都会有一个程序计数器,它是线程私有的。在任意时刻,一个线程只允许执行一个方法的代码。每当执行到一条Java方法的指令时,程序计数器保存当前执行字节码的地址;若执行的为native方法,则PC的值为undefined。

Java 虚拟机栈

Java虚拟机栈也是线程私有的,每一条线程都拥有自己私有的Java 虚拟机栈,它与线程同时创建。它描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至完成的过程,都对应一个栈帧从入栈到出栈的过程。关于栈帧详细的内容在后边复习虚拟机字节码执行引擎的时候再说吧。
Java 虚拟机栈在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的入栈和出栈之外,Java虚拟机栈不会再受其它因素的影响,因此栈帧可在系统的堆上分配内存(注意,是系统的Heap而不是Java Heap)。Java虚拟机栈所使用的内存不需要保证是连续的。

本地方法栈

本地方法栈和Java虚拟机栈的作用相似,Java虚拟机栈执行的是字节码,而本地方法栈执行的是native方法。本地方法栈使用传统的栈(C Stack)来支持native方法。在HotSpot JVM中Java虚拟机栈和本地方法栈合二为一。

Java 堆

在JVM中,Java 堆是可供各线程共享的运行时内存区域,是Java 虚拟机所管理的内存区域中最大的一块。此区域非常重要,几乎所有的对象实例和数组实例都要在Java堆上分配,但随着JIT编译器及逃逸分析技术的发展,也可能会被优化为栈上分配,高大上。。。)。同时,Java 堆也是发生GC收集的主要区域。
从内存回收的角度来看,它可以分为新生代老年代,再细分可以分为Eden Space,From Survivor Space,To Survivor Space区域。Java堆的容量可以是固定的,也可以随着需要来扩展,并且在用不到的时候自动收缩。

方法区

方法区是线程共享的,它储存了每一个类的结构信息,比如运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些初始化的时候用到的特殊方法。方法区是堆的逻辑部分。
在JDK1.7及以前的HotSpot JVM中,方法区位于永久代(Permanent Generation,PermGen)中。由于永久代内可能会发生内存泄露或溢出等问题而导致的java.lang.OutOfMemoryError: PermGen ,JEP小组从JDK1.7开始就筹划移除永久代(JEP 122: Remove the Permanent Generation),并且在JDK 1.7中把字符串常量,符号引用等移出了永久代。到了Java 8,永久代被彻底地移出了JVM,取而代之的是元空间(Metaspace):

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

所以从Java 8开始,方法区被移至 Metaspace 内。有关Metaspace的相关总结,见下一篇文章。

运行时常量池

运行时常量池是class文件中每一个类或接口的常量池表的运行时表示形式,是方法区的一部分。它包括了若干种不同的常量。常量池表存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池具有动态性,运行期间也可以将新的量放到运行时常量池中,典型的应用是String类的intern方法:

1
public native String intern()

JDK 1.7开始,字符串常量和符号引用等就被移出永久代:

  • 符号引用迁移至系统堆内存(Native Heap)
  • 字符串字面量迁移至Java堆(Java Heap)

下一篇文章我将会总结Java 8中的Metaspace相关知识


参考资料

Docker 入门学习札记

很早之前就听说过Docker,但一直木有实践过。前一阵子参加OSC的源创会时,里面嘉宾讲的东西提到了Docker,感觉Docker是一个很有意义的工具,于是就学习了一下它的基本用法,在此总结一下。

以我的理解,Docker是一种虚拟化容器,这个容器里可以运行各种程序,是一种轻量级的虚拟化技术,使用Golang编写。它与传统的虚拟化技术(KVM和Xen)的区别是,Docker容器是操作系统层面上的虚拟化(内核级),基于LXC技术,直接服用本地操作系统,而传统的虚拟化技术则是从硬件方面进行虚拟化。它的好处是,启动特别快,而且对系统资源的利用率较高,一台机器上可以运行很多Docker容器,而且因为Docker非常轻量,它为快速部署提供了很好的技术支持。并且Docker引入了版本控制的思想,可以更好地持续构建和部署。

下面总结一下Docker的基本用法:

安装

用apt-get或者yum安装都是坑,基本上都不是最新版本的。要安装还是用官方的命令安装:

1
curl -sSL https://get.docker.com/ | sh

不过由于Docker的安装文件存放在Amazon S3上,会间接性抽风,因此最好找个国内的源。

(2015-7 补充:DaoCloud是不错的选择,服务很好)

容器与镜像

Docker容器可以理解为运行在沙盒里的进程。

通俗地讲,镜像(images)相当于一个类,而容器(container)相当于类的一个实例。容器可以不断地被修改,并通过commit化为一个镜像。

基本命令

主要有:

Docker信息

查看Docker版本:docker version
登录至远程库:docker login

镜像操作

列出镜像列表:docker images
删除镜像:docker rmi [id]
搜索镜像:docker search [name]
从远程拉取镜像:docker pull [name]:[tag]
将镜像发布至远程:docker push [name]

容器运行

容器运行用docker run命令,它有一些详细的参数很有用,比如:
以交互模式启动容器centos:latest : docker run -i - t centos:latest /bin/bash
-i表示支持输入,-t表示命令行交互。
以后台模式运行,并进行端口映射: docker run -d -p 8080:8080 sczyh30/tomcat:v1 /start.sh
-d表示daemon,即以后台方式运行。-p表示端口映射。
这里用的时候有个比较fucking的地方,如果运行容器的时候相应的命令不阻塞,那么容器会自动结束运行(比如resin)。看来以后运行命令要加个阻塞的命令咯~
另外对于在容器里自动执行apt-get命令时,一定要加上-y,否则容器虽然进入交互模式但却无法响应,运行完即退出。

容器管理

查看正在运行的容器:docker ps
查看所有容器:docker ps -a
删除容器:docker rm [name/id]
删除所有容器:docker rm 'docker ps -a -q'
停止、启动、杀死一个容器:

1
2
3
$docker stop [Name/ID]
$docker start [Name/ID]
$docker kill [Name/ID]

从容器中读取日志:docker logs [Name/ID]
列出更改项:docker diff [Name/ID]
从容器中拷贝文件至本机:docker cp ID:/container_path to_path
附加到一个运行的容器上:docker attach [ID]

容器的简单构建

通过命令操作来部署容器显然很麻烦,所以通过Dockerfile进行自动化容器构建是非常方便的。
这里我自己写了一个简单的Dockerfile来练练手,这是一个用于构建Hexo博客的Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#构建的镜像基础
FROM node:slim
#维护者
MAINTAINER sczyh30 root@sczyh30.com
#执行命令用RUN,注意要遇到需要交互的地方加-y
RUN apt-get update \
&& apt-get install -y git ssh-client ca-certificates --no-install-recommends \
&& rm -r /var/lib/apt/lists/*
#更改时区
RUN echo "Asia/Shanghai" > /etc/timezone \
&& dpkg-reconfigure -f noninteractive tzdata
#安装hexo
RUN npm install hexo-cli -g
#初始化
RUN cd / && hexo init blog && cd blog && npm install
#设定默认工作目录
WORKDIR /blog
#设定暴露端口
EXPOSE 4000
#设定容器运行时执行的命令,只能有一条,若多条则执行最后一条
CMD hexo s

然后在此目录执行构建:

1
docker build -t sczyh30/simple_test:v1 .

.表示在当前目录构建
docker build的一些参数:

1
2
3
4
--no-cache=false Do not use cache when building the image
-q, --quiet=false Suppress the verbose output generated by the containers
--rm=true Remove intermediate containers after a successful build
-t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success

执行后就会按照Dockerfile的内容进行构建,过几分钟构建成功,用docker run运行一下:

1
docker run -d -p 80:4000 sczyh30/simple_test:v1

运行成功~在docker ps里可以看到容器已成功运行:

1
2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
45782259f2c3 sczyh30/simple_test:v1 "/bin/sh -c 'hexo s'" 2 minutes ago Up 2 minutes 0.0.0.0:80->4000/tcp trusting_ramanujan

在浏览器中打开,一个简单的Hexo已经搭好咯~

总结一下,Dockerfile非常方便,以后用它构建容器进行集群部署貌似是不错的选择呢~
后面将进行更多的关于Docker的实践。

Unity游戏开发总结

记录我在2014.11 - 2015.2用Unity开发独立游戏的体验~

Prolouge

这是一开始玩 Unity 的时候做的画面,现在看起来一分钟就可以堆出来。。然而当时处于探索的初级阶段,捣鼓了不少时间才捣鼓出来。。。

2014.11.24 开发截图

2014.11.25 开发截图

当时的艰辛呀!调个特效都调半天。。

Good Job

自己的独立游戏(开发代号 XXJ)一共9个场景(部分场景未完成),美工素材大部分都来源于网络(美工渣没办法。。),古风类型的,渲染和游戏脚本为自创,开发用的Unity 4.5.5f1。

Scene 1

先放一张最原汁原味的Scene 1的图吧~(地图还没来得及起名→_→我想要文艺的名字)

XXJ-Scene 1-UltraRendering

Scene 2

Scene 2 - Desert:

XXJ-Scene 2-UltraRendering-Dev

Scene 6

待更新~

后记

今年3月份Unity 5也轰轰烈烈地发布了。说实话,Unity 5的特效方面改进非常大,特别是光照!然而Unity 5 Project结构貌似也有不小的变动(特别是用自带的Standard Assets,迁移版本都失败了,Shader和脚本的变化应该很大)。

最近体验Unity 5随手搓的Scene