转自 该篇文章,将从我们熟悉的Java反射API出发,一直到JVM源码对Java反射的支持。本文分析用的是JDK18。与原文的1.7相比,变化还是不少的,但总体思想没变 首先看使用的示例代码: 这段代码的逻辑十分简单,就是利用反射来获取一个字符串的哈希值。该示例毫无实际利用的价值,但是拿来举例子是足够的了。 这篇文章我们要弄明白的问题是: getDeclaredMethod方法是如何正确查找到方法的; invoke方法是如何被执行的; 这两个方法的调用,在虚拟机的层面上究竟发生了什么; JDK分析 反射的源码在JDK层面上是比较简单的,getDeclaredMethod方法的源码是: 其中最重要的就是 searchMethods是依据传入的方法名字和方法参数,从privateGetDeclaredMethods返回的Method[]中找到对应的方法,返回该方法的 副本。所以重要的逻辑是在privateGetDeclaredMethods中,该方法的源码是: 该方法大体上可以分成两个部分: 从本地缓存中获取Method[]; 如果本地没有缓存,则从虚拟机中获取; 本地缓存 先考察从本地缓存获取,其中关键的一句话是: 这一句话就是访问本地缓存。ReflectionData是Class的一个内部类,它里面存储了一个Class所含有的各色信息,包括字段、方法,并且进行了一定的分类,源码是: 这个源码要注意的是其构造函数,在这个构造函数中,只设置了redefinedCount,其余字段都会是Null。reflectionData()方法源码是: 这段代码有一个很关键的点:本地缓存的ReflectionData是使用SoftReference的,这意味着,在内存紧张的时候会被回收掉。如果在回收掉之后再次请求获得Method[],那么就会新建一个SoftReference,作为缓存。也就是其中newReflectionData的逻辑: 这段逻辑看上去很简单,但是其中有一些很容易遗漏的点。首先要注意的是,当走到 这一句的时候,rd里面只有一个redefinedCount字段被设置,其余字段都还是null。该方法的核心是: 该方法的底层是使用Unsafe的compareAndSwapObject方法来实现的: 这个方法的含义就是:如果clazz内存分布中,在reflectionDataOffset位置的地方,如果期望的值是oldData,那么就会使用newData来替换掉oldData。而在clazz内存的reflectionDataOffset的地方,恰好就是Class类中reflectionData域引用的地方。所以这个方法的真实含义,借助CAS原子操作,将老的reflectionData替换为新的reflectionData。 但是,这里要注意的一点是,此时的newData,也就是 它是一个新建的ReflectionData的实例的soft reference。而新建的ReflectionData,也就是rd,前面已经提到了,它只有redefinedCount字段是被设置好了的,其余字段都还是null。所以如果没有本地缓存,在方法返回了之后,一直到privateGetDeclaredMethods方法的 此刻的rd就是方才新建的ReflectionData,只有redefinedCount字段是设置的。所以判断: 是必然不成立的,因此最终会走到从JVM中获取Method[]的代码处。我们总结一下,在这种情况下,Class的实例类似于: 从JVM获取 前面已经提到,在本地缓存失效,或者被回收了之后,需要从JVM当中获得Method[]: 所以最后是通过调用native方法。下面是C++的环节了 从JVM中取到了Method[]。该方法在JVM中的头文件声明是: 该方法的实现是: 如我在其中注释的一样,整个过程可以分成几步: 先处理基本类型和数组类型; 确保类已经完成链接,在此处就是确保String已经完成了链接 统计符合要求的方法,并根据方法个数为结果分配内存 创建Method[]数组 下面我们将对2和4进行详细的分析。在这之前先对JVM的oop-klass做一个简单的介绍: oop: ordinary object pointer,普通对象指针,用于描述对象的实例信息 klass:Java类在JVM中的表示,是对Java类的描述,看名称是元数据对于我们日常说的一个对象来说,它们的oop-klass模型如图: JVM就是用这种方式,将一个对象的数据和对象模型进行分离。普遍意义上来说,我们说持有一个对象的引用,指的是图中的handle,它是oop的一个封装。 处理基本类型和数组类型 JNIHandles::resolve_non_null方法将ofClass转化为JVM内部的一个oop,因为JVM只会直接操作oop实例。顾名思义,is_primitive是判断是否属于基本类型,其源码实现是: 这一段的逻辑十分简单,取到java_class这个oop中在klass字段上的值,如果是Null则被认为是基本类型。因为对于任何一个非基本类型的对象来说,oop中必然包含着一个指向其klass实例的指针。 另外一个判断条件: java_lang_Class::as_klass方法将一个oop包装成一个klass,逻辑和前面的is_primitive十分相像: 在JVM中,对象在内存中的基本存在形式就是oop,正如前面的图所描述的那样。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass关系如图: 我们接下来看调用的 is_array_klass […]
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] GC调优,当前主要是在Java 8下PerNew + CMS可以通过调优来尽可能的减少FGC,但在9引入G1后,调优的点已经发生了变化,在11引入ZGC后更是如此(并发、不再分代),技术迭代。。。。 jstat参数 jstat -h-h requires an integer argumentUsage: jstat -help|-options jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]Definitions: <option> An option reported by the -options option <vmid> Virtual Machine Identifier. A vmid takes the following form: […]
转自 JVM调优目标 JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。 程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。 这里有几个比较重要的指标: 内存占用:程序正常运行需要的内存大小。 延迟:由于垃圾收集而引起的程序停顿时间。 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。 当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。 JVM调优工具 调优相关依照 调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。 ①系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。 ②堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。 ③GC日志:程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化。比如如下一段GC日志: 上面一共是4条GC日志,来看第一行日志,“2018-08-02T14:39:11.560-0800”是精确到了毫秒级别的UTC 通用标准时间格式,配置了“-XX:+PrintGCDateStamps”这个参数可以跟随gc日志打印出这种时间戳,“10.171”是从JVM启动到发生gc经过的秒数。第一行日志正文开头的“[GC”说明这次GC没有发生Stop-The-World(用户线程停顿),第二行日志正文开头的“[Full GC”说明这次GC发生了Stop-The-World,所以说,[GC和[Full GC跟新生代和老年代没关系,和垃圾收集器的类型有关系,如果直接调用System.gc(),将显示[Full GC(System)。接下来的“[PSYoungGen”、“[ParOldGen”表示GC发生的区域,具体显示什么名字也跟垃圾收集器有关,比如这里的“[PSYoungGen”表示Parallel Scavenge收集器,“[ParOldGen”表示Serial Old收集器,此外,Serial收集器显示“[DefNew”,ParNew收集器显示“[ParNew”等。再往后的“30128K->4091K(30208K)”表示进行了这次gc后,该区域的内存使用空间由30128K减小到4091K,总内存大小为30208K。每个区域gc描述后面的“51092K->50790K(98816K), 0.0140970 secs”进行了这次垃圾收集后,整个堆内存的内存使用空间由51092K减小到50790K,整个堆内存总空间为98816K,gc耗时0.0140970秒。 ④线程快照:顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息,更详细的使用和分析网上有很多例,这篇文章写到这里已经很长了就不过多叙述了,贴一篇博客供参考:http://www.cnblogs.com/kongzhongqijing/articles/3630264.html ⑤堆转储快照:程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。 JVM调优工具 ①用 jps(JVM process Status)可以查看虚拟机启动的所有进程、执行主类的全名、JVM启动参数,比如当执行了JPSTest类中的main方法后(main方法持续执行),执行jps -l可看到下面的JPSTest类的pid为31354,加上-v参数还可以看到JVM启动参数jps -l -v。 ②用jstat(JVM Statistics Monitoring Tool)监视虚拟机信息jstat -gc pid 500 […]
new String都是在堆上创建字符串对象。当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。 通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。 常量字符串的“+”操作,编译阶段直接会合成为一个字符串。如string str=”JA”+”VA”,在编译阶段会直接合并成语句String str=”JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。 对于final字段,编译期直接进行了常量替换。而对于非final字段则是在运行期进行赋值处理的。 常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆上创建新的对象 JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。 JVM 常量池中存储的是对象还是引用 如果是runtime constant pool(而不是interned string pool / StringTable之类的其他东西)的话,其中的引用类型常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之类)都存的是引用,实际的对象还是存在Java heap上的。
这里包含一个问题,为何要移除方法区 改用元空间:https://openjdk.java.net/projects/jdk8/features 移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。 为什么移除持久代。在JDK1.8移除了永久代,添加了元空间 它的大小是在启动时固定好的——很难验证并进行调优。-XX:MaxPermSize HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,非强类型,难以跟踪调试,需要存储元数据的元数据信息。 简化垃圾回收:对每一个回收集使用专门的元数据迭代器。 可以在GC不进行暂停的情况下并发地释放类数据。 使得原来受限于持久代的一些改进未来有可能实现 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 元空间的特性 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致 每个加载器有专门的存储空间 只进行线性分配 不会单独回收某个类 省掉了GC扫描及压缩的时间 元空间里的对象的位置是固定的 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉 元空间的内存分配模型 绝大多数的类元数据的空间都从本地内存中分配 用来描述类元数据的类也被删除了 分元数据分配了多个虚拟内存空间 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些 归还内存块,释放内存块列表 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉 减少碎片的策略 元空间内存管理 元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。 准确的来说,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定Java引用。 元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个全局的空闲组块列表。当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。当一个类加载器不再存活,那么其持有的组块将会被释放,并返回给全局组块列表。类加载器持有的组块又会被分成多个块,每一个块存储一个单元的元信息。组块中的块是线性分配(指针碰撞分配形式)。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。 元空间的问题 元空间虚拟机采用了组块分配的形式,同时区块的大小由类加载器类型决定。类信息并不是固定大小,因此有可能分配的空闲区块和类需要的区块大小不同,这种情况下可能导致碎片存在。元空间虚拟机目前并不支持压缩操作,所以碎片化是目前最大的问题。
转自 三色标记法是一种垃圾回收法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。 # 三色标记算法思想 **** 三色标记法将对象的颜色分为了黑、灰、白,三种颜色。 白色:该对象没有被标记过。(对象垃圾) 灰色:该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾) 黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象) # 算法流程 **** 从我们main方法的根对象(JVM中称为GC Root)开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象,扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World),再次进行扫描,此时因为黑色对象的属性都也已经被标记过了,所以只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多), 此时程序继续执行,GC线程扫描所有的内存,找出扫描之后依旧被标记为白色的对象(垃圾),清除。 具体流程: 首先创建三个集合:白、灰、黑。 将所有对象放入白色集合中。 然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合 重复 4 直到灰色中无任何对象 通过write-barrier检测对象有变化,重复以上操作 收集所有白色对象(垃圾) # 三色标记存在问题 **** 1.浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,浮动垃圾对系统的影响不大,留给下一次GC进行处理即可。 2.对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。 # 解决办法 **** 在JVM虚拟机中有两种常见垃圾回收器使用了该算法:CMS(Concurrent Mark Sweep)、G1(Garbage First) ,为了解决三色标记法对对象漏标问题各自有各自的法: # CMS回顾 **** CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求(但是实际由于某些问题,很少有使用CMS作为主要垃圾回收器的)。 从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括: 1)初始标记(CMS initial […]
转自 扩展: IDEA Debug实现原理 如何不重启JVM,替换掉已经加载的类 Java字节码增强探秘 熟悉Spring的小伙伴们应该都对aop比较了解,面向切面编程允许我们在目标方法的前后织入想要执行的逻辑,而今天要给大家介绍的Java Agent技术,在思想上与aop比较类似,翻译过来可以被称为Java代理、Java探针技术。 Java Agent出现在JDK1.5版本以后,它允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序,先从下面这张图直观的看一下它都被应用在哪些场景: 看到这里你是不是也很好奇,究竟是什么神仙技术,能够应用在这么多场景下,那今天我们就来挖掘一下,看看神奇的Java Agent是如何工作在底层,默默支撑了这么多优秀的应用。 回到文章开头的类比,我们还是用和aop比较的方式,来先对Java Agent有一个大致的了解: 作用级别:aop运行于应用程序内的方法级别,而agent能够作用于虚拟机级别 组成部分:aop的实现需要目标方法和逻辑增强部分的方法,而Java Agent要生效需要两个工程,一个是agent代理,另一个是需要被代理的主程序 执行场合:aop可以运行在切面的前后或环绕等场合,而Java Agent的执行只有两种方式,jdk1.5提供的preMain模式在主程序运行前执行,jdk1.6提供的agentMain在主程序运行后执行 下面我们就分别看一下在两种模式下,如何动手实现一个agent代理程序。 Premain模式 Premain模式允许在主程序执行前执行一个agent代理,实现起来非常简单,下面我们分别实现两个组成部分。 agent 先写一个简单的功能,在主程序执行前打印一句话,并打印传递给代理的参数: 在写完了agent的逻辑后,需要把它打包成jar文件,这里我们直接使用maven插件打包的方式,在打包前进行一些配置。 配置的打包参数中,通过manifestEntries的方式添加属性到MANIFEST.MF文件中,解释一下里面的几个参数: Premain-Class:包含premain方法的类,需要配置为类的全路径 Can-Redefine-Classes:为true时表示能够重新定义class Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换 Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀 其中Premain-Class为必须配置,其余几项是非必须选项,默认情况下都为false,通常也建议加入,这几个功能我们会在后面具体介绍。在配置完成后,使用mvn命令打包: 打包完成后生成myAgent-1.0.jar文件,我们可以解压jar文件,看一下生成的MANIFEST.MF文件: 可以看到,添加的属性已经被加入到了文件中。到这里,agent代理部分就完成了,因为代理不能够直接运行,需要附着于其他程序,所以下面新建一个工程来实现主程序。 主程序 在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。 在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下: 并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行: 以我们在idea中执行程序为例,在VM options中加入添加启动参数: 执行main方法,查看输出结果: 根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。 缺陷 在提供便利的同时,premain模式也有一些缺陷,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。我们对上面例子中agent的代码进行一下改造,手动抛出一个异常。 再次运行主程序: 可以看到,在agent抛出异常后主程序也没有启动。针对premain模式的一些缺陷,在jdk1.6之后引入了agentmain模式。 Agentmain模式 agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。 agent agent部分和上面一样,实现简单的打印功能: 修改maven插件配置,指定Agent-Class: 主程序 这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。 attach机制 和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖: VirtualMachine代表了一个要被附着的java虚拟机,也就是程序中需要监控的目标虚拟机,外部进程可以使用VirtualMachine的实例将agent加载到目标虚拟机中。先看一下它的静态方法attach: […]
转:阿里技术。做了部分补充 一 为什么讲这个? 总结AQS之后,对这方面顺带的复习一下。本文从以下几个高频问题出发: 对象在内存中的内存布局是什么样的? 描述synchronized和ReentrantLock的底层实现和重入的底层原理。 谈谈AQS,为什么AQS底层是CAS+volatile? 描述下锁的四种状态和锁升级过程? Object o = new Object() 在内存中占用多少字节? 自旋锁是不是一定比重量级锁效率高? 打开偏向锁是否效率一定会提升? 重量级锁到底重在哪里? 重量级锁什么时候比轻量级锁效率高,同样反之呢? 二 加锁发生了什么? 无意识中用到锁的情况: 简单加锁发生了什么? 要弄清楚加锁之后到底发生了什么需要看一下对象创建之后再内存中的布局是个什么样的? 一个对象在new出来之后在内存中主要分为4个部分: markword这部分其实就是加锁的核心,同时还包含的对象的一些生命信息,例如是否GC、经过了几次Young GC还存活。 klass pointer记录了指向对象的class文件指针。 instance data记录了对象里面的变量数据。 padding作为对齐使用,对象在64位服务器版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么就靠对齐来补。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6。 知道了这4个部分之后,我们来验证一下底层。借助于第三方包 JOL = Java Object Layout java内存布局去看看。很简单的几行代码就可以看到内存布局的样式: 将结果打印出来: 从输出结果看: 1)对象头包含了12个字节分为3行,其中前2行其实就是markword,第三行就是klass指针。值得注意的是在加锁前后输出从001变成了000。Markword用处:8字节(64bit)的头记录一些信息,锁就是修改了markword的内容8字节(64bit)的头记录一些信息。从001无锁状态,变成了00轻量级锁状态。 2)New出一个object对象,占用16个字节。对象头占用12字节,由于Object中没有额外的变量,所以instance = 0,考虑要对象内存大小要被8字节整除,那么padding=4,最后new Object() 内存大小为16字节。 拓展:什么样的对象会进入老年代?很多场景例如对象太大了可以直接进入,但是这里想探讨的是为什么从Young GC的对象最多经历15次Young GC还存活就会进入Old区(年龄是可以调的,默认是15)。上图中hotspots的markword的图中,用了4个bit去表示分代年龄,那么能表示的最大范围就是0-15。所以这也就是为什么设置新生代的年龄不能超过15,工作中可以通过-XX:MaxTenuringThreshold去调整,但是一般我们不会动。 三 锁的升级过程 1 锁的升级验证 探讨锁的升级之前,先做个实验。两份代码,不同之处在于一个中途让它睡了5秒,一个没睡。看看是否有区别。 这两份代码会不会有什么区别?运行之后看看结果: […]
转:https://tech.meituan.com/2016/09/23/g1.html 前言 G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推出,相信熟悉JVM的同学们都不会对它感到陌生。在JDK 9中,G1被提议设置为默认垃圾收集器(JEP 248)。在官网中,是这样描述G1的: The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 […]
转自:美团技术团队 1. 写在前面 | 本文主要针对 Hotspot VM 中“CMS + ParNew”组合的一些使用场景进行总结。重点通过部分源码对根因进行分析以及对排查方法进行总结,排查过程会省略较多。另外,本文专业术语较多,有一定的阅读门槛,如未介绍清楚,还请自行查阅相关材料。 | 本文总字数 2 万左右(不包含代码片段),整体阅读时间约 60min ,文章较长,可以选择你感兴趣的场景进行研究。 **** 1.1 引言 自 Sun 发布 Java 语言以来,开始使用 GC 技术来进行内存自动管理,避免了手动管理带来的悬挂指针(Dangling Pointer)问题,很大程度上提升了开发效率,从此 GC 技术也一举成名。GC 有着非常悠久的历史,1960 年有着“Lisp 之父”和“人工智能之父”之称的 John McCarthy 就在论文中发布了 GC 算法,60 年以来, GC 技术的发展也突飞猛进,但不管是多么前沿的收集器也都是基于三种基本算法的组合或应用,也就是说 GC 要解决的根本问题这么多年一直都没有变过。笔者认为,在不太远的将来, GC 技术依然不会过时,比起日新月异的新技术,GC 这门古典技术更值得我们学习。 那么,GC 问题处理能力能不能系统性掌握?一些影响因素都是互为因果的问题该怎么分析?比如一个服务 RT 突然上涨,有 GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高四个表象,到底哪个是诱因?如何判断 GC […]