JVM初探——使用堆外内存减少Full GC
問題: 大部分主流互聯網企業線上Server JVM選用了CMS收集器(如Taobao、LinkedIn、Vdian), 雖然CMS可與用戶線程并發GC以降低STW時間, 但它也并非十分完美, 尤其是當出現Concurrent Mode Failure由并行GC轉入串行時, 將導致非常長時間的Stop The World(詳細可參考JVM初探- 內存分配、GC原理與垃圾收集器).
解決: 由GCIH可以聯想到:?將長期存活的對象(如Local Cache)移入堆外內存(off-heap, 又名直接內存/direct-memory), 從而減少CMS管理的對象數量, 以降低Full GC的次數和頻率, 達到提高系統響應速度的目的.
引入
這個idea最初來源于TaobaoJVM對OpenJDK定制開發的GCIH部分(詳見撒迦的分享-JVM定制改進@淘寶), 其中GCIH就是將CMS Old Heap區的一部分劃分出來, 這部分內存雖然還在堆內, 但已不被GC所管理.將長生命周期Java對象放在Java堆外, GC不能管理GCIH內Java對象(GC Invisible Heap):
(圖片來源: JVM@Taobao PPT)
- 這樣做有兩方面的好處:
- 減少GC管理內存:
由于GCIH會從Old區“切出”一塊, 因此導致GC管理區域變小, 可以明顯降低GC工作量, 提高GC效率, 降低Full GC STW時間(且由于這部分內存仍屬于堆, 因此其訪問方式/速度不變-?不必付出序列化/反序列化的開銷). - GCIH內容進程間共享:
由于這部分區域不再是JVM運行時數據的一部分, 因此GCIH內的對象可供對個JVM實例所共享(如一臺Server跑多個MR-Job可共享同一份Cache數據), 這樣一臺Server也就可以跑更多的VM實例.
(實際測試數據/圖示可下載撒迦分享PPT).
但是大部分的互聯公司不能像阿里這樣可以有專門的工程師針對自己的業務特點定制JVM, 因此我們只能”眼饞”GCIH帶來的性能提升卻無法”享用”. 但通用的JVM開放了接口可直接向操作系統申請堆外內存(ByteBuffer?or?Unsafe), 而這部分內存也是GC所顧及不到的, 因此我們可用JVM堆外內存來模擬GCIH的功能(但相比GCIH不足的是需要付出serialize/deserialize的開銷).
JVM堆外內存
在JVM初探 -JVM內存模型一文中介紹的Java運行時數據區域中是找不到堆外內存區域的:
因為它并不是JVM運行時數據區的一部分, 也不是Java虛擬機規范中定義的內存區域, 這部分內存區域直接被操作系統管理.
在JDK 1.4以前, 對這部分內存訪問沒有光明正大的做法: 只能通過反射拿到Unsafe類, 然后調用allocateMemory()/freeMemory()來申請/釋放這塊內存. 1.4開始新加入了NIO, 它引入了一種基于Channel與Buffer的I/O方式, 可以使用Native函數庫直接分配堆外內存, 然后通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內存的引用進行操作,?ByteBuffer提供了如下常用方法來跟堆外內存打交道:
| static ByteBuffer allocateDirect(int capacity) | Allocates a new direct byte buffer. |
| ByteBuffer put(byte b) | Relative put method (optional operation). |
| ByteBuffer put(byte[] src) | Relative bulk put method (optional operation). |
| ByteBuffer putXxx(Xxx value) | Relative put method for writing a Char/Double/Float/Int/Long/Short value (optional operation). |
| ByteBuffer get(byte[] dst) | Relative bulk get method. |
| Xxx getXxx() | Relative get method for reading a Char/Double/Float/Int/Long/Short value. |
| XxxBuffer asXxxBuffer() | Creates a view of this byte buffer as a Char/Double/Float/Int/Long/Short buffer. |
| ByteBuffer asReadOnlyBuffer() | Creates a new, read-only byte buffer that shares this buffer’s content. |
| boolean isDirect() | Tells whether or not this byte buffer is direct. |
| ByteBuffer duplicate() | Creates a new byte buffer that shares this buffer’s content. |
下面我們就用通用的JDK API來使用堆外內存來實現一個local cache.
示例1.: 使用JDK API實現堆外Cache
注: 主要邏輯都集中在方法invoke()內, 而AbstractAppInvoker是一個自定義的性能測試框架, 在后面會有詳細的介紹.
| 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 | /** ?* @author jifang ?* @since 2016/12/31 下午6:05. ?*/ public class DirectByteBufferApp extends AbstractAppInvoker { ????@Test ????@Override ????public void invoke(Object... param) { ????????Map<String, FeedDO> 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<String, FeedDO> 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<String, FeedDO> createInHeapMap(int size) { ????????long createTime = System.currentTimeMillis(); ????????Map<String, FeedDO> 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只能申請到一個類似一維數組的ByteBuffer, JDK并未提供基于堆外內存的實用數據結構實現(如堆外的Map、Set), 因此想要實現Cache的功能只能在write()時先將數據put()到一個堆內的HashMap, 然后再將整個Map序列化后MoveIn到DirectMemory, 取緩存則反之. 由于需要在堆內申請HashMap, 因此可能會導致多次Full GC. 這種方式雖然可以使用堆外內存, 但性能不高、無法發揮堆外內存的優勢.
幸運的是開源界的前輩開發了諸如Ehcache、MapDB、Chronicle Map等一系列優秀的堆外內存框架, 使我們可以在使用簡潔API訪問堆外內存的同時又不損耗額外的性能.
其中又以Ehcache最為強大, 其提供了in-heap、off-heap、on-disk、cluster四級緩存, 且Ehcache企業級產品(BigMemory Max?/?BigMemoryGo)實現的BigMemory也是Java堆外內存領域的先驅.
示例2: MapDB API實現堆外Cache
| 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 | public class MapDBApp extends AbstractAppInvoker { ????private static HTreeMap<String, FeedDO> 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); ????????????} ????????} ????} } |
結果 & 分析
- DirectByteBufferApp
| 1 2 | 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 |
- the last one jstat of?MapDBApp
| 1 2 | 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 |
運行DirectByteBufferApp.invoke()會發現有看到很多Full GC的產生, 這是因為HashMap需要一個很大的連續數組, Old區很快就會被占滿, 因此也就導致頻繁Full GC的產生.
而運行MapDBApp.invoke()可以看到有一個DirectMemory持續增長的過程, 但FullGC卻一次都沒有了.
實驗: 使用堆外內存減少Full GC
實驗環境
- java -version
| 1 2 3 | 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) |
- VM Options
| 1 2 3 4 5 6 7 | -Xmx512M -XX:MaxDirectMemorySize=512M -XX:+PrintGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly |
- 實驗數據
170W條動態(FeedDO).
實驗代碼
第1組: in-heap、affect by GC、no serialize
- ConcurrentHashMapApp
| 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 | public class ConcurrentHashMapApp extends AbstractAppInvoker { ????private static final Map<String, FeedDO> 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); ????????????} ????????} ????} } |
GuavaCacheApp類似, 詳細代碼可參考完整項目.
第2組: off-heap、not affect by GC、need serialize
- EhcacheApp
| 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 | public class EhcacheApp extends AbstractAppInvoker { ????private static Cache<String, FeedDO> cache; ????static { ????????ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder() ????????????????.heap(1000, EntryUnit.ENTRIES) ????????????????.offheap(480, MemoryUnit.MB) ????????????????.build(); ????????CacheConfiguration<String, FeedDO> 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); ????????????} ????????} ????} } |
MapDBApp與前同.
第3組: off-process、not affect by GC、serialize、affect by process communication
- LocalRedisApp
| 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 | 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); ????????????} ????????} ????} } |
RemoteRedisApp類似, 詳細代碼可參考下面完整項目.
實驗結果
| 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 | - |
備注:
- TTC: Total Time Cost 總共耗時
- C/T: Count/Time 次數/耗時(seconds)
結果分析
對比前面幾組數據, 可以有如下總結:
- 將長生命周期的大對象(如cache)移出heap可大幅度降低Full GC次數與耗時;
- 使用off-heap存儲對象需要付出serialize/deserialize成本;
- 將cache放入分布式緩存需要付出進程間通信/網絡通信的成本(UNIX Domain/TCP IP)
附:
off-heap的Ehcache能夠跑出比in-heap的HashMap/Guava更好的成績確實是我始料未及的O(∩_∩)O~, 但確實這些數據和堆內存的搭配導致in-heap的Full GC太多了, 當heap堆開大之后就肯定不是這個結果了. 因此在使用堆外內存降低Full GC前, 可以先考慮是否可以將heap開的更大.
附: 性能測試框架
在main函數啟動時, 掃描com.vdian.se.apps包下的所有繼承了AbstractAppInvoker的類, 然后使用Javassist為每個類生成一個代理對象: 當invoke()方法執行時首先檢查他是否標注了@Test注解(在此, 我們借用junit定義好了的注解), 并在執行的前后記錄方法執行耗時, 并最終對比每個實現類耗時統計.
- 依賴
| 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 | <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> |
啟動類:?OffHeapStarter
| 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 | /** ?* @author jifang ?* @since 2017/1/1 上午10:47. ?*/ public class OffHeapStarter { ????private static final Map<String, Long> STATISTICS_MAP = new HashMap<>(); ????public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException { ????????Set<Class<?>> 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<String, Long> 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; ????????} ????} } |
- 包掃描工具: PackageScanUtil
| 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 | public class PackageScanUtil { ????private static final String CLASS_SUFFIX = ".class"; ????private static final String FILE_PROTOCOL = "file"; ????public static Set<Class<?>> scanPackage(String packageName) throws IOException { ????????Set<Class<?>> classes = new HashSet<>(); ????????String packageDir = packageName.replace('.', '/'); ????????Enumeration<URL> 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<Class<?>> 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); ????} } |
注: 在此僅掃描項目目錄下的單層目錄的class文件, 功能更強大的包掃描工具可參考spring源代碼或Touch源代碼中的PackageScanUtil類.
AppInvoker基類:?AbstractAppInvoker
提供通用測試參數 & 工具函數.
| 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 | 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); } |
序列化/反序列化接口與實現
| 1 2 3 4 5 6 | public interface IObjectSerializer { ????<T> byte[] serialize(T obj); ????<T> T deserialize(byte[] bytes); } |
| 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 | public class Hessian2Serializer implements IObjectSerializer { ????private static final Logger LOGGER = LoggerFactory.getLogger(Hessian2Serializer.class); ????@Override ????public <T> 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> 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; ????} } |
完整項目地址:?https://github.com/feiqing/off-heap-tester.git.
GC統計工具
| 1 2 3 4 | #!/bin/bash pid=`jps | grep $1 | awk '{print $1}'` jstat -gcutil ${pid} 400 10000 |
- 使用
1 sh jstat-uti.sh ${u-main-class}
附加: 為什么在實驗中in-heap cache的Minor GC那么少?
現在我還不能給出一個確切地分析答案, 有的同學說是因為CMS Full GC會連帶一次Minor GC, 而用jstat會直接計入Full GC, 但查看詳細的GC日志也并未發現什么端倪. 希望有了解的同學可以在下面評論區可以給我留言, 再次先感謝了O(∩_∩)O~.
- by?攻城師@翡青
- Email: feiqing.zjf@gmail.com
- 博客:?攻城師-翡青?-?http://blog.csdn.net/zjf280441589
- 微博:?攻城師-翡青?-?http://weibo.com/u/3319050953
原文出處:?攻城師-翡青
from:?http://www.importnew.com/23186.html
總結
以上是生活随笔為你收集整理的JVM初探——使用堆外内存减少Full GC的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈WeakHashMap
- 下一篇: JDK7与JDK8中HashMap的实现