常见数据结构与算法整理总结（上） 常见数据结构与算法整理总结（下） Java - 集合框架完全解析 关于Java集合的小抄 Java - 虚拟机学习笔记1 自动内存管理机制 Java - 虚拟机学习笔记2 类加载机制 Java - 网络编程完全总结 Java - 数据库学习笔记 Java - 设计模式学习总结 Java - 并发编程必知必会 如何理解面向对象思想 NIO 入门 Java动态性之:反射机制(Reflection) Java动态性之:动态编译(DynamicCompile) Java动态性之:脚本引擎执行javascript代码 JVM核心机制 JVM的栈上分配与逃逸分析 深入 JVM 内核—原理、诊断与优化之: GC 算法与种类 Hadoop框架基础（一） Hadoop框架基础（二） 关于数据科学的笔试和面试(BAT) hive优化总结 hadoop面试45题 java面试基础 多线程面试 红黑树 Hadoop深入面试问答题 hadoop面试原理性问题 Java内存模型深入讲解 《深入理解Java虚拟机》学习笔记（九）（Java内存模型与线程） 十种排序算法及其比较（java实现） Java内存模型与volatile关键字解析 Storm 性能优化 15道使用频率极高的基础算法题 二叉树相关算法题 链表相关算法题 经典算法题每日演练——第四题 最长公共子序列 经典算法专题 Java性能优化指南1.8版，及唯品会的实战 优秀博客 花钱的年华 骊骅 为梦码一生
线程安全 多线程网站统计访问人数 使用锁，维护计数器的串行访问与安全性 多线程访问ArrayList public static List<Integer> numberList =new ArrayList<Integer>(); public static class AddToList implements Runnable{ int startnum=0; public AddToList(int startnumber){ startnum=startnumber; } @Override public void run() { int count=0; while(count<1000000){ numberList.add(startnum); startnum+=2; count++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new AddToList(0)); Thread t2=new Thread(new AddToList(1)); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()){ Thread.sleep(1); } System.out.println(numberList.size()); } 在ArrayList进行扩容的时候处于不可用状态,这时进行操作会发生IndexOutOfBoundException 对象头Mark Mark Word，对象头的标记，32位 描述对象的hash、锁信息，垃圾回收标记，年龄 指向锁记录的指针 指向monitor的指针 GC标记 偏向锁线程ID 偏向锁 大部分情况是没有竞争的，所以可以通过偏向来提高性能 所谓的偏向，就是偏心，即锁会偏向于当前已经占有锁的线程 将对象头Mark的标记设置为偏向，并将线程ID写入对象头Mark 只要没有竞争，获得偏向锁的线程，在将来进入同步块，不需要做同步 当其他线程请求相同的锁时，偏向模式结束 -XX:+UseBiasedLocking 默认启用 在竞争激烈的场合，偏向锁会增加系统负担 public static List<Integer> numberList =new Vector<Integer>(); public static void main(String[] args) throws InterruptedException { long begin=System.currentTimeMillis(); int count=0; int startnum=0; while(count<10000000){ numberList.add(startnum); startnum+=2; count++; } long end=System.currentTimeMillis(); System.out.println(end-begin); } 本例中，使用偏向锁，可以获得5%以上的性能提升 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -XX:-UseBiasedLocking 轻量级锁 BasicObjectLock 嵌入在线程栈中的对象 普通的锁处理性能不够理想，轻量级锁是一种快速的锁定方法。 如果对象没有被锁定 将对象头的Mark指针保存到锁对象中 将对象头设置为指向锁的指针（在线程栈空间中） lock->set_displaced_header(mark); if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } lock位于线程栈中,判断一个线程是否持有轻量级锁，只要判断对象头的指针，是否在线程的栈空间范围内 如果轻量级锁失败，表示存在竞争，升级为重量级锁（常规锁） 在没有锁竞争的前提下，减少传统锁使用OS互斥量产生的性能损耗 在竞争激烈时，轻量级锁会多做很多额外操作，导致性能下降 自旋锁 当竞争存在时，如果线程可以很快获得锁，那么可以不在OS层挂起线程，让线程做几个空操作（自旋） JDK1.6中-XX:+UseSpinning开启 JDK1.7中，去掉此参数，改为内置实现 如果同步块很长，自旋失败，会降低系统性能 如果同步块很短，自旋成功，节省线程挂起切换时间，提升系统性能 偏向锁，轻量级锁，自旋锁总结 不是Java语言层面的锁优化方法 内置于JVM中的获取锁的优化方法和获取锁的步骤 偏向锁可用会先尝试偏向锁 轻量级锁可用会先尝试轻量级锁 以上都失败，尝试自旋锁 再失败，尝试普通锁，使用OS互斥量在操作系统层挂起 减少锁持有时间 public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); } public void syncMethod2(){ othercode1(); synchronized(this){ mutextMethod(); } othercode2(); } 持有时间长，自旋容易失败 减小锁粒度 将大对象，拆成小对象，大大增加并行度，降低锁竞争 偏向锁，轻量级锁成功率提高 ConcurrentHashMap HashMap的同步实现 Collections.synchronizedMap(Map<K,V> m) 返回SynchronizedMap对象 public V get(Object key) { synchronized (mutex) {return m.get(key);} } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} } 粒度大，竞争激烈，偏向锁，轻量级锁失败概率就高 ConcurrentHashMap 若干个Segment ：Segment<K,V>[] segments Segment中维护HashEntry<K,V> put操作时 先定位到Segment，锁定一个Segment，执行put 在减小锁粒度后， ConcurrentHashMap允许若干个线程同时进入 减少锁粒度后，可能会带来什么负面影响呢？以ConcurrentHashMap为例，说明分割为多个 Segment后，在什么情况下，会有性能损耗？ 锁分离 * 根据功能进行锁分离 ReadWriteLock 读多写少的情况，可以提高性能 读写分离思想可以延伸，只要操作互不影响，锁就可以分离 LinkedBlockingQueue 队列 链表 take只作用于前端，put只作用于尾端,E入队时，只要将D.last=E,A出队时，只要head=head.next,从功能的角度做分离，功能不同，互补影响，就可以分,LinkedBlockingQueue实现中，可以使用takeLock和putLock两个锁 锁粗化 通常情况下，为了保证多线程间的有效并发，会要求每个线程持有锁的时间尽量短，即在使用完公共资源后，应该立即释放锁。只有这样，等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是，凡事都有一个度，如果对同一个锁不停的进行请求、同步和释放，其本身也会消耗系统宝贵的资源，反而不利于性能的优化 public void demoMethod(){ synchronized(lock){ //do sth. } //做其他不需要的同步的工作，但能很快执行完毕 synchronized(lock){ //do sth. } } public void demoMethod(){ //整合成一次锁请求 synchronized(lock){ //do sth. //做其他不需要的同步的工作，但能很快执行完毕 } } for(int i=0;i<CIRCLE;i++){ synchronized(lock){ } } synchronized(lock){ for(int i=0;i<CIRCLE;i++){ } } 锁消除 在即时编译器时，如果发现不可能被共享的对象，则可以消除这些对象的锁操作 public static void main(String args[]) throws InterruptedException { long start = System.currentTimeMillis(); for (int i = 0; i < CIRCLE; i++) { craeteStringBuffer("JVM", "Diagnosis"); } long bufferCost = System.currentTimeMillis() - start; System.out.println("craeteStringBuffer: " + bufferCost + " ms"); } public static String craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1);//同步操作 sb.append(s2); return sb.toString(); } 锁不是由程序员引入的，JDK自带的一些库，可能内置锁,栈上对象，不会被全局访问的，没有必要加锁 CIRCLE= 2000000 -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks createStringBuffer: 187 ms -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks createStringBuffer: 254 ms 无锁 锁是悲观的操作 无锁是乐观的操作 无锁的一种实现方式 CAS(Compare And Swap) 非阻塞的同步 CAS(V,E,N) 在应用层面判断多线程的干扰，如果有干扰，则通知线程重试 CAS算法的过程是这样：它包含3个参数CAS(V,E,N)。V表示要更新的变量，E表示预期值，N表示新值。仅当V值等于E值时，才会将V的值设为N，如果V值和E值不同，则说明已经有其他线程做了更新，则当前线程什么都不做。最后，CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的，它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时，只有一个会胜出，并成功更新，其余均会失败。失败的线程不会被挂起，仅是被告知失败，并且允许再次尝试，当然也允许失败的线程放弃操作。基于这样的原理，CAS操作即时没有锁，也可以发现其他线程对当前线程的干扰，并进行恰当的处理。 java.util.concurrent.atomic.AtomicInteger public final int getAndSet(int newValue) { for (;;) { int current = get(); if (compareAndSet(current, newValue)) return current; } } 设置新值，返回旧值 public final boolean compareAndSet(int expect, int update)更新成功返回true java.util.concurrent.atomic包使用无锁实现，性能高于一般的有锁操作
深入 JVM 内核—原理、诊断与优化之: Java堆分析 (八)
内存溢出(OOM)的原因 在JVM中，有哪些内存区间？ 堆,永久区,线程栈,直接内存 堆溢出 public static void main(String args[]){ ArrayList<byte[]> list=new ArrayList<byte[]>(); for(int i=0;i<1024;i++){ list.add(new byte[1024*1024]); } } 占用大量堆空间，直接溢出 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at geym.jvm.ch8.oom.SimpleHeapOOM.main(SimpleHeapOOM.java:14) 解决方法：增大堆空间，及时释放内存 永久区 生成大量的类 public static void main(String[] args) { for(int i=0;i<100000;i++){ CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap()); } } Caused by: java.lang.OutOfMemoryError: PermGen space [Full GC[Tenured: 2523K->2523K(10944K), 0.0125610 secs] 2523K->2523K(15936K), [Perm : 4095K->4095K(4096K)], 0.0125868 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] Heap def new generation total 4992K, used 89K [0x28280000, 0x287e0000, 0x2d7d0000) eden space 4480K, 2% used [0x28280000, 0x282966d0, 0x286e0000) from space 512K, 0% used [0x286e0000, 0x286e0000, 0x28760000) to space 512K, 0% used [0x28760000, 0x28760000, 0x287e0000) tenured generation total 10944K, used 2523K [0x2d7d0000, 0x2e280000, 0x38280000) the space 10944K, 23% used [0x2d7d0000, 0x2da46cf0, 0x2da46e00, 0x2e280000) compacting perm gen total 4096K, used 4095K [0x38280000, 0x38680000, 0x38680000) the space 4096K, 99% used [0x38280000, 0x3867fff0, 0x38680000, 0x38680000) ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000) rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000) 解决方法：增大Perm区,允许Class回收 Java栈溢出 这里的栈溢出指，在创建线程的时候，需要为线程分配栈空间，这个栈空间是向操作系统请求的，如果操作系统无法给出足够的空间，就会抛出OOM public static class SleepThread implements Runnable{ public void run(){ try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]){ for(int i=0;i<1000;i++){ new Thread(new SleepThread(),"Thread"+i).start(); System.out.println("Thread"+i+" created"); } } -Xmx1g -Xss1m Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread 解决方法：减少堆内存 减少线程栈大小 直接内存溢出 ByteBuffer.allocateDirect()无法从操作系统获得足够的空间 for(int i=0;i<1024;i++){ ByteBuffer.allocateDirect(1024*1024); System.out.println(i); System.gc(); } -Xmx1g -XX:+PrintGCDetails 解决方法：减少堆内存 有意触发GC 遇到内存溢出后，应该如何思考和处理问题？ MAT使用基础 Memory Analyzer（MAT） 基于Eclipse的软件 http://www.eclipse.org/mat/ 柱状图显示，显示每个类的使用情况，比如类的数量，所占空间等 显示支配树 在对象引用图中，所有指向对象B的路径都经过对象A，则认为对象A支配对象B.如果对象A是离对象B最近的一个支配对象，则认为对象A为对象B的直接支配者 支配者被回收被支配者也会被回收,可以了解一个对象被释放后可以回收多少空间 显示线程信息 显示堆总体信息，比如消耗最大的一些对象等 显示一个对象引用的对象 显示引用这个对象的对象 浅堆 深堆 浅堆 一个对象结构所占用的内存大小 JDK7后，String结构发生变化,但是向8字节对齐后，浅堆还是24字节 3个int类型以及一个引用类型合计占用内存3*4+4=16个字节。再加上对象头的8个字节，因此String对象占用的空间，即浅堆的大小是16+8=24字节 对象大小按照8字节对齐 浅堆大小和对象的内容无关，只和对象的结构有关 深堆 一个对象被GC回收后，可以真实释放的内存大小 只能通过对象访问到的（直接或者间接）所有对象的浅堆之和 （支配树） 可以看到，所有的Point实例浅堆和深堆的大小都是16字节。而dLine对象，浅堆为16字节，深堆也是16字节，这是因为dLine对象内的两个点f和g没有被设置为null，因此，即使dLine被回收，f和g也不会被释放。对象cLine内的引用对象d和e由于仅在cLine内还存在引用，因此只要cLine被释放，d和e必然也作为垃圾被回收，即d和e在cLine的保留集内，因此cLine的深堆为16*2+16=48字节。 使用Visual VM分析堆 java自带的多功能分析工具，可以用来分析堆Dump 类的柱状图 ，显示对象数量，总大小等 从类视图切换到实例试图，显示所有的实例 使用OQL查询 返回引用了(0,0)这个点的所有对象 Tomcat OOM分析案例 Tomcat OOM Tomcat 在接收大量请求时发生OOM，获取堆Dump文件，进行分析。 使用MAT打开堆 分析目的： 找出OOM的原因 推测系统OOM时的状态 给出解决这个OOM的方法 确认是否有大量session 9941/((1403324677648-1403324645728)/1000)= 320次/秒 结论，tomcat在32M内存下，承受每秒320次请求，持续31秒，合计9941次请求，导致OOM 解决方法： OOM由于保存session过多引起，可以考虑增加堆大小 如果应用允许，缩短session的过期时间，使得session可以及时过期，并回收
深入 JVM 内核—原理、诊断与优化之: 类加载器 (六)
class装载验证流程 加载 链接 验证 准备 解析 初始化 class装载验证流程 -加载 装载类的第一个阶段 取得类的二进制流 转为方法区数据结构 在Java堆中生成对应的java.lang.Class对象 class装载验证流程 -链接 验证 目的：保证Class流的格式是正确的 文件格式的验证 是否以0xCAFEBABE开头 版本号是否合理 元数据验证 是否有父类 继承了final类？ 非抽象类实现了所有的抽象方法 字节码验证 (很复杂) 运行检查 栈数据类型和操作码数据参数吻合 跳转指令指定到合理的位置 符号引用验证 常量池中描述类是否存在 访问的方法或字段是否存在且有足够的权限 class装载验证流程 -链接 准备 分配内存，并为类设置初始值 （方法区中） public static int v=1; 在准备阶段中，v会被设置为0 在初始化的中才会被设置为1 对于static final类型，在准备阶段就会被赋上正确的值 public static final int v=1; class装载验证流程 - 链接 解析 符号引用替换为直接引用 字符串引用对象不一定被加载 指针或者地址偏移量引用对象一定在内存 class装载验证流程 – 初始化 执行类构造器 static变量 赋值语句 static{}语句 子类的调用前保证父类的被调用 是线程安全的 Java.lang.NoSuchFieldError错误可能在什么阶段抛出? 什么是类装载器ClassLoader ClassLoader是一个抽象类 ClassLoader的实例将读入Java字节码将类装载到JVM中 ClassLoader可以定制，满足不同的字节码流获取方式 ClassLoader负责类装载过程中的加载阶段 JDK中ClassLoader默认设计模式 ClassLoader的重要方法 public Class<?> loadClass(String name) throws ClassNotFoundException 载入并返回一个Class protected final Class<?> defineClass(byte[] b, int off, int len) 定义一个类，不公开调用 protected Class<?> findClass(String name) throws ClassNotFoundException loadClass回调该方法，自定义ClassLoader的推荐做法 protected final Class<?> findLoadedClass(String name) 寻找已经加载的类 JDK中ClassLoader默认设计模式 – 分类 BootStrap ClassLoader （启动ClassLoader） Extension ClassLoader （扩展ClassLoader） App ClassLoader （应用ClassLoader/系统ClassLoader） Custom ClassLoader(自定义ClassLoader) 每个ClassLoader都有一个Parent作为父亲,启动ClassLoader除外 JDK中ClassLoader默认设计模式 – 协同工作 JDK中ClassLoader默认设计模式 public class HelloLoader { public void print(){ System.out.println("I am in apploader"); } } 放到D:/tmp/clz public class HelloLoader { public void print(){ System.out.println("I am in bootloader"); } } public class FindClassOrder { public static void main(String args[]){ HelloLoader loader=new HelloLoader(); loader.print(); } } 直接运行以上代码： I am in apploader 加上参数 -Xbootclasspath/a:D:/tmp/clz I am in bootloader 此时AppLoader中不会加载HelloLoader I am in apploader 在classpath中却没有加载 说明类加载是从上往下的 强制在apploader中加载 public static void main(String args[]) throws Exception { ClassLoader cl=FindClassOrder2.class.getClassLoader(); byte[] bHelloLoader=loadClassBytes("geym.jvm.ch6.findorder.HelloLoader"); Method md_defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class,int.class,int.class); md_defineClass.setAccessible(true); md_defineClass.invoke(cl, bHelloLoader,0,bHelloLoader.length); md_defineClass.setAccessible(false); HelloLoader loader = new HelloLoader(); System.out.println(loader.getClass().getClassLoader()); loader.print(); } -Xbootclasspath/a:D:/tmp/clz 在查找类的时候，先在底层的Loader查找，是从下往上的。Apploader能找到，就不会去上层加载器加载 能否只用反射，仿照上面的写法，将类注入启动ClassLoader呢？ JDK中ClassLoader默认设计模式 – 问题 双亲模式的问题： 顶层ClassLoader，无法加载底层ClassLoader的类 Java框架(rt.jar)如何加载应用的类？ javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface SPI 位于rt.jar 即接口在启动ClassLoader中。而SPI的实现类，在AppLoader。 JDK中ClassLoader默认设计模式 – 解决 Thread.setContextClassLoader() 上下文加载器 是一个角色 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题 基本思想是，在顶层ClassLoader中，传入底层ClassLoader的实例 static private Class getProviderClass(String className, ClassLoader cl, boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException { try { if (cl == null) { if (useBSClsLoader) { return Class.forName(className, true, FactoryFinder.class.getClassLoader()); } else { cl = ss.getContextClassLoader(); if (cl == null) { throw new ClassNotFoundException(); } else { return cl.loadClass(className); //使用上下文ClassLoader } } } else { return cl.loadClass(className); } } catch (ClassNotFoundException e1) { if (doFallback) { // Use current class loader - should always be bootstrap CL return Class.forName(className, true, FactoryFinder.class.getClassLoader()); } ….. 代码来自于javax.xml.parsers.FactoryFinder展示如何在启动类加载器加载AppLoader的类 上下文ClassLoader可以突破双亲模式的局限性 双亲模式的破坏 双亲模式是默认的模式，但不是必须这么做 Tomcat的WebappClassLoader 就会先加载自己的Class，找不到再委托parent OSGi的ClassLoader形成网状结构，根据需要自由加载Class 破坏双亲模式例子- 先从底层ClassLoader加载 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class re=findClass(name); if(re==null){ System.out.println(“无法载入类:”+name+“ 需要请求父加载器"); return super.loadClass(name,resolve); } return re; } protected Class<?> findClass(String className) throws ClassNotFoundException { Class clazz = this.findLoadedClass(className); if (null == clazz) { try { String classFile = getClassFile(className); FileInputStream fis = new FileInputStream(classFile); FileChannel fileC = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outC = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); 省略部分代码 fis.close(); byte[] bytes = baos.toByteArray(); clazz = defineClass(className, bytes, 0, bytes.length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return clazz; } OrderClassLoader myLoader=new OrderClassLoader("D:/tmp/clz/"); Class clz=myLoader.loadClass("geym.jvm.ch6.classloader.DemoA"); System.out.println(clz.getClassLoader()); System.out.println("==== Class Loader Tree ===="); ClassLoader cl=myLoader; while(cl!=null){ System.out.println(cl); cl=cl.getParent(); } java.io.FileNotFoundException: D:\tmp\clz\java\lang\Object.class (系统找不到指定的路径。) at java.io.FileInputStream.open(Native Method) ..... at geym.jvm.ch6.classloader.ClassLoaderTest.main(ClassLoaderTest.java:7) 无法载入类:java.lang.Object需要请求父加载器 geym.jvm.ch6.classloader.OrderClassLoader@18f5824 ==== Class Loader Tree ==== geym.jvm.ch6.classloader.OrderClassLoader@18f5824 sun.misc.Launcher$AppClassLoader@f4f44a sun.misc.Launcher$ExtClassLoader@1d256fa DemoA在ClassPath中，但由OrderClassLoader加载 因为先从OrderClassLoader加载，找不到Object，之后使用appLoader加载Object 如果OrderClassLoader不重载loadClass()，只重载findClass，那么程序输出为 sun.misc.Launcher$AppClassLoader@b23210 ==== Class Loader Tree ==== geym.jvm.ch6.classloader.OrderClassLoader@290fbc sun.misc.Launcher$AppClassLoader@b23210 sun.misc.Launcher$ExtClassLoader@f4f44a 热替换 当一个class被替换后，系统无需重启，替换的类立即生效 例子： geym.jvm.ch6.hot.CVersionA public class CVersionA { public void sayHello() { System.out.println("hello world! (version A)"); } } DoopRun 不停调用CVersionA . sayHello()方法，因此有输出： hello world! (version A) 在DoopRun 的运行过程中，替换CVersionA 为： public class CVersionA { public void sayHello() { System.out.println("hello world! (version B)"); } } 替换后， DoopRun 的输出变为 hello world! (version B) 资料
深入JVM内核—原理、诊断与优化之:GC参数(五)
串行收集器 最古老，最稳定 效率高 可能会产生较长的停顿 -XX:+UseSerialGC 新生代、老年代使用串行回收 新生代复制算法 老年代标记-压缩 0.844: [GC 0.844: [DefNew: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.0189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 老年代: 8.259: [Full GC 8.259: [Tenured: 43711K->40302K(43712K), 0.2960477 secs] 63350K->40302K(63360K), [Perm : 17836K->17836K(32768K)], 0.2961554 secs] [Times: user=0.28 sys=0.02, real=0.30 secs] 并行收集器 ParNew -XX:+UseParNewGC 新生代并行 老年代串行 Serial收集器新生代的并行版本 复制算法 多线程，需要多核支持 -XX:ParallelGCThreads 限制线程数量 多线程不一定快(看什么cpu),也会stop the world 0.834: [GC 0.834: [ParNew: 13184K->1600K(14784K), 0.0092203 secs] 13184K->1921K(63936K), 0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Parallel收集器 类似ParNew 新生代复制算法 老年代 标记-压缩 更加关注吞吐量 -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行 -XX:+UseParallelOldGC 使用Parallel收集器+ 并行老年代 1.500: [Full GC [PSYoungGen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K)] 30717K->30437K(62848K) [PSPermGen: 10943K->10928K(32768K)], 0.2902791 secs] [Times: user=1.44 sys=0.03, real=0.30 secs] 并行收集器 -XX:MaxGCPauseMills 最大停顿时间，单位毫秒 GC尽力保证回收时间不超过设定值 -XX:GCTimeRatio 0-100的取值范围 垃圾收集时间占总时间的比 默认99，即最大允许1%时间做GC 这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优 CMS收集器 Concurrent Mark Sweep 并发标记清除(与用户线程一起执行) 标记-清除算法 与标记-压缩相比 并发阶段会降低吞吐量 老年代收集器（新生代使用ParNew） -XX:+UseConcMarkSweepGC CMS运行过程比较复杂，着重实现了标记的过程，可分为 初始标记 根可以直接关联到的对象 速度快 并发标记（和用户线程一起） 主要标记过程，标记全部对象 重新标记 由于并发标记时，用户线程依然运行，因此在正式清理前，再做修正 并发清除（和用户线程一起） 基于标记结果，直接清理对象 1.662: [GC [1 CMS-initial-mark: 28122K(49152K)] 29959K(63936K), 0.0046877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 1.666: [CMS-concurrent-mark-start] 1.699: [CMS-concurrent-mark: 0.033/0.033 secs] [Times: user=0.25 sys=0.00, real=0.03 secs] 1.699: [CMS-concurrent-preclean-start] 1.700: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 1.700: [GC[YG occupancy: 1837 K (14784 K)]1.700: [Rescan (parallel) , 0.0009330 secs]1.701: [weak refs processing, 0.0000180 secs] [1 CMS-remark: 28122K(49152K)] 29959K(63936K), 0.0010248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 1.702: [CMS-concurrent-sweep-start] 1.739: [CMS-concurrent-sweep: 0.035/0.037 secs] [Times: user=0.11 sys=0.02, real=0.05 secs] 1.739: [CMS-concurrent-reset-start] 1.741: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 特点 尽可能降低停顿 会影响系统整体吞吐量和性能 比如，在用户线程运行过程中，分一半CPU去做GC，系统性能在GC阶段，反应速度就下降一半 清理不彻底 因为在清理阶段，用户线程还在运行，会产生新的垃圾，无法清理 因为和用户线程一起运行，不能在空间快满时再清理 -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值 如果不幸内存预留空间不够，就会引起concurrent mode failure 33.348: [Full GC 33.348: [CMS33.357: [CMS-concurrent-sweep: 0.035/0.036 secs] [Times: user=0.11 sys=0.03, real=0.03 secs] (concurrent mode failure): 47066K->39901K(49152K), 0.3896802 secs] 60771K->39901K(63936K), [CMS Perm : 22529K->22529K(32768K)], 0.3897989 secs] [Times: user=0.39 sys=0.00, real=0.39 secs] 使用串行收集器作为后备 有关碎片:标记-清除和标记-压缩 -XX:+ UseCMSCompactAtFullCollection Full GC后，进行一次整理 整理过程是独占的，会引起停顿时间变长 -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后，进行一次碎片整理 -XX:ParallelCMSThreads 设定CMS的线程数量 为减轻GC压力，我们需要注意些什么？ 软件如何设计架构 代码如何写 堆空间如何分配 GC参数整理 -XX:+UseSerialGC：在新生代和老年代使用串行收集器 -XX:SurvivorRatio：设置eden区大小和survivior区大小的比例 -XX:NewRatio:新生代和老年代的比 -XX:+UseParNewGC：在新生代使用并行收集器 -XX:+UseParallelGC ：新生代使用并行回收收集器 -XX:+UseParallelOldGC：老年代使用并行回收收集器 -XX:ParallelGCThreads：设置用于垃圾回收的线程数 -XX:+UseConcMarkSweepGC：新生代使用并行收集器，老年代使用CMS+串行收集器 -XX:ParallelCMSThreads：设定CMS的线程数量 -XX:CMSInitiatingOccupancyFraction：设置CMS收集器在老年代空间被使用多少后触发 -XX:+UseCMSCompactAtFullCollection：设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理 -XX:CMSFullGCsBeforeCompaction：设定进行多少次CMS垃圾回收后，进行一次内存压缩 -XX:+CMSClassUnloadingEnabled：允许对类元数据进行回收 -XX:CMSInitiatingPermOccupancyFraction：当永久区占用率达到这一百分比时，启动CMS回收 -XX:UseCMSInitiatingOccupancyOnly：表示只在到达阀值的时候，才进行CMS回收 Tomcat实例 环境 Tomcat 7 JSP 网站 测试网站吞吐量和延时 工具 JMeter 目的 让Tomcat有一个不错的吞吐量 系统结构 Jmeter 性能测试工具 建立10个线程，每个线程请求Tomcat 1000次 共10000次请求 JDK6：使用32M堆处理请求 参数： set CATALINA_OPTS=-server -Xloggc:gc.log -XX:+PrintGCDetails -Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M JDK6：使用最大堆512M堆处理请求 参数： set CATALINA_OPTS=-Xmx512m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails 结果：FULL GC很少，基本上是Minor GC 从16M增长到60M JDK6：使用最大堆512M堆处理请求 参数： set CATALINA_OPTS=-Xmx512m -Xms64m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 结果：GC压力原本不大，修改GC方式影响很小 JDK 6 set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails 减小堆大小，增加GC压力，使用Serial回收器 JDK 6 set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 减小堆大小，增加GC压力，使用并行回收器 JDK 6 set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M -* Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParNewGC 减小堆大小，增加GC压力，使用ParNew回收器 启动Tomcat 7 使用JDK6 不加任何参数启动测试 启动Tomcat 7 使用JDK7 不加任何参数启动测试 升级JDK可能会带来额外的性能提升！ 性能的根本在应用 GC参数属于微调 设置不合理，会影响性能，产生大的延时
深入JVM内核—原理、诊断与优化之:GC算法与种类(四)
GC的概念 Garbage Collection 垃圾收集 1960年 List 使用了GC Java中，GC的对象是堆空间和永久区 引用计数法 老牌垃圾回收算法 通过引用计算来回收垃圾 使用者 COM ActionScript3 Python 引用计数器的实现很简单，对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1，当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用 引用计数法的问题 引用和去引用伴随加法和减法，影响性能 很难处理循环引用 标记-清除 标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段：标记阶段和清除阶段。一种可行的实现是，在标记阶段，首先通过根节点，标记所有从根节点开始的可达对象。因此，未被标记的对象就是未被引用的垃圾对象。然后，在清除阶段，清除所有未被标记的对象。 标记-压缩 标记-压缩算法适合用于存活对象较多的场合，如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样，标记-压缩算法也首先需要从根节点开始，对所有可达对象做一次标记。但之后，它并不简单的清理未标记的对象，而是将所有的存活对象压缩到内存的一端。之后，清理边界外所有的空间。 标记压缩对标记清除而言，有什么优势呢？ 复制算法 与标记-清除算法相比，复制算法是一种相对高效的回收方法 不适用于存活对象较多的场合 如老年代 将原有的内存空间分为两块，每次只使用其中一块，在垃圾回收时，将正在使用的内存中的存活对象复制到未使用的内存块中，之后，清除正在使用的内存块中的所有对象，交换两个内存的角色，完成垃圾回收 复制算法的最大问题是：空间浪费 整合标记清理思想 -XX:+PrintGCDetails的输出 Heap def new generation total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000) eden space 12288K, 91% used [0x27e80000, 0x28975f20, 0x28a80000) from space 1536K, 0% used [0x28a80000, 0x28a80000, 0x28c00000) to space 1536K, 0% used [0x28c00000, 0x28c00000, 0x28d80000) tenured generation total 5120K, used 0K [0x28d80000, 0x29280000, 0x34680000) the space 5120K, 0% used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000) compacting perm gen total 12288K, used 142K [0x34680000, 0x35280000, 0x38680000) the space 12288K, 1% used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000) ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000) rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000) from 是两个小块的左面那个 to 是两个小块的右面那个 分代思想 依据对象的存活周期进行分类，短命对象归为新生代，长命对象归为老年代。 根据不同代的特点，选取合适的收集算法 少量对象存活，适合复制算法 大量对象存活，适合标记清理或者标记压缩 GC算法总结整理 引用计数 没有被Java采用 标记-清除 标记-压缩 复制算法 新生代 所有的算法，需要能够识别一个垃圾对象，因此需要给出一个可触及性的定义 可触及性 可触及的 从根节点可以触及到这个对象 可复活的 一旦所有引用被释放，就是可复活状态 因为在finalize()中可能复活该对象 不可触及的 在finalize()后，可能会进入不可触及状态 不可触及的对象不可能复活 可以回收 public class CanReliveObj { public static CanReliveObj obj; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("CanReliveObj finalize called"); obj = this; } @Override public String toString() { return "I am CanReliveObj"; } public static void main(String[] args) throws InterruptedException { obj = new CanReliveObj(); obj = null; // 可复活 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第二次gc"); obj = null; // 不可复活 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } } CanReliveObj finalize called obj 可用 第二次gc obj 是 null 经验：避免使用finalize()，操作不慎可能导致错误。 优先级低，何时被调用， 不确定 何时发生GC不确定 可以使用try-catch-finally来替代它 根 栈中引用的对象 方法区中静态成员或者常量引用的对象（全局对象） JNI方法栈中引用对象 Stop-The-World Java中一种全局暂停的现象 全局停顿，所有Java代码停止，native代码可以执行，但不能和JVM交互 多半由于GC引起 Dump线程 死锁检查 堆Dump GC时为什么会有全局停顿？ 类比在聚会时打扫房间，聚会时很乱，又有新的垃圾产生，房间永远打扫不干净，只有让大家停止活动了，才能将房间打扫干净。 危害 长时间服务停止，没有响应 遇到HA系统，可能引起主备切换，严重危害生产环境。 每秒打印10条 public class MyThread extends Thread { HashMap<Long, byte[]> map = new HashMap<Long, byte[]>(); @Override public void run() { try { while (true) { //大于450M时，清理内存 if (map.size() * 512 / 1024 / 1024 >= 450) { System.out.println("=====准备清理=====:" + map.size()); map.clear(); } for (int i = 0; i < 1024; i++) { map.put(System.nanoTime(), new byte[512]); } Thread.sleep(1); } } catch (Exception e) { e.printStackTrace(); } } } -Xmx512M -Xms512M -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails -Xmn1m -XX:PretenureSizeThreshold=50 -XX:MaxTenuringThreshold=1 预期，应该是每秒中有10条输出 time:2018 time:2121 time:2221 time:2325 time:2425 time:2527 time:2631 time:2731 time:2834 time:2935 time:3035 time:3153 time:3504 time:4218 ======before clean map=======:921765 time:4349 time:4450 time:4551 3.292: [GC3.292: [DefNew: 959K->63K(960K), 0.0024260 secs] 523578K->523298K(524224K), 0.0024879 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 3.296: [GC3.296: [DefNew: 959K->959K(960K), 0.0000123 secs]3.296: [Tenured: 523235K->523263K(523264K), 0.2820915 secs] 524195K->523870K(524224K), [Perm : 147K->147K(12288K)], 0.2821730 secs] [Times: user=0.26 sys=0.00, real=0.28 secs] 3.579: [Full GC3.579: [Tenured: 523263K->523263K(523264K), 0.2846036 secs] 524159K->524042K(524224K), [Perm : 147K->147K(12288K)], 0.2846745 secs] [Times: user=0.28 sys=0.00, real=0.28 secs] 3.863: [Full GC3.863: [Tenured: 523263K->515818K(523264K), 0.4282780 secs] 524042K->515818K(524224K), [Perm : 147K->147K(12288K)], 0.4283353 secs] [Times: user=0.42 sys=0.00, real=0.43 secs] 4.293: [GC4.293: [DefNew: 896K->64K(960K), 0.0017584 secs] 516716K->516554K(524224K), 0.0018346 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ……省略若干….. 4.345: [GC4.345: [DefNew: 960K->960K(960K), 0.0000156 secs]4.345: [Tenured: 522929K->12436K(523264K), 0.0781624 secs] 523889K->12436K(524224K), [Perm : 147K->147K(12288K)], 0.0782611 secs] [Times: user=0.08 sys=0.00, real=0.08 secs]
深入JVM内核—原理、诊断与优化之:常用JVM配置参数(三)
Trace跟踪参数 -verbose:gc -XX:+printGC 可以打印GC的简要信息 [GC 4790K->374K(15872K), 0.0001606 secs] [GC 4790K->374K(15872K), 0.0001474 secs] [GC 4790K->374K(15872K), 0.0001563 secs] [GC 4790K->374K(15872K), 0.0001682 secs] GC前使用的内存->GC后使用的内存 -XX:+PrintGCDetails 打印GC详细信息 -XX:+PrintGCTimeStamps 打印GC发生的时间戳 例 [GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] XX:+PrintGCDetails的输出(程序结束后打印堆的情况) Heap total = 12288K + 1536K ---------------------- 低边界(开始位置) 当前位置 最高边界(结束位置) def new generation total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000)(新生代) eden space 12288K, 91% used [0x27e80000, 0x28975f20, 0x28a80000)(伊甸园) from space 1536K, 0% used [0x28a80000, 0x28a80000, 0x28c00000)(两个幸存代内存大小一样) to space 1536K, 0% used [0x28c00000, 0x28c00000, 0x28d80000) tenured generation total 5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)(老年代) the space 5120K, 0% used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000) compacting perm gen total 12288K, used 142K [0x34680000, 0x35280000, 0x38680000)(永久区) the space 12288K, 1% used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000) ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)(只读共享区,JDK 5.0之后有一个永久区的共享,基础类会被加载到共享区,供所有的JVM虚拟机使用) rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)(可读可写的共享区) -Xloggc:log/gc.log 指定GC log的位置,以文件输出 帮助开发人员分析问题 -XX:+PrintHeapAtGC 每次GC前后,都打印堆信息 { Heap before GC invocations=0 (full 0): def new generation total 3072K, used 2752K [0x33c80000, 0x33fd0000, 0x33fd0000) eden space 2752K, 100% used [0x33c80000, 0x33f30000, 0x33f30000) from space 320K, 0% used [0x33f30000, 0x33f30000, 0x33f80000) to space 320K, 0% used [0x33f80000, 0x33f80000, 0x33fd0000) tenured generation total 6848K, used 0K [0x33fd0000, 0x34680000, 0x34680000) the space 6848K, 0% used [0x33fd0000, 0x33fd0000, 0x33fd0200, 0x34680000) compacting perm gen total 12288K, used 143K [0x34680000, 0x35280000, 0x38680000) the space 12288K, 1% used [0x34680000, 0x346a3c58, 0x346a3e00, 0x35280000) ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000) rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000) [GC[DefNew: 2752K->320K(3072K), 0.0014296 secs] 2752K->377K(9920K), 0.0014604 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap after GC invocations=1 (full 0): def new generation total 3072K, used 320K [0x33c80000, 0x33fd0000, 0x33fd0000) eden space 2752K, 0% used [0x33c80000, 0x33c80000, 0x33f30000) from space 320K, 100% used [0x33f80000, 0x33fd0000, 0x33fd0000) to space 320K, 0% used [0x33f30000, 0x33f30000, 0x33f80000) tenured generation total 6848K, used 57K [0x33fd0000, 0x34680000, 0x34680000 the space 6848K, 0% used [0x33fd0000, 0x33fde458, 0x33fde600, 0x34680000) compacting perm gen total 12288K, used 143K [0x34680000, 0x35280000, 0x38680000) the space 12288K, 1% used [0x34680000, 0x346a3c58, 0x346a3e00, 0x35280000) ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000) rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000) } -XX:+TraceClassLoading 监控类的加载 [Loaded java.lang.Object from shared objects file] [Loaded java.io.Serializable from shared objects file] [Loaded java.lang.Comparable from shared objects file] [Loaded java.lang.CharSequence from shared objects file] [Loaded java.lang.String from shared objects file] [Loaded java.lang.reflect.GenericDeclaration from shared objects file] [Loaded java.lang.reflect.Type from shared objects file] -XX:PrintClassHistogram 按下Ctrl+Break后，打印类的信息: num #instances #bytes class name ---------------------------------------------- 1: 890617 470266000 [B 2: 890643 21375432 java.util.HashMap$Node 3: 890608 14249728 java.lang.Long 4: 13 8389712 [Ljava.util.HashMap$Node; 5: 2062 371680 [C 6: 463 41904 java.lang.Class 分别显示：序号、实例数量、总大小、类型 堆的分配参数 -Xmx –Xms 指定最大堆(最多能使用的空间)和最小堆(至少使用的空间) -Xmx20m -Xms5m 运行代码： System.out.print("Xmx="); System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M"); System.out.print("free mem="); System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M"); //当前分配的空间 System.out.print("total mem="); System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M"); Xmx=19.375M free mem=4.342750549316406M total mem=4.875M byte[] b=new byte[110241024]; System.out.println("分配了1M空间给数组"); System.out.print("Xmx="); System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M"); System.out.print("free mem="); System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M"); System.out.print("total mem="); System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M"); 分配了1M空间给数组 Xmx=19.375M free mem=3.4791183471679688M total mem=4.875M Java会尽可能维持在最小堆 b=new byte[410241024]; System.out.println("分配了4M空间给数组"); System.out.print("Xmx="); System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M"); System.out.print("free mem="); System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M"); System.out.print("total mem="); System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M"); 分配了4M空间给数组 Xmx=19.375M free mem=3.5899810791015625M total mem=9.00390625M 总内存变多了 System.gc(); System.out.println("回收内存"); System.out.print("Xmx="); System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M"); System.out.print("free mem="); System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M"); System.out.print("total mem="); System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M"); 回收内存 Xmx=19.375M free mem=6.354591369628906M total mem=10.75390625M 空闲内存增多 -Xmx 和 –Xms 应该保持一个什么关系，可以让系统的性能尽可能的好呢？ 如果你要做一个Java的桌面产品，需要绑定JRE，但是JRE又很大，你如何做一下JRE的瘦身呢？ -Xmn * 设置新生代大小 绝对值 -XX:NewRatio 新生代（eden+2*s）和老年代（不包含永久区）的比值 4 表示 新生代:老年代=1:4，即年轻代占堆的1/5 百分比 -XX:SurvivorRatio 设置两个Survivor区和eden的比 8表示 两个Survivor :eden=2:8，即一个Survivor占年轻代的1/10 例子 public static void main(String[] args) { byte[] b=null; for(int i=0;i<10;i++) b=new byte[1*1024*1024]; } -Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails 没有触发GC 新生代存不下 全部分配在老年代 -Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails 没有触发GC 全部分配在eden 老年代没有使用 -Xmx20m -Xms20m –Xmn7m -XX:+PrintGCDetails 进行了2次新生代GC s0 s1 太小需要老年代担保 -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails 进行了3次新生代GC s0 s1 增大 -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails 幸存代太大会造成浪费 -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError OOM时导出堆到文件 -XX:+HeapDumpPath 导出OOM的路径 -Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump Vector v=new Vector(); for(int i=0;i<25;i++) v.add(new byte[1*1024*1024]); -XX:OnOutOfMemoryError 在OOM时，执行一个脚本 "-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p" printstack.bat --> D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt 当程序OOM时，在D:/a.txt中将会生成线程的dump 可以在OOM时，发送邮件，甚至是重启程序 堆的分配参数 – 总结 根据实际事情调整新生代和幸存代的大小 官方推荐新生代占堆的3/8 幸存代占新生代的1/10 在OOM时，记得Dump出堆，确保可以排查现场问题 永久区分配参数 -XX:PermSize -XX:MaxPermSize * 设置永久区的初始空间和最大空间 他们表示，一个系统可以容纳多少个类型 使用CGLIB等库的时候，可能会产生大量的类，这些类，有可能撑爆永久区导致OOM for(int i=0;i<100000;i++){ //不断产生新的类 CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap()); } 打开堆的Dump 堆空间实际占用非常少 但是永久区溢出 一样抛出OOM 如果堆空间没有用完也抛出了OOM，有可能是永久区导致的 栈大小分配 -Xss 通常只有几百K 决定了函数调用的深度 每个线程都有独立的栈空间 局部变量、参数 分配在栈上 public class TestStackDeep { private static int count=0; public static void recursion(long a,long b,long c){ long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10; count++; recursion(a,b,c); } public static void main(String args[]){ try{ recursion(0L,0L,0L); }catch(Throwable e){ System.out.println("deep of calling = "+count); e.printStackTrace(); } } } 递归调用 -Xss128K deep of calling = 701 java.lang.StackOverflowError -Xss256K deep of calling = 1817 java.lang.StackOverflowError
深入JVM内核—原理、诊断与优化之:初识JVM(一)
JVM概念 JVM是Java Virtual Machine的简称.意为Java虚拟机 虚拟机 指通过软件模拟的具有完整硬件系统功能的,运行在一个完全隔离环境中的完整计算机系统 有哪些虚拟机 VMWare Visual Box JVM VMWare或者Visual Box都是使用软件模拟物理CPU指令集 JVM使用软件模拟Java字节码的指令集 Java和JVM的历史 1996年SUN JDK 1.0 Classic VM 纯解释运行,使用外挂进行JIT 1997年 JDK 1.1 发布 AWT,内部类,JDBC,RMI,反射 1998年 JDK 1.2 Solaris Exact VM JIT 解释器混合 Accurate Memory Management 精确内存管理,数据类型敏感 提升了GC性能 JDK 1.2 开始成为Java2 J2SE J2EE J2ME出现 加入Swing Collections 2000年 JDK 1.3 Hotspot 作为默认虚拟机发布 加入JavaSound 2002年 JDK 1.4 Classic VM 退出历史舞台 Assert 正则表达式 NIO IPV6 日志API 加密类库 2004年发布 JDK1.5 即JDK5,J2SE 5,Java 5 泛型 注解 装箱 枚举 可变长的参数 Foreach循环 JDK1.6 JDK6 脚本语言支持 JDBC 4.0 Java编译器 API 2011年 JDK7发布 由于JDK7加入内容太多,项目规划出现问题直到2011年才发布,延误项目推出到JDK8 G1垃圾回收器 动态语言增强 64位系统中的压缩指针 NIO 2.0 2014年 JDK8 发布 Lambda表达式 Stream 语法增强 Java类型注解 2016年 JDK9 模块化 Java + REPL = jshell 简化进程API 轻量化JSON API 货币API 改善竞争锁 分割代码缓存区 Smart Java Compilation项目 大事记 使用最为广泛的JVM为HotSpot HotSpot为Longview Technologies开发 被SUN收购 2006年Java开源并建立OpenJDK HotSpot成为SUN JDK和Open JDK中所带的虚拟机 2008年Oracle收购BEA 得到JRockit VM 2010年Oracle收购SUN 得到Hotspot Oracle宣布在JDK8时整合JRockit和Hotspot,优势互补 在Hotspot基础上,移植JRockit优秀特性 各式JVM KVM Sun发布 IOS Android前,广泛用于手机系统 CDC/CLDC HotSpot 手机,电子书,Pad等电子设备上建立统一的Java编程接口 J2ME的重要组成部分 JRockit BEA IBM J9 VM IBM内部 Apache Harmony 兼容与JDK 1.5和JDK 1.6的Java程序运行平台 与Oracle关系恶劣 退出JCP Java社区的分裂 OpenJDK出现后,受到挑战,2011年退役 没有大规模商用经历 对Android的发展有积极作用 规范 Java语言规范 语法 变量 类型 文法 JVM规范 Class文件类型 运行时数据 帧栈 虚拟机的启动 虚拟机的指令集 Java语言规范 语法定义 IfThenStatement: if( Expression ) Statement ArgumentList: Argument ArgumentList,Argument 词法结构 \u + 4个16进制数字 表示UTF-16 行终结符:CR or LF,or CR LF 空白符 空格 tab \t 换页 \f 行终结符 注释 标识符(变量、类、方法等命名的符号) 关键字 int 0 2 0372 0xData_Cafe 1996 0x00_FF__00_FF 从JDK 1.7开始允许使用下划线分割数字 long 0l 0777L 0x1000000000L 2_17_483_68L 0xC0B0L float 1e1f 2.f .3f 0f 3.1444f 6.022137e+23f double 1e1 2. .3 0.0 3.14 1e-9d 1e137 操作 += -= *= /= &= |= ^= %= <<= >>= >>>= 类型和变量 元类型 byte short int long float char 变量初始化 boolean false char \u0000 泛型 JVM规范 Java语言规范定义了什么是Java语言 Java语言和JVM行对独立 Gloovy Clojure Scala JVM主要定义二进制class文件和JVM指令集 Class文件格式 数字的内部表示和存储 byte -128 to 127 (-2^7 to 2^7 -1) returnAddress 数据类型定义 指向操作字节码的指针.不对应Java数据类型,不能在运行时修改.Finally实现需要 定义PC(指令集寄存器) 堆 栈 方法区 整数的表达 原码:第一位为符号位(0位正数,1为负数) 反码:符号位不动,原码取反 负数补码:符号位不动,反码加1 正数补码:和原码相同 打印整数的二进制表示 int a = -6; for(int i=0;i<32;i++){ int t = (a & 0x80000000 >>> i)>>>(31-i); System.out.print(t); } 为什么要用补码 计算0的表示 按照0是正数:00000000,负数:10000000,补码都为00000000没有歧义 计算加分法直接用补码相加就可以了 float的表示与定义 支持IEEE 754 s eeeeeeee mmmmmmmmmmmmmmmmmmmmm s为符号位,e为指数位(8位),m为尾数(23位) e全0 尾数附加位为0 否则尾数附加位为1 sm2^(e-127) -5 1100000010100000000000000000000000 -12^(129-127)(2^0+2^-2) 一些特殊的方法 <clint> 类的初始化方法,只有在类初始化的时候调用一次 <init> 每初始化一个实例调用一次 VM指令集 类型转换 l2i(long-->int) 出栈入栈操作 aload astore 运算 iadd isub 流程控制 ifeq ifne 函数调用 invokevirtual invokeinterface invokespecial invokestatic JVM需要对Java Library提供以下支持 反射 java.lang.reflect ClassLoader 初始化class和interface 安全相关 java.security 多线程 弱引用 JVM的编译 源码到JVM指令对应格式 javap(反编译器) JVM反汇编格式 <index><opcode>[<operand1>[operand2...]][<comment>] (index:索引,opcode:操作码,operand:操作数,comment:注释) 实例 void spin(){ int i; for(i=0;i<100;i++){ ; //Loop body is empty } } 0 iconst_0 //Push int constant 0 1 istore_1 //Store into local variable 1 (i=0) 2 goto 8 //First time throught don't increment 5 iinc 1 1 //Increment local variable 1 by 1(i++) 8 iload_1 //Push local variable 1(i) 9 bipush 100 //Push int constant 100 11 if_icmplt 5 //Compare and loop if less than(i<100) 14 return //Return void when done
1.类加载全过程 为什么研究类加载全过程 有助于了解JVM运行过程 更深入了解java动态性(热部署,动态加载),提高程序的灵活性 类加载机制 JVM把class文件加载到内存,并对数据进行校验,解析和初始化,最终形成JVM可以直接使用的java类型的全过程 加载-->链接(验证,准备,解析)-->初始化-->使用-->卸载 加载 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中运行时结构,在堆中生产一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与 字节码可以是本地的也可以是从网络获取的,数据库获取的,等等 链接 验证 确保加载的类信息符合JVM规范,没有安全方面的问题 准备 正式为类变量(static变量)分配内存并设置类变量初始化的阶段,这些内存都将在方法区中进行分配(赋的是默认值,真正赋你定义的值在初始化阶段) 解析 虚拟机常量池内的符号引用替换为直接引用的过程(比如类名,方法名,变量名,类的代码,字符串常量等) 初始化 初始化阶段是执行类构造器\<clinit>()方法的过程,类构造器\<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先初始化其父类 虚拟机会保证一个类的\<clinit>()方法在多线程环境中被正确加锁和同步 当访问一个java类的静态域(变量)的时候,只有真正声明这个域的类才会被初始化 类的主动引用(一定会发生类的初始化) new一个类的对象 调用类的静态成员(除了final常量)和静态方法 使用java.lang.reflect包的方法对类进行反射调用 当虚拟机启动,一定会初始化main方法所在的类 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类 类的被动引用(不会发生类的初始化) 当访问一个静态域时,只有真正声明这个域的类才会被初始化 通过子类引用父类的静变量,不会导致子类初始化 通过数组定义类引用,不会触发此类的初始化 引用常量不会触发此类的初始化(常量在编译阶段存入调用类的常量池中了) Demo: public class Demo01{ public static void main(String[] args)throws Exception{ //主动引用 A a = new A(); System.out.println(A.width); Class.forName("A"); //被动调用 System.out.println(A.MAX); A[] as = new A[10]; System.out.println(B.width) } } class B extends A{ static{ System.out.println("静态初始化B类"); } } class A extends A_Father{ public static int width = 100; public static final int MAX = 100; static{ System.out.println("静态初始化A类"); width = 300; } public A(){ System.out.println("创建A类的对象"); } } class A_Father{ static{ System.out.println("静态初始化A_Father"); } } 2.类加载器作用 类加载器的作用 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口 类缓存 标准的Java SE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间.不过JVM垃圾收集器可以回收这些Class对象 3.java.classClassLoader类介绍 作用 java.class.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生产其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例 除此之外ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等 相关方法 getParent() 返回该类加载器的父类加载器 loadClass(String name) 加载名称为name的类,返回结果是java.lang.Class类的实例 findClass(String name) 查找名称为name的类,没找到则加载,返回的结果是java.lang.Class类的实例 findLoadedClass(String name) 查找名称为name已经被加载的类,返回的结果是java.lang.Class类的实例 defineClass(String name,byte[] b,int off,int len) 把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例,这个方法被声明为final的 resolveClass(Class<?> c) 链接指定的Java类 对于以上给出的方法,表示类名的name参数的值是类的二进制名称.需要注意的是内部类的表示 4.类加载器的层次结构(树状结构) 引导类加载器(bootstart class loader) 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径内容),是用原生代码(C++)来实现的,并不继承自java.lang.ClassLoader 加载扩展类和应用程序类加载器.并指定他们的父类加载器 扩展类加载器(extensions class loader) 用来加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)Java虚拟机的实现会提供一个扩展库目录.该类加载器在此目录里面查找并加载Java类 由sun.misc.Launcher$ExtClassLoader实现 应用程序类加载器(application class loader) 它根据Java应用的类路径(classpath,java.class.path路径)一般来说Java应用的类都是由它来完成加载的 由sun.misc.Launcher$APPClassLoader实现 自定义类加载器 开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求 5.类加载器的代理模式 代理模式 交给其他加载器来加载指定的类 双亲委托机制 某个特定的类加载器在接到类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类加载任务,就返回成功.只有父类加载器无法完成此加载任务时,自己才去加载 双亲委托机制是为了保证Java核心库的类型安全,这种机制是为了保证不会出现用户自己能定义java.lang.Object类的情况 类加载器除了用于加载类,也是安全的最基本的屏障 双亲委托机制是代理模式的一种,并不是所有的类加载器都采用双亲委托机制,tomcat服务器类加载器也使用代理模式,所不同的是它首先尝试去加载某个类,如果找不到再代理给父类加载器.这与一般类加载器的顺序是相反的 6.自定义类加载器 继承java.lang.ClassLoader 检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回 委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例 调用本类加载器的findClass()方法,试图获取对应得字节码,如果获取到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),loadClass()抛出异常,终止加载过程 注意:被两个类加载器加载的同一个类,JVM不认为是相同的类 文件类加载器 网络类加载器 加密解密类加载器(取反操作,DES对称加密解密) Demo: 本地类存放在D:/TEST/com/lorinda/matrix42/Helloworld.class package com.lorinda.matrix42; public class Helloworld{ public static void main(String[] args){ System.out.println("aaa"); } } 文件类加载器: package com.lorinda.jvm; import java.io.InputStream; import java.io.FileInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class FileSystemClassLoader extends ClassLoader{ private String rootDir; public FileSystemClassLoader(String rootDir){ this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException{ Class<?> c = findLoadedClass(name); //应该先查询有没有加载过这个类,如果已经加载,则直接返回加载好的类,如果没有,则加载新的类 if(c!=null){ return c; }else{ ClassLoader parent = this.getParent(); try{ c = parent.loadClass(name); }catch(ClassNotFoundException e){ } if(c!=null){ return c; }else{ byte[] classDate = getClassData(name); if(classDate==null){ throw new ClassNotFoundException(); }else{ c = defineClass(name,classDate,0,classDate.length); } } } return c; } private byte[] getClassData(String classname){ String path = rootDir+"/"+classname.replace('.','/')+".class"; //System.out.println(path); //IOUtils 可以使用它将流中的数据转换成字节数组 InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ is = new FileInputStream(path); byte[] buffer = new byte[1024]; int temp = 0; while((temp=is.read(buffer))!=-1){ baos.write(buffer,0,temp); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); }finally{ try{ if(is!=null){ is.close(); } }catch(IOException e){ e.printStackTrace(); } try{ if(baos!=null){ baos.close(); } }catch(IOException e){ e.printStackTrace(); } } return null; } } main: package com.lorinda.jvm; public class Demo{ public static void main(String[] args) throws ClassNotFoundException{ FileSystemClassLoader loader = new FileSystemClassLoader("D:/TEST"); FileSystemClassLoader loader2 = new FileSystemClassLoader("D:/TEST"); Class<?> c = loader.loadClass("com.lorinda.matrix42.Helloworld"); Class<?> c2 = loader.loadClass("com.lorinda.matrix42.Helloworld"); Class<?> c3 = loader2.loadClass("com.lorinda.matrix42.Helloworld"); Class<?> c4 = loader2.loadClass("java.lang.String"); Class<?> c5 = loader2.loadClass("com.lorinda.jvm.Demo"); System.out.println(c.hashCode()); System.out.println(c2.hashCode()); System.out.println(c3.hashCode()); //加载器不一样,JVM认为是不同的类 System.out.println(c3.getClassLoader()); System.out.println(c4.getClassLoader()); //null 引导类加载器 System.out.println(c5.getClassLoader()); /* 1808253012 1808253012 589431969 com.lorinda.jvm.FileSystemClassLoader@42a57993 null sun.misc.Launcher$AppClassLoader@73d16e93 */ } } 网络类加载器 import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; public class NetClassLoader extends ClassLoader{ private String rootUrl; public NetClassLoader(String rootUrl){ this.rootUrl = rootUrl; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException{ Class<?> c = findLoadedClass(name); //应该先查询有没有加载过这个类,如果已经加载,则直接返回加载好的类,如果没有,则加载新的类 if(c!=null){ return c; }else{ ClassLoader parent = this.getParent(); try{ c = parent.loadClass(name); }catch(ClassNotFoundException e){ } if(c!=null){ return c; }else{ byte[] classDate = getClassData(name); if(classDate==null){ throw new ClassNotFoundException(); }else{ c = defineClass(name,classDate,0,classDate.length); } } } return c; } private byte[] getClassData(String classname){ String path = rootUrl+"/"+classname.replace('.','/')+".class"; //System.out.println(path); //IOUtils 可以使用它将流中的数据转换成字节数组 InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ URL url = new URL(path); is = url.openStream(); byte[] buffer = new byte[1024]; int temp = 0; while((temp=is.read(buffer))!=-1){ baos.write(buffer,0,temp); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); }finally{ try{ if(is!=null){ is.close(); } }catch(IOException e){ e.printStackTrace(); } try{ if(baos!=null){ baos.close(); } }catch(IOException e){ e.printStackTrace(); } } return null; } } 加密解密类加载器(取反操作,DES对称加密解密) /** * 测试简单加密解密(取反)操作 * @author Matrix42 * */ public class Demo03 { public static void main(String[] args) throws ClassNotFoundException { //测试取反操作 // int a = 3; //00000011 // System.out.println(Integer.toBinaryString(a^0xff)); //加密后的class文件,正常的类加载器无法加载,报classFormatError // FileSystemClassLoader loader = new FileSystemClassLoader("D:/TEST/com/lorinda/matrix42/tmp"); //Class<?> c = loader.loadClass("Helloworld"); //System.out.println(c); DecriptClassLoader loader = new DecriptClassLoader("D:/TEST/"); Class<?> c = loader.loadClass("com.lorinda.matrix42.Helloworld"); System.out.println(c); } } import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class EncrptUtil { public static void main(String[] args) { encrpt("D:/TEST/com/lorinda/matrix42/Helloworld.class", "D:/TEST/com/lorinda/matrix42/tmp/Helloworld.class"); } public static void encrpt(String src,String dest){ FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(src); fos = new FileOutputStream(dest); int temp = -1; while((temp=fis.read())!=-1){ fos.write(temp^0xff); } } catch (Exception e) { e.printStackTrace(); }finally{ try { if (fis!=null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fos!=null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); } } } } import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * 加载文件系统中加密后的class字节码的类加载器 * @author Matrix42 * */ public class DecriptClassLoader extends ClassLoader{ private String rootDir; public DecriptClassLoader(String rootDir){ this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException{ Class<?> c = findLoadedClass(name); //应该先查询有没有加载过这个类,如果已经加载,则直接返回加载好的类,如果没有,则加载新的类 if(c!=null){ return c; }else{ ClassLoader parent = this.getParent(); try{ c = parent.loadClass(name); }catch(ClassNotFoundException e){ } if(c!=null){ return c; }else{ byte[] classDate = getClassData(name); if(classDate==null){ throw new ClassNotFoundException(); }else{ c = defineClass(name,classDate,0,classDate.length); } } } return c; } private byte[] getClassData(String classname){ String path = rootDir+"/"+classname.replace('.','/')+".class"; //System.out.println(path); //IOUtils 可以使用它将流中的数据转换成字节数组 InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try{ is = new FileInputStream(path); //相当于解密 int temp = -1; while((temp=is.read())!=-1){ baos.write(temp^0xff); } return baos.toByteArray(); }catch(Exception e){ e.printStackTrace(); }finally{ try{ if(is!=null){ is.close(); } }catch(IOException e){ e.printStackTrace(); } try{ if(baos!=null){ baos.close(); } }catch(IOException e){ e.printStackTrace(); } } return null; } }