• 开发必会系列:《深入理解JVM(第二版)》读书笔记


    一  开始前
    虚拟机Ubuntu16编译openjdk7
    二  内存管理机制
    第二章  内存区域
    1. 运行时内存
    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙里的人想出来。
    1. 运行时数据区:
    2、程序计数器:线程私有。字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令。
    3、Java虚拟机栈:
    线程私有。它是用来描述Java方法执行的内存模型。
    每个方法执行时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    每一方法从调用到执行完,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    局部变量表存放了编译期可知的基本数据类型、对象引用(reference类型)、returnAddress类型。
    此区域有两种异常:
    1. 线程请求的栈深度大于虚拟机所允许的深度——StackOverflowError
    2. 当前大部分虚拟机可动态扩展,虚拟机栈在动态扩展时无法申请到足够的内存——OutOfMemoryError
    1. 本地方法栈:
    与虚拟机栈类似,异常也一样。
    区别是虚拟机栈为虚拟机执行Java方法(即字节码)服务;本地方法栈为虚拟机使用到的Native方法服务。
    1. Java堆:
    被所有线程共享。用来存放对象实例。
    几乎所有对象实例在这里分配内存。
    它是垃圾收集器管理的主要区域。也称GC堆。
    记住:无论堆如何划分,都与存放内容无关;无论是哪个区域,存储的都是对象实例。划分——是为了更好地回收内存,更快地分配内存。
    堆可以在物理上不连续,只要在逻辑上连续。
    虚拟机的可扩展通过-Xmx和Xms控制,如果堆没内存了,抛OutOfMemoryError
    Xmx是用来设置你的应用程序能够使用的最大内存数(致使应用程序,不是整个jvm),如果程序要花很大内存的话,那就需要修改增加此数的值。
        Xms是用它来设置程序初始化的时候内存栈的大小,增加这个值的话你的程序的启动性能会得到提高。(不过同样有前面的限制,以及受到xmx的限制)
        所以根据程序的大小,还有电脑的实际配置,来进行这两个的参数配置即可,参数的单位都是m(兆)。
    1. 方法区:
    被所有线程共享。用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,但是,这更容易遇到内存溢出。HotSpot官方规划放弃永久代,逐步改为用Native Memory实现方法区。在jdk1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。
    此区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
    6.1运行时常量池:
    它是方法区的一部分,class文件中有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。
    附:
    直接内存:NIO可以使用Native函数库直接分配堆外内存,然后,通过一个存储在java堆中的DirectByteBuffer对象,作为引用,操作此内存。如果DirectByteBuffer通过计算得知,分配的内存不够,就会报出OOM异常。
    这种异常的Heap dump很小,程序里间接用过NIO。
    (二)hotspot的对象
    1、对象创建:
    步骤:1)虚拟机遇到一个new指令,先去常量池定位一个类的符号引用,并检查这个符号引用代表的类是否被加载过。没有——得先执行类加载。
    1. 检查完类加载,虚拟机就为新生对象分配内存。大小在类加载完后就能确定了。分配内存会触发java堆中指针碰撞(Bump the Pointer)或空闲列表(Free List)——遇到并发,可以采用CAS保证原子性或TLAB本地线程分配缓冲。
    2. 对对象进行设置,把类元数据信息、对象哈希码、对象GC分代年龄存到对象头(Object Header)
    3. 这时虚拟机的活干完了,但java程序还需要执行<init>方法
    2、对象的访问
    Java程序通过栈上的reference数据来操作堆上的具体对象。Reference只是个引用,如何访问对象取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种。
    (三)OOM
    1、Java堆溢出:
    想在控制台直接输出错误信息:
    1. 在Debug/Run的VM arguments写
    -verbose:gc -Xms20M -Xms20M -Xmn10M -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    1. 在代码中做文档注释写:
    /**
    *VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
    *@author zhaotong
    */
    注意:将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。
    找到错误信息,然后用内存映射分析工具
    分析出是内存泄漏(Memory Leak)还是内存溢出(Memory OverFlow)。
    就是申请了内存空间,并让一个指针变量指向这个空间,但之后却错误地在未释放这个空间,并且没有用别的指针变量指向这个空间的情况下,将指针变量指到了别的地方,这样就导致了无法再访问到这个内存空间的情况,这就是内存泄漏
    再通俗点,内存泄漏,就是new的这个对象死了,没用。
    最后根据工具,找到泄漏对象类型信息和GC Roots引用链,以此定位泄漏代码位置。
    1. 虚拟机栈和本地方法栈溢出
    通过StackOverflowError报错信息可以找到问题所在。
    内存减去Xmx(最大堆容量),再减去MaPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略。剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。
    如果是建立多线程导致的内存溢出,在不能减少线程数或换更大机器的情况下,就只能通过减少最大堆和减少栈容量来获取更多线程。
    1. 方法区和运行时常量池溢出
    方法区是用来存放类信息的。在经常动态生成大量Class的应用中,要注意类回收情况。容易犯错的地方有:大量JSP或动态产生JSP的应用(JSP第一次运行时需要编译为Java类)、基于OSGI的应用(即使是同一个类文件,被不同的加载器加载也会被视为不同的类)。
    插个知识点:String,StringBuffer,StringBuilder
    代码里,常用String sql = “”+””+””+……拼接sql语句,后面需要跟参数,则
    StringBuffer sb = new StringBuffer(sql);
    sb.append(“……”);
    不过以后可以用StringBuilder我觉得。
    1.  垃圾收集器和内存分配策略
    Java堆和方法区需要的内存,是动态分配和回收的,这是本章重点。
    1. 判断堆里实例对象死了没
    1)教科书用的是引用计数算法,引用对象加1,引用失效减1。
    Java虚拟机没用这方法管理内存,因为它很难解决对象之间相互循环引用的问题。
    Java用的啥?是可达性分析(Reachability Analysis):GC Roots对象作起始点,向下搜,搜过的路径叫引用链(Reference Chain),如果一对象到GC Roots,没有任何引用链相连,这个对象就是没用的。
    谁当GC Roots对象?有四个:
    虚拟机栈(栈针中的本地变量表)中引用的对象;
    方法区中类静态属性引用的对象;
    方法区中常量引用的对象;
    本地方法栈中JNI(即Native方法)引用的对象。
    1. 引用,JDK1.2后,引用分四类:
    强引用:如new出来的。它存在就不被回收。
    软引用:有用但不是必需的对象。
    弱引用:不是必要的对象。无论内存够不,GC都回收。
    虚引用:为一对象设置虚引用就一个目的——它被GC回收时收到个系统通知。
    任何一个对象的finalize方法只执行一次,它可以在此时自救。如果这个对象面临下一次回收,它的finalize()方法不会再执行。
    1. 方法区回收
    方法区(或称永久代)主要回收废弃常量和无用类。废弃常量回收和堆里类似,而无用类需要三个条件:
    该类所有实例都被回收了;加载该类的ClassLoader已被回收;该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    大量用反射动态代理等功能的场景,需要虚拟机用-Xnoclassgc参数啥的,控制类的卸载。
    1. 垃圾收集算法
    标记清除等。
    1. hotspot的算法实现
    Hotspot只在特定位置,通过一个叫OopMap的数据结构,枚举GC Roots根节点,这时是程序停下来,进行GC。这个位置叫安全点。
    程序怎么停在安全点?主动式中断:设置个标志,各线程去轮询它,发现它为真线程就中断挂起。轮询标志的地方和安全点重合。
    线程挂起或中断不会轮询了咋办?安全区:它就是个大点的安全点。
    1. 具体咋收垃圾?垃圾收集器
    CMS:Concurrent Mark Sweep
    Garbage First:G1
    没有完美的垃圾收集器,它们都在缩短停顿时间,但不能消除。
    新生代收集器ParNew是-XX:+UseConcMarkSweepGC选项后的默认新生代收集器。
     
    并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
     
    Parallel Scavenge收集器提供参数控制吞吐量。停顿时间短适合用户交互的程序,高吞吐量适合后台运算的程序。根据业务决定吧。
    但有些参数不能设置太小,比如GC停顿时间,它是以牺牲吞吐量和新生代空间来换取的。
    如果真不会手工优化,Parallel Scavenge可以设置参数,把优化交给虚拟机。
    CMS注重停顿时间短,很适合网站或B/S系统服务端。它基于标记-清除算法。
    G1很前沿,也是追求停顿时间短。
    5、说完咋内存咋回收,再说说咋分配?
    分配主要在堆上。对象主要分配在新生代的Eden区上。
    需要大量连续空间的java对象通常进入老年代,另外写程序应该避免制造一群“朝生熄灭”的“短命大对象”。
    虚拟机给每个新生的对象定义个年龄计数器,这对象每熬过一次MinorGC,就长大一岁,到了15岁,它就作为长期存活对象,进入老年代。
    1.  虚拟机性能监控与故障处理工具
    1. jdk命令行工具
    在jdk的bin目录里,有很多用于监控的工具。他们类似linux命名。在dos窗口先进到bin路径,然后直接输入*.exe运行,然后执行如 jps -v的命令行,就能看一些检测结果了。
    1. jps,虚拟机进程状况工具。进程的本地虚拟机唯一ID(Local Virtual Machine Identifier, LVMID),它与操作系统进程(Process Identifier, PID)一致。
    1. 可视化工具
    Jdk1.5及以前用jconsole,1.6及以后用visualvm
    1.  调优实战
    1. 高性能硬件上的程序部署策略
    这个主要有两种部署方式:
    1. 通过64位JDK来使用大内存
    2. 使用若干个32位虚拟机建立逻辑集群来利用硬件资源
    用户交互多的系统,分配超大堆的前提是控制Full GC 频率够低,比如每天深夜定时执行一次。
    而控制Full GC 频率关键是:不有能成批、活的很久的大对象产生。这样才能保障老年代空间稳定。
    无Session复制的亲和式集群:就是均衡器按一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到一个固定的集群节点进行处理即可,这样程序开发阶段就基本不用为集群环境做什么特别的考虑了。
    1. 集群间同步导致的内存溢出:避免过于频繁的写操作。
    2. 堆外内存导致的溢出错误:直接内存的问题
    4、外部命令导致系统缓慢:频繁用Java虚拟机调Runtime新建进程。可改用API达到相同目的。
    编译时间是指虚拟机的JIT编译器(Just In Time Compiler)编译热点代码(Hot Spot Code)的耗时。
    Eclipse调优参数:
    第三部分    虚拟机执行子系统
    1.  类文件结构
    1. 魔数
    Class文件开头四个字节为魔数(magic number),它是标识符。紧接着四个字节是版本号,jdk1.1是45。
    如:16进制编译器打开一个class文件,前八字节为CA FE BA BE 00 00 00 32
    0x0032是十进制的50。此文件能被jdk1.6及以前版本运行。
    1. 常量池
    主版本号再往后,就是常量池入口,它是个放资源的仓库,有字面量(用final修饰)和符号引用(如类或接口全限定名)。
    虚拟机运行时,要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
    1. 常量池后面,是访问标志,用于识别类或接口的访问信息。比如:这个Class是类还是接口,它是不是public类型……
    2. 后面是类索引、父类索引和接口索引,这三项数据确定这个类的继承关系。
    3. 后面是字段表集合,字段表用于描述接口或者类中声明的变量。此外还有方法表和属性表。
    6、字节码指令简介:略
    7、虚拟机实现的方式主要有两种:
    1)将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集
    2)将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)
    1.  虚拟机类加载机制
    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
    1. 类加载过程
    1. 类加载器:
    每个类加载器有独立的类名称空间,所以类加载器和类本身共同确定一个类。
    就算两个类来自同一class文件,被同一虚拟机加载,如果加载他们的类加载器不同,那这俩类必不“相等”。
    类加载器分类:
    启动类加载器(Bootstrap ClassLoader):属于虚拟机一部分,hotspot的是用C++实现。它负责加载<JAVA_HOME>lib目录或-Xbootclasspath指定目录里的类库。
    扩展类加载器(Extension ClassLoader):sun.misc.Launcher$ExtClassLoader实现,加载libext目录的类库。
    应用程序类加载器(Application ClassLoader):sun.misc.Launcher$App-ClassLoader实现,加载用户路径(ClassPath)的类库。
    双亲委派模型:
    既然不同类加载器加载出的类不同,那基础类怎么保持相同呢?双亲模式接到类加载请求,把请求交给父类加载器,父类加载器再交给它的父类加载器,一直转交,如果顶层父类加载器加载不了,再自己加载。这样,无论哪个类加载器加载一些基础类如java.lang.Object,最后都由顶层的启动类加载器进行加载。
    破坏双亲委派模型:
    如果基础类调用用户类,需要破坏双亲模式,这时引入线程上下文类加载器(Thread Context ClassLoader)。
    与API对应,还有SPI(Service Provider Interface);
    OSGI实现模块化热部署的关键则是它自定义的类加载器机制的实现。弄懂了OSGI的实现,就算是掌握了类加载器的精髓。
    1. 虚拟机如何执行定义在Class文件里的字节码
    编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
        解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的。
    1. 运行时栈针结构
    1. 局部变量表:其容量以变量槽(Variable Slot)为最小单位。
    虚拟机通过索引定位引用局部变量表。索引n表示使用第n个Slot。
    遇到对象占用内存大、方法的栈针长时间不能被回收、方法调用次数达不到JIT的编译条件——可以将不再使用的变量手动置为null。
    因为:就算一个对象没用了,但其在局部变量表中占用的Slot还没被其他变量复用,所以作为GC Roots一部分的局部变量表仍然保持对它的关联。
    不过,最优雅的编码方法还是控制变量作用域。上述方法只适合解释器执行时,JIT编译器优化时会把赋值null消除掉。
    类变量在系统初始化时有默认值,但局部变量却不行!成员变量也有默认值,但我不知它是在类加载的哪一步初始化的。
    2)操作数栈:
    3)动态链接:每个栈针有,指向常量池中,该栈针所属方法,的引用。持有该引用就是为了动态链接(调方法)。
    2、方法调用
    只是确定调用哪个方法。方法调用在Class文件里存储的是符号引用。
    1)虚拟机(准确说是编译器)在重载时是通过参数的静态类型作为判定依据的,而且静态类型是编译期可知的,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本。
    依赖静态类型定位方法执行版本的动作称为静态分派,静态分派的典型应用是方法重载。
    2)动态分派:在运行期根据实际类型确定方法执行版本。如重写。
    1.  运行期优化
    1. 虚拟机刚开始用解释器解释执行,省编译时间,但渐渐发现某个代码很常用,就通过即时编译器JIT把它编译成与本地平台相关的机器码,提高运行效率。如何发现热点代码——方法调用计数器,计数器超过一定阈值,就触发JIT编译。
    1.  线程安全和锁优化
    附:悲观就是认为不加锁肯定出问题,所以加了很多锁,导致效率不高。
    而乐观正相反。
    1. 线程安全实现:
    1. 互斥同步:属于悲观,重量级锁。
    主要问题是阻塞唤醒带来的性能问题。
    synchronized是重量级锁;java.util.concurrent中的重入锁(ReentrantLock)和它类似,但多了三个高级功能:等待可中断、按时间顺序的公平锁、锁可通过new Condition绑定多个条件。Jdk1.6之后两者性能持平。
    1. 非阻塞同步:乐观。
    它不加锁,失败了就不断尝试,直至成功。
    1. 无同步
    可重入代码:代码可随时中断;
    线程本地存储:把共享数据的可见范围控制在同一个线程内。
    1. 锁优化
    1. 自旋锁与自适应自旋:
    让后面请求锁的那个线程“稍等一下”,就是让那个线程执行一个忙循环(自旋),但如果等很久,自旋就没用了,还很浪费时间。所以,jdk1.6出现了能自己决定自旋时间的自适应自旋。
    1. 锁消除:
    对代码上同步,但不存在共享数据竞争的锁进行消除。它的判断依据是11章的逃逸分析。
    1. 锁粗化:
    虚拟机探测到有一系列操作对同一个对象反复加锁解锁,就会把加锁同步范围扩展(粗化)到整个操作序列外部。所以有时加一个锁就好了……
    1. 轻量级锁:
    附:对象的头部分存储了个锁标志位。
    它的加锁解锁通过CAS操作。
    1. 偏向锁:
    如果说轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步消除掉,连CAS操作都不做了。
    锁,偏向第一个获得它的线程,以后它没被其他线程获取,有偏向锁的线程永远不需要再进行同步。
     
  • 相关阅读:
    up6-chrome 45+安装教程
    HttpUploader7-授权码配置
    jsp-MySQL连接池
    WordPaster2-正式包布署说明
    HTTP文件上传插件开发文档-ASP
    HTTP文件上传插件开发文档-JSP
    eWebEditor9.x整合教程-Xproer.WordPaster
    42. Trapping Rain Water
    41. First Missing Positive
    40. Combination Sum II
  • 原文地址:https://www.cnblogs.com/zhaot1993/p/13467904.html
Copyright © 2020-2023  润新知