使用sun.misc.Unsafe及反射对内存进行内省(introspection)

对于一个有经验的JAVA程序员来说,了解一个或者其它的JAVA对象占用了多少内存,这将会非常有用。你可能已经听说过我们所生活的世界,存储容量将不再是一个问题,这个对于你的文本编辑器来说可能是对的(不过,打开一个包含大量的图片以及图表的文档,看看你的编辑器会消耗多少内存),对于一个专用服务器软件来说也可能是对的(至少在你的企业成长到足够大或者是在同一台服务器运行其它的软件之前),对于基于云的软件来说也可能是对的,如果你足够的富有可以花足够的钱可以买顶级的服务器硬件。

然而,现实是你的软件如果是受到了内存限制,需要做的是花钱优化它而不是尝试获取更好的硬件(原文:in the real world your software will once reach a point where it makes sense to spend money in its optimization rather than trying to obtain an even better hardware)(目前你可以获取到的最好的商业服务器是64G内存),此时你不得不分析你的应用程序来找出是哪个数据结构消耗了大部份的内存。对于这种分析任务,最好的工具就是一个好的性能分析工具,但是你可以在刚开始的时候,使用分析你代码中的对象这种用廉价的方式。这篇文章描述了使用基于Oracle
JDK的ClassIntrospector类,来分析你的应用程序内存消耗。

我曾经在文章字符串包装第1部分:将字符转换为字节中提到了JAVA对象内存结构,例如我曾经写过,在JAVA1.7.0_06以前,一个具有28个字符的字符串会占用104个字节,事实上,我在写这篇文章的时候,通过自己的性能分析器证实了我的计算结果。现在我们使用Oracle
JDK中特殊类sun.misc.Unsafe,通过纯JAVA来实现一个JAVA对象内省器(introspector)。

我们使用sun.misc.Unsafe的以下方法:

[java] view
plain
copy

  1. //获取字节对象中非静态方法的偏移量(get offset of a non-static field in the object in bytes
  2. public native long objectFieldOffset(java.lang.reflect.Field field);
  3. //获取数组中第一个元素的偏移量(get offset of a first element in the array)
  4. public native int arrayBaseOffset(java.lang.Class aClass);
  5. //获取数组中一个元素的大小(get size of an element in the array)
  6. public native int arrayIndexScale(java.lang.Class aClass);
  7. //获取JVM中的地址值(get address size for your JVM)
  8. public native int addressSize();

在sun.misc.Unsafe中有两个额外的内省方法:staticFieldBase及staticFieldOffset,但是在这篇文章中不会使用到。这两个方法对于非安全的读写静态方法会有用。

我们应如何找到一个对象的内存布局?

1、循环的在分析类及父类上调用Class.getDeclaredFields,获取所有对象的字段,包括其父类中的字段;

2、针对非静态字段(通过Field.getModifiers() & Modifiers.STATIC判断静态字段),通过使用Unsafe.objectFieldOffset在其父类中获取一个字段的偏移量以及该字段的shallow(注:shallow指的是当前对象本身的大小)大小:基础类型的默认值及4个或8个字节的对象引用(更多看下面);

3、对数组来说,调用Unsafe.arrayBaseOffset及Unsafe.arrayIndexScale,数组的整个shallow大小将会是 当前数组的偏移量+每个数组的大小*数组的长度(原文是:offset + scale * Array.getLength(array)),当然了也包括对数组本身引用的大小(看前面提到的);

4、别忘了对象图的循环引用,因而就需要对前面已经分析过的对象进行跟踪记录(针对这些情况,推荐使用IdentityHashMap

Java对象引用大小是一个非常不确定的值(原文:Java Object reference size is quite a virtual value),它可能是4个字节或者是8个字节,这个取决于你的JVM设置以及给了多少内存给JVM,针对32G以上的堆,它就总是8个字节,但是针对小一点的堆就是4个字节除非你在JVM设置里关掉设置-XX:-UseCompressedOops(我不确定这个功能是在JVM的哪个版本加进来的,或者是默认是打开的)。结果就是,安全的方式获取对像引用的大小就是找到Object[]数组中一个元素的大小:unsafe.arrayIndexScale(
Object[].class )
,针对这种情况,Unsafe.addressSize倒不实用了。

针对32G以下堆内存中例用4字节引用的一点小小注意。一个正常的4个字节的指针可以定位到4G地址空间任何地址。如果我们假设所有已分配的对象将通过8字节边界对齐,在我们的32位指针中我们将不再需要最低3位(这些位将总是等于零)。这意味着我们可以存储35位地址在32位中。(这一节附上原文如下:

A small implementation note on 4 byte references on under 32G heaps. A normal 4 byte pointer could addressany byte in 4G address space. If we will assume that all allocated objects will be aligned by 8 bytes boundary, we won’t
need 3 lowest bits in our 32 bit pointers anymore (these bits will always be equal to zeroes). This means that we can store 35 bit addresses in 32 bit value:)

[plain] view
plain
copy

  1. 32_bit_reference = ( int ) ( actual_64_bit_pointer >> 3 )

35位允许寻址 32位*8=4G*8=32G地址空间。

写这个工具时发现的其它的一些有趣的事情

1、要打印数组的内容,必须使用Arrays.toString(包括基本类型及对象数组);

2、你必须要小心 - 内省方法(introspection method)只接受对象作为字段值,因此你最终可能处在无限循环中:整型打包成整数,以便传递到内省的方法。里面你会发现一个Integer.value字段,并尝试再次内省了 - 瞧,你又回到了开始的地方!

3、要内省(introspect)对象数组中所有非空的值 - 这仅仅是间接的对象图中的外部level(原文:this is just an extra level of indirection in the object graph)

如何使用ClassIntrospector类?仅需要实例化它并且在你的任意的对象中调用它的实例内省(introspect)方法,它会返回一个ObjectInfo对象,这个对象与你的“根‘对象有关,这个对象将指向它的所有子项,我想这可能是足够的打印其toString方法的结果和/或调用ObjectInfo.getDeepSize方法(原文:I think it may be sufficient to print its toString method
result and/or to call ObjectInfo.getDeepSize method),它将通过你的”根“对象引用,返回你的所有对象的总内存消耗。

ClassIntrospector不是线程安全的,但是你可以在同一个线程中任意多次调用内省(introspect)方法。

总结

1、你可以使用sun.misc.Unsafe的这些方法获取Java对象的布局信息:objectFieldOffsetarrayBaseOffset and arrayIndexScale

2、Java对象引用的大小取决于你当前的环境,根据不同JVM的设置以及分配给JVM的内存大小,它可能是4个或者8个字节。在大于32G的堆中,对像引用的大小总会是8个字节,但是在一个比较小的堆中它就会是4个字节,除非关闭JVM设置:-XX:-UseCompressedOops

源码

ClassIntrospector:

[java] view
plain
copy

  1. import sun.misc.Unsafe;
  2. import java.lang.reflect.Array;
  3. import java.lang.reflect.Field;
  4. import java.lang.reflect.Modifier;
  5. import java.math.BigDecimal;
  6. import java.util.*;
  7. /**
  8. * This class could be used for any object contents/memory layout printing.
  9. */
  10. public class ClassIntrospector
  11. {
  12. public static void main(String[] args) throws IllegalAccessException {
  13. final ClassIntrospector ci = new ClassIntrospector();
  14. final Map<String, BigDecimal> map = new HashMap<String, BigDecimal>( 10);
  15. map.put( "one", BigDecimal.ONE );
  16. map.put( "zero", BigDecimal.ZERO );
  17. map.put( "ten", BigDecimal.TEN );
  18. final ObjectInfo res;
  19. res = ci.introspect( "0123456789012345678901234567" );
  20. //res = ci.introspect( new TestObjChild() );
  21. //res = ci.introspect(map);
  22. //res = ci.introspect( new String[] { "str1", "str2" } );
  23. //res = ci.introspect(ObjectInfo.class);
  24. //res = ci.introspect( new TestObj() );
  25. System.out.println( res.getDeepSize() );
  26. System.out.println( res );
  27. }
  28. /** First test object - testing various arrays and complex objects */
  29. private static class TestObj
  30. {
  31. protected final String[] strings = { "str1", "str2" };
  32. protected final int[] ints = { 14, 16 };
  33. private final Integer i = 28;
  34. protected final BigDecimal bigDecimal = BigDecimal.ONE;
  35. @Override
  36. public String toString() {
  37. return "TestObj{" +
  38. "strings=" + (strings == null ? null : Arrays.asList(strings)) +
  39. ", ints=" + Arrays.toString( ints ) +
  40. ", i=" + i +
  41. ", bigDecimal=" + bigDecimal +
  42. ‘}‘;
  43. }
  44. }
  45. /** Test class 2 - testing inheritance */
  46. private static class TestObjChild extends TestObj
  47. {
  48. private final boolean[] flags = { true, true, false };
  49. private final boolean flag = false;
  50. @Override
  51. public String toString() {
  52. return "TestObjChild{" +
  53. "flags=" + Arrays.toString( flags ) +
  54. ", flag=" + flag +
  55. ‘}‘;
  56. }
  57. }
  58. private static final Unsafe unsafe;
  59. /** Size of any Object reference */
  60. private static final int objectRefSize;
  61. static
  62. {
  63. try
  64. {
  65. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  66. field.setAccessible(true);
  67. unsafe = (Unsafe)field.get(null);
  68. objectRefSize = unsafe.arrayIndexScale( Object[].class );
  69. }
  70. catch (Exception e)
  71. {
  72. throw new RuntimeException(e);
  73. }
  74. }
  75. /** Sizes of all primitive values */
  76. private static final Map<Class, Integer> primitiveSizes;
  77. static
  78. {
  79. primitiveSizes = new HashMap<Class, Integer>( 10 );
  80. primitiveSizes.put( byte.class, 1 );
  81. primitiveSizes.put( char.class, 2 );
  82. primitiveSizes.put( int.class, 4 );
  83. primitiveSizes.put( long.class, 8 );
  84. primitiveSizes.put( float.class, 4 );
  85. primitiveSizes.put( double.class, 8 );
  86. primitiveSizes.put( boolean.class, 1 );
  87. }
  88. /**
  89. * Get object information for any Java object. Do not pass primitives to this method because they
  90. * will boxed and the information you will get will be related to a boxed version of your value.
  91. * @param obj Object to introspect
  92. * @return Object info
  93. * @throws IllegalAccessException
  94. */
  95. public ObjectInfo introspect( final Object obj ) throws IllegalAccessException
  96. {
  97. try
  98. {
  99. return introspect( obj, null );
  100. }
  101. finally { //clean visited cache before returning in order to make this object reusable
  102. m_visited.clear();
  103. }
  104. }
  105. //we need to keep track of already visited objects in order to support cycles in the object graphs
  106. private IdentityHashMap<Object, Boolean> m_visited = new IdentityHashMap<Object, Boolean>( 100 );
  107. private ObjectInfo introspect( final Object obj, final Field fld ) throws IllegalAccessException
  108. {
  109. //use Field type only if the field contains null. In this case we will at least know what‘s expected to be
  110. //stored in this field. Otherwise, if a field has interface type, we won‘t see what‘s really stored in it.
  111. //Besides, we should be careful about primitives, because they are passed as boxed values in this method
  112. //(first arg is object) - for them we should still rely on the field type.
  113. boolean isPrimitive = fld != null && fld.getType().isPrimitive();
  114. boolean isRecursive = false; //will be set to true if we have already seen this object
  115. if ( !isPrimitive )
  116. {
  117. if ( m_visited.containsKey( obj ) )
  118. isRecursive = true;
  119. m_visited.put( obj, true );
  120. }
  121. final Class type = ( fld == null || ( obj != null && !isPrimitive) ) ?
  122. obj.getClass() : fld.getType();
  123. int arraySize = 0;
  124. int baseOffset = 0;
  125. int indexScale = 0;
  126. if ( type.isArray() && obj != null )
  127. {
  128. baseOffset = unsafe.arrayBaseOffset( type );
  129. indexScale = unsafe.arrayIndexScale( type );
  130. arraySize = baseOffset + indexScale * Array.getLength( obj );
  131. }
  132. final ObjectInfo root;
  133. if ( fld == null )
  134. {
  135. root = new ObjectInfo( "", type.getCanonicalName(), getContents( obj, type ), 0, getShallowSize( type ),
  136. arraySize, baseOffset, indexScale );
  137. }
  138. else
  139. {
  140. final int offset = ( int ) unsafe.objectFieldOffset( fld );
  141. root = new ObjectInfo( fld.getName(), type.getCanonicalName(), getContents( obj, type ), offset,
  142. getShallowSize( type ), arraySize, baseOffset, indexScale );
  143. }
  144. if ( !isRecursive && obj != null )
  145. {
  146. if ( isObjectArray( type ) )
  147. {
  148. //introspect object arrays
  149. final Object[] ar = ( Object[] ) obj;
  150. for ( final Object item : ar )
  151. if ( item != null )
  152. root.addChild( introspect( item, null ) );
  153. }
  154. else
  155. {
  156. for ( final Field field : getAllFields( type ) )
  157. {
  158. if ( ( field.getModifiers() & Modifier.STATIC ) != 0 )
  159. {
  160. continue;
  161. }
  162. field.setAccessible( true );
  163. root.addChild( introspect( field.get( obj ), field ) );
  164. }
  165. }
  166. }
  167. root.sort(); //sort by offset
  168. return root;
  169. }
  170. //get all fields for this class, including all superclasses fields
  171. private static List<Field> getAllFields( final Class type )
  172. {
  173. if ( type.isPrimitive() )
  174. return Collections.emptyList();
  175. Class cur = type;
  176. final List<Field> res = new ArrayList<Field>( 10 );
  177. while ( true )
  178. {
  179. Collections.addAll( res, cur.getDeclaredFields() );
  180. if ( cur == Object.class )
  181. break;
  182. cur = cur.getSuperclass();
  183. }
  184. return res;
  185. }
  186. //check if it is an array of objects. I suspect there must be a more API-friendly way to make this check.
  187. private static boolean isObjectArray( final Class type )
  188. {
  189. if ( !type.isArray() )
  190. return false;
  191. if ( type == byte[].class || type == boolean[].class || type == char[].class || type == short[].class ||
  192. type == int[].class || type == long[].class || type == float[].class || type == double[].class )
  193. return false;
  194. return true;
  195. }
  196. //advanced toString logic
  197. private static String getContents( final Object val, final Class type )
  198. {
  199. if ( val == null )
  200. return "null";
  201. if ( type.isArray() )
  202. {
  203. if ( type == byte[].class )
  204. return Arrays.toString( ( byte[] ) val );
  205. else if ( type == boolean[].class )
  206. return Arrays.toString( ( boolean[] ) val );
  207. else if ( type == char[].class )
  208. return Arrays.toString( ( char[] ) val );
  209. else if ( type == short[].class )
  210. return Arrays.toString( ( short[] ) val );
  211. else if ( type == int[].class )
  212. return Arrays.toString( ( int[] ) val );
  213. else if ( type == long[].class )
  214. return Arrays.toString( ( long[] ) val );
  215. else if ( type == float[].class )
  216. return Arrays.toString( ( float[] ) val );
  217. else if ( type == double[].class )
  218. return Arrays.toString( ( double[] ) val );
  219. else
  220. return Arrays.toString( ( Object[] ) val );
  221. }
  222. return val.toString();
  223. }
  224. //obtain a shallow size of a field of given class (primitive or object reference size)
  225. private static int getShallowSize( final Class type )
  226. {
  227. if ( type.isPrimitive() )
  228. {
  229. final Integer res = primitiveSizes.get( type );
  230. return res != null ? res : 0;
  231. }
  232. else
  233. return objectRefSize;
  234. }
  235. }

ObjectInfo:

[java] view
plain
copy

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.Comparator;
  4. import java.util.List;
  5. /**
  6. * This class contains object info generated by ClassIntrospector tool
  7. */
  8. public class ObjectInfo {
  9. /** Field name */
  10. public final String name;
  11. /** Field type name */
  12. public final String type;
  13. /** Field data formatted as string */
  14. public final String contents;
  15. /** Field offset from the start of parent object */
  16. public final int offset;
  17. /** Memory occupied by this field */
  18. public final int length;
  19. /** Offset of the first cell in the array */
  20. public final int arrayBase;
  21. /** Size of a cell in the array */
  22. public final int arrayElementSize;
  23. /** Memory occupied by underlying array (shallow), if this is array type */
  24. public final int arraySize;
  25. /** This object fields */
  26. public final List<ObjectInfo> children;
  27. public ObjectInfo(String name, String type, String contents, int offset, int length, int arraySize,
  28. int arrayBase, int arrayElementSize)
  29. {
  30. this.name = name;
  31. this.type = type;
  32. this.contents = contents;
  33. this.offset = offset;
  34. this.length = length;
  35. this.arraySize = arraySize;
  36. this.arrayBase = arrayBase;
  37. this.arrayElementSize = arrayElementSize;
  38. children = new ArrayList<ObjectInfo>( 1 );
  39. }
  40. public void addChild( final ObjectInfo info )
  41. {
  42. if ( info != null )
  43. children.add( info );
  44. }
  45. /**
  46. * Get the full amount of memory occupied by a given object. This value may be slightly less than
  47. * an actual value because we don‘t worry about memory alignment - possible padding after the last object field.
  48. *
  49. * The result is equal to the last field offset + last field length + all array sizes + all child objects deep sizes
  50. * @return Deep object size
  51. */
  52. public long getDeepSize()
  53. {
  54. return length + arraySize + getUnderlyingSize( arraySize != 0 );
  55. }
  56. private long getUnderlyingSize( final boolean isArray )
  57. {
  58. long size = 0;
  59. for ( final ObjectInfo child : children )
  60. size += child.arraySize + child.getUnderlyingSize( child.arraySize != 0 );
  61. if ( !isArray && !children.isEmpty() )
  62. size += children.get( children.size() - 1 ).offset + children.get( children.size() - 1 ).length;
  63. return size;
  64. }
  65. private static final class OffsetComparator implements Comparator<ObjectInfo>
  66. {
  67. @Override
  68. public int compare( final ObjectInfo o1, final ObjectInfo o2 )
  69. {
  70. return o1.offset - o2.offset; //safe because offsets are small non-negative numbers
  71. }
  72. }
  73. //sort all children by their offset
  74. public void sort()
  75. {
  76. Collections.sort( children, new OffsetComparator() );
  77. }
  78. @Override
  79. public String toString() {
  80. final StringBuilder sb = new StringBuilder();
  81. toStringHelper( sb, 0 );
  82. return sb.toString();
  83. }
  84. private void toStringHelper( final StringBuilder sb, final int depth )
  85. {
  86. depth( sb, depth ).append("name=").append( name ).append(", type=").append( type )
  87. .append( ", contents=").append( contents ).append(", offset=").append( offset )
  88. .append(", length=").append( length );
  89. if ( arraySize > 0 )
  90. {
  91. sb.append(", arrayBase=").append( arrayBase );
  92. sb.append(", arrayElemSize=").append( arrayElementSize );
  93. sb.append( ", arraySize=").append( arraySize );
  94. }
  95. for ( final ObjectInfo child : children )
  96. {
  97. sb.append( ‘\n‘ );
  98. child.toStringHelper(sb, depth + 1);
  99. }
  100. }
  101. private StringBuilder depth( final StringBuilder sb, final int depth )
  102. {
  103. for ( int i = 0; i < depth; ++i )
  104. sb.append( ‘\t‘ );
  105. return sb;
  106. }
  107. }

原文地址:http://java-performance.info/memory-introspection-using-sun-misc-unsafe-and-reflection/

使用sun.misc.Unsafe及反射对内存进行内省(introspection)

时间: 2024-10-22 22:11:33

使用sun.misc.Unsafe及反射对内存进行内省(introspection)的相关文章

java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值

在上一篇文章中.我们列出了计算java对象大小的几个结论以及jol工具的使用,jol工具的源代码有兴趣的能够去看下.如今我们利用JDK中的sun.misc.Unsafe来计算下字段的偏移地址,一则验证下之前文章中的结论,再则跟jol输出结果对照下.怎样获取sun.misc.Unsafe对象.能够參考这篇文章. public class VO { public int a = 0; public long b = 0; public static String c= "123"; pub

Java中的sun.misc.Unsafe包

chronicle项目:https://github.com/peter-lawrey/Java-Chronicle 这个项目是利用mmap机制来实现高效的读写数据,号称每秒写入5到20百万条数据. 作者有个测试,写入1百万条log用时0.234秒,用java自带的logger,用时7.347秒. 在看chronicle的源代码,发现一个牛B的利用Unsafe来直接读写内存,从而提高效率的例子. 详细见这个类:https://github.com/peter-lawrey/Java-Chroni

Java Magic. Part 4: sun.misc.Unsafe

原文地址 译文地址 译者:许巧辉 校对:梁海舰 Java是一门安全的编程语言,防止程序员犯很多愚蠢的错误,它们大部分是基于内存管理的.但是,有一种方式可以有意的执行一些不安全.容易犯错的操作,那就是使用Unsafe类. 本文是sun.misc.Unsafe公共API的简要概述,及其一些有趣的用法. Unsafe 实例 在使用Unsafe之前,我们需要创建Unsafe对象的实例.这并不像Unsafe unsafe = new Unsafe()这么简单,因为Unsafe的构造器是私有的.它也有一个静

Java sun.misc.unsafe类的使用

Java是一个安全的开发工具,它阻止开发人员犯很多低级的错误,而大部份的错误都是基于内存管理方面的.如果你想搞破坏,可以使用Unsafe这个类.这个类是属于sun.*API中的类,并且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文档,更可悲的是,它也没有比较好的代码文档. 1.实例化sun.misc.Unsafe 如果你尝试创建Unsafe类的实例,基于以下两种原因是不被允许的. 1).Unsafe类的构造函数是私有的: 2).虽然它有静态的getUnsafe()方法,但是如果你尝试

sun.misc.unsafe

Java中大部分错误都是基于内存管理方面的.如果想破坏,可以使用Unsafe这个类. 实例化Unsafe: 下面两种方式是不行的 private Unsafe() {} //私有构造方法 @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if(!VM.isSystemDomainLoader(var0.getClassLoader())) {//如果不是JDK

sun.misc.unsafe类的使用

这个帖子是关于JAVA中鲜为人知的特性的后续更新,如果想得到下次在线讨论的更新,请通过邮件订阅,并且不要忘了在评论区留下你的意见和建议. Java是一个安全的开发工具,它阻止开发人员犯很多低级的错误,而大部份的错误都是基于内存管理方面的.如果你想搞破坏,可以使用Unsafe这个类.这个类是属于sun.* API中的类,并且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文档,更可悲的是,它也没有比较好的代码文档. 实例化sun.misc.Unsafe 如果你尝试创建Unsafe类的实例,

sun.misc.Unsafe的理解

以下sun.misc.Unsafe源码和demo基于jdk1.7: 最近在看J.U.C里的源码,很多都用到了sun.misc.Unsafe这个类,一知半解,看起来总感觉有点不尽兴,所以打算对Unsafe的源码及使用做个分析: 另外,网上找了份c++的源代码natUnsafe.cc(可惜比较老,Copyright (C) 2006, 2007年的,没找到新的),也就是sun.misc.Unsafe的C++实现,跟Unsafe类中的native方法对照起来看更加容易理解: Unsafe类的作用 可以

java的sun.misc.Unsafe类

阅读目录 前言 Unsafe类的作用 获取Unsafe对象 Unsafe类中的API 前言 以下sun.misc.Unsafe源码和demo基于jdk1.7: 最近在看J.U.C里的源码,很多都用到了sun.misc.Unsafe这个类,一知半解,看起来总感觉有点不尽兴,所以打算对Unsafe的源码及使用做个分析: 另外,网上找了份c++的源代码natUnsafe.cc(可惜比较老,Copyright (C) 2006, 2007年的,没找到新的),也就是sun.misc.Unsafe的C++实

聊聊序列化(二)使用sun.misc.Unsafe绕过new机制来创建Java对象

在序列化的问题域里面有一个常见的问题,就是反序列化时用何种方式来创建Java对象,因为反序列化的目的是把一段二进制流转化成一个对象. 在Java里面创建对象有几种方式: 1. 显式地调用new语句, 比如 DemoClass demo = new DemoClass() 2. 利用反射机制,通过Class对象的newInstance()方法,比如DemoClass demo = DemoClass.class.newInstance(). 但是有个前提就是必须提供无参的构造函数 3. 利用反射机