标签 : JVM
这个 idea 最初来源于 TaobaoJVM 对 OpenJDK 定制开发的 GCIH 部分 (详见的分享 -), 其中 GCIH 就是将 CMS Old Heap 区的一部分划分出来, 这部分内存虽然还在堆内, 但已不被 GC 所管理. 将长生命周期 Java 对象放在 Java 堆外, GC 不能管理 GCIH 内 Java 对象 (GC Invisible Heap):
但是大部分的互联公司不能像阿里这样可以有专门的工程师针对自己的业务特点定制 JVM, 因此我们只能 "眼馋"GCIH 带来的性能提升却无法 "享用". 但通用的 JVM 开放了接口可直接向操作系统申请堆外内存 (
or
- ByteBuffer
), 而这部分内存也是 GC 所顾及不到的, 因此我们可用 JVM 堆外内存来模拟 GCIH 的功能 (但相比 GCIH 不足的是需要付出 serialize/deserialize 的开销).
- Unsafe
在一文中介绍的中是找不到堆外内存区域的:
因为它并不是 JVM 运行时数据区的一部分, 也不是 Java 虚拟机规范中定义的内存区域, 这部分内存区域直接被操作系统管理.
在 JDK 1.4 以前, 对这部分内存访问没有光明正大的做法: 只能通过反射拿到
类, 然后调用
- Unsafe
来申请 / 释放这块内存. 1.4 开始新加入了 NIO, 它引入了一种基于 Channel 与 Buffer 的 I/O 方式, 可以使用 Native 函数库直接分配堆外内存, 然后通过一个存储在 Java 堆里面的
- allocateMemory()/freeMemory()
对象作为这块内存的引用进行操作,
- DirectByteBuffer
提供了如下常用方法来跟堆外内存打交道:
- ByteBuffer
API | 描述 |
---|---|
|
Allocates a new direct byte buffer. |
|
Relative put method (optional operation). |
|
Relative bulk put method (optional operation). |
|
Relative put method for writing a Char/Double/Float/Int/Long/Short value (optional operation). |
|
Relative bulk get method. |
|
Relative get method for reading a Char/Double/Float/Int/Long/Short value. |
|
Creates a view of this byte buffer as a Char/Double/Float/Int/Long/Short buffer. |
|
Creates a new, read-only byte buffer that shares this buffer's content. |
|
Tells whether or not this byte buffer is direct. |
|
Creates a new byte buffer that shares this buffer's content. |
- /**
- * @author jifang
- * @since 2016/12/31 下午6:05.
- */
- public class DirectByteBufferApp extends AbstractAppInvoker {
- @Test
- @Override
- public void invoke(Object... param) {
- Map map = createInHeapMap(SIZE);
- // move in off-heap
- byte[] bytes = serializer.serialize(map);
- ByteBuffer buffer = ByteBuffer.allocateDirect(bytes.length);
- buffer.put(bytes);
- buffer.flip();
- // for gc
- map = null;
- bytes = null;
- System.out.println("write down");
- // move out from off-heap
- byte[] offHeapBytes = new byte[buffer.limit()];
- buffer.get(offHeapBytes);
- Map deserMap = serializer.deserialize(offHeapBytes);
- for (int i = 0; i < SIZE; ++i) {
- String key = "key-" + i;
- FeedDO feedDO = deserMap.get(key);
- checkValid(feedDO);
- if (i % 10000 == 0) {
- System.out.println("read " + i);
- }
- }
- free(buffer);
- }
- private Map createInHeapMap(int size) {
- long createTime = System.currentTimeMillis();
- Map map = new ConcurrentHashMap<>(size);
- for (int i = 0; i < size; ++i) {
- String key = "key-" + i;
- FeedDO value = createFeed(i, key, createTime);
- map.put(key, value);
- }
- return map;
- }
- }
由 JDK 提供的堆外内存访问 API 只能申请到一个类似一维数组的
, JDK 并未提供基于堆外内存的实用数据结构实现 (如堆外的
- ByteBuffer
、
- Map
), 因此想要实现 Cache 的功能只能在
- Set
时先将数据
- write()
到一个堆内的
- put()
, 然后再将整个
- HashMap
序列化后
- Map
到, 取缓存则反之. 由于需要在堆内申请
- MoveIn
, 因此可能会导致多次 Full GC. 这种方式虽然可以使用堆外内存, 但性能不高、无法发挥堆外内存的优势. 幸运的是开源界的前辈开发了诸如 Ehcache、MapDB、Chronicle Map 等一系列优秀的堆外内存框架, 使我们可以在使用简洁 API 访问堆外内存的同时又不损耗额外的性能.
- HashMap
- public class MapDBApp extends AbstractAppInvoker {
- private static HTreeMap mapDBCache;
- static {
- mapDBCache = DBMaker.hashMapSegmentedMemoryDirect()
- .expireMaxSize(SIZE)
- .make();
- }
- @Test
- @Override
- public void invoke(Object... param) {
- for (int i = 0; i < SIZE; ++i) {
- String key = "key-" + i;
- FeedDO feed = createFeed(i, key, System.currentTimeMillis());
- mapDBCache.put(key, feed);
- }
- System.out.println("write down");
- for (int i = 0; i < SIZE; ++i) {
- String key = "key-" + i;
- FeedDO feedDO = mapDBCache.get(key);
- checkValid(feedDO);
- if (i % 10000 == 0) {
- System.out.println("read " + i);
- }
- }
- }
- }
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 0.00 0.00 5.22 78.57 59.85 19 2.902 13 7.251 10.153
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 0.00 0.03 8.02 0.38 44.46 171 0.238 0 0.000 0.238
- java version "1.7.0_79"
- Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
- Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
- -Xmx512M
- -XX:MaxDirectMemorySize=512M
- -XX:+PrintGC
- -XX:+UseConcMarkSweepGC
- -XX:+CMSClassUnloadingEnabled
- -XX:CMSInitiatingOccupancyFraction=80
- -XX:+UseCMSInitiatingOccupancyOnly
- public class ConcurrentHashMapApp extends AbstractAppInvoker {
- private static final Map cache = new ConcurrentHashMap<>();
- @Test
- @Override
- public void invoke(Object... param) {
- // write
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());
- cache.put(key, feedDO);
- }
- System.out.println("write down");
- // read
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- FeedDO feedDO = cache.get(key);
- checkValid(feedDO);
- if (i % 10000 == 0) {
- System.out.println("read " + i);
- }
- }
- }
- }
- public class EhcacheApp extends AbstractAppInvoker {
- private static Cache cache;
- static {
- ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
- .heap(1000, EntryUnit.ENTRIES)
- .offheap(480, MemoryUnit.MB)
- .build();
- CacheConfiguration configuration = CacheConfigurationBuilder
- .newCacheConfigurationBuilder(String.class, FeedDO.class, resourcePools)
- .build();
- cache = CacheManagerBuilder.newCacheManagerBuilder()
- .withCache("cacher", configuration)
- .build(true)
- .getCache("cacher", String.class, FeedDO.class);
- }
- @Test
- @Override
- public void invoke(Object... param) {
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());
- cache.put(key, feedDO);
- }
- System.out.println("write down");
- // read
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- Object o = cache.get(key);
- checkValid(o);
- if (i % 10000 == 0) {
- System.out.println("read " + i);
- }
- }
- }
- }
- public class LocalRedisApp extends AbstractAppInvoker {
- private static final Jedis cache = new Jedis("localhost", 6379);
- private static final IObjectSerializer serializer = new Hessian2Serializer();
- @Test
- @Override
- public void invoke(Object... param) {
- // write
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());
- byte[] value = serializer.serialize(feedDO);
- cache.set(key.getBytes(), value);
- if (i % 10000 == 0) {
- System.out.println("write " + i);
- }
- }
- System.out.println("write down");
- // read
- for (int i = 0; i < SIZE; ++i) {
- String key = String.format("key_%s", i);
- byte[] value = cache.get(key.getBytes());
- FeedDO feedDO = serializer.deserialize(value);
- checkValid(feedDO);
- if (i % 10000 == 0) {
- System.out.println("read " + i);
- }
- }
- }
- }
* | ConcurrentMap | Guava |
---|---|---|
TTC | 32166ms/32s | 47520ms/47s |
Minor C/T | 31/1.522 | 29/1.312 |
Full C/T | 24/23.212 | 36/41.751 |
MapDB | Ehcache | |
TTC | 40272ms/40s | 30814ms/31s |
Minor C/T | 511/0.557 | 297/0.430 |
Full C/T | 0/0.000 | 0/0.000 |
LocalRedis | NetworkRedis | |
TTC | 176382ms/176s | 1h+ |
Minor C/T | 421/0.415 | - |
Full C/T | 0/0.000 | - |
- <dependency>
- <groupId>
- org.apache.commons
- </groupId>
- <artifactId>
- commons-proxy
- </artifactId>
- <version>
- ${commons.proxy.version}
- </version>
- </dependency>
- <dependency>
- <groupId>
- org.javassist
- </groupId>
- <artifactId>
- javassist
- </artifactId>
- <version>
- ${javassist.version}
- </version>
- </dependency>
- <dependency>
- <groupId>
- com.caucho
- </groupId>
- <artifactId>
- hessian
- </artifactId>
- <version>
- ${hessian.version}
- </version>
- </dependency>
- <dependency>
- <groupId>
- com.google.guava
- </groupId>
- <artifactId>
- guava
- </artifactId>
- <version>
- ${guava.version}
- </version>
- </dependency>
- <dependency>
- <groupId>
- junit
- </groupId>
- <artifactId>
- junit
- </artifactId>
- <version>
- ${junit.version}
- </version>
- </dependency>
- /**
- * @author jifang
- * @since 2017/1/1 上午10:47.
- */
- public class OffHeapStarter {
- private static final Map STATISTICS_MAP = new HashMap<>();
- public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
- Set> classes = PackageScanUtil.scanPackage("com.vdian.se.apps");
- for (Class clazz : classes) {
- AbstractAppInvoker invoker = createProxyInvoker(clazz.newInstance());
- invoker.invoke();
- //System.gc();
- }
- System.out.println("********************* statistics **********************");
- for (Map.Entry entry : STATISTICS_MAP.entrySet()) {
- System.out.println("method [" + entry.getKey() + "] total cost [" + entry.getValue() + "]ms");
- }
- }
- private static AbstractAppInvoker createProxyInvoker(Object invoker) {
- ProxyFactory factory = new JavassistProxyFactory();
- Class superclass = invoker.getClass().getSuperclass();
- Object proxy = factory
- .createInterceptorProxy(invoker, new ProfileInterceptor(), new Class[]{superclass});
- return (AbstractAppInvoker) proxy;
- }
- private static class ProfileInterceptor implements Interceptor {
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- Class clazz = invocation.getProxy().getClass();
- Method method = clazz.getMethod(invocation.getMethod().getName(), Object[].class);
- Object result = null;
- if (method.isAnnotationPresent(Test.class)
- && method.getName().equals("invoke")) {
- String methodName = String.format("%s.%s", clazz.getSimpleName(), method.getName());
- System.out.println("method [" + methodName + "] start invoke");
- long start = System.currentTimeMillis();
- result = invocation.proceed();
- long cost = System.currentTimeMillis() - start;
- System.out.println("method [" + methodName + "] total cost [" + cost + "]ms");
- STATISTICS_MAP.put(methodName, cost);
- }
- return result;
- }
- }
- }
- public class PackageScanUtil {
- private static final String CLASS_SUFFIX = ".class";
- private static final String FILE_PROTOCOL = "file";
- public static Set> scanPackage(String packageName) throws IOException {
- Set> classes = new HashSet<>();
- String packageDir = packageName.replace('.', '/');
- Enumeration packageResources = Thread.currentThread().getContextClassLoader().getResources(packageDir);
- while (packageResources.hasMoreElements()) {
- URL packageResource = packageResources.nextElement();
- String protocol = packageResource.getProtocol();
- // 只扫描项目内class
- if (FILE_PROTOCOL.equals(protocol)) {
- String packageDirPath = URLDecoder.decode(packageResource.getPath(), "UTF-8");
- scanProjectPackage(packageName, packageDirPath, classes);
- }
- }
- return classes;
- }
- private static void scanProjectPackage(String packageName, String packageDirPath, Set> classes) {
- File packageDirFile = new File(packageDirPath);
- if (packageDirFile.exists() && packageDirFile.isDirectory()) {
- File[] subFiles = packageDirFile.listFiles(new FileFilter() {
- @Override
- public boolean accept(File pathname) {
- return pathname.isDirectory() || pathname.getName().endsWith(CLASS_SUFFIX);
- }
- });
- for (File subFile : subFiles) {
- if (!subFile.isDirectory()) {
- String className = trimClassSuffix(subFile.getName());
- String classNameWithPackage = packageName + "." + className;
- Class clazz = null;
- try {
- clazz = Class.forName(classNameWithPackage);
- } catch (ClassNotFoundException e) {
- // ignore
- }
- assert clazz != null;
- Class superclass = clazz.getSuperclass();
- if (superclass == AbstractAppInvoker.class) {
- classes.add(clazz);
- }
- }
- }
- }
- }
- // trim .class suffix
- private static String trimClassSuffix(String classNameWithSuffix) {
- int endIndex = classNameWithSuffix.length() - CLASS_SUFFIX.length();
- return classNameWithSuffix.substring(0, endIndex);
- }
- }
- public abstract class AbstractAppInvoker {
- protected static final int SIZE = 170_0000;
- protected static final IObjectSerializer serializer = new Hessian2Serializer();
- protected static FeedDO createFeed(long id, String userId, long createTime) {
- return new FeedDO(id, userId, (int) id, userId + "_" + id, createTime);
- }
- protected static void free(ByteBuffer byteBuffer) {
- if (byteBuffer.isDirect()) {
- ((DirectBuffer) byteBuffer).cleaner().clean();
- }
- }
- protected static void checkValid(Object obj) {
- if (obj == null) {
- throw new RuntimeException("cache invalid");
- }
- }
- protected static void sleep(int time, String beforeMsg) {
- if (!Strings.isNullOrEmpty(beforeMsg)) {
- System.out.println(beforeMsg);
- }
- try {
- Thread.sleep(time);
- } catch (InterruptedException ignored) {
- // no op
- }
- }
- /**
- * 供子类继承 & 外界调用
- *
- * @param param
- */
- public abstract void invoke(Object... param);
- }
- public interface IObjectSerializer {
- byte[] serialize(T obj);
- T deserialize(byte[] bytes);
- }
- public class Hessian2Serializer implements IObjectSerializer {
- private static final Logger LOGGER = LoggerFactory.getLogger(Hessian2Serializer.class);
- @Override
- public byte[] serialize(T obj) {
- if (obj != null) {
- try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
- Hessian2Output out = new Hessian2Output(os);
- out.writeObject(obj);
- out.close();
- return os.toByteArray();
- } catch (IOException e) {
- LOGGER.error("Hessian serialize error ", e);
- throw new CacherException(e);
- }
- }
- return null;
- }
- @SuppressWarnings("unchecked")
- @Override
- public T deserialize(byte[] bytes) {
- if (bytes != null) {
- try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) {
- Hessian2Input in = new Hessian2Input(is);
- T obj = (T) in.readObject();
- in.close();
- return obj;
- } catch (IOException e) {
- LOGGER.error("Hessian deserialize error ", e);
- throw new CacherException(e);
- }
- }
- return null;
- }
- }
- # ! /bin/bash
- pid = `jps | grep $1 | awk' {
- print $1
- }'`jstat - gcutil $ {
- pid
- }
- 400 10000
- sh jstat-uti.sh ${u-main-class}
来源: