JVM 浅析(二)
7-引用类型
根搜索算法
把内存中的每个对象看作一个节点,定义对象作为根节点“GC Roots”。如果一个对象有另一个对象引用,则认为这个对象有一条指向另一个对象的边
GC Roots
第一种:虚拟机栈中的引用的对象
第二种:类中定义的全局的静态对象
第三种:使用关键字 static final 的常量引用
第四种:JNI 技术,使用 native 方法
四种引用
(1)强引用
使用关键 new 生成的对象的引用,如 Object obj = new Object(),只要 obj 对象生命周期未结束,或者没有显示把 obj 赋为 null,JVM 不会进行回收。
当内存容量不足時,宁愿出现 OOM 错误,也要一直保存的引用,不會對其對象進行回收。
强引用是 Java 默认支持的模式,只要栈内存中还有指向堆内存的引用,則当内存不足時,只会抛出 OutOfMemoryError 错误,不会对对象进行回收
因强引用是我們开发一直使用的模式,并且具有这样的异常错误产生,所以,尽量少实例化对象,避免因无法回收对象造成内存溢出
(2)软引用
当内存不足时,才會对软引用对象进行回收,常应用于高速缓存
*Mybatis 开发框架使用高速缓存,也就是软引用方式。
对于当下的开源项目,使用软引用作为项目的缓存组件。当内存充足时,不进行对象回收,反之,进行对象回收。使用 java.lang.ref.SoftReference 类实现,所有的引用都在 java.lang.ref 包下。
SoftReference 类的使用方法如下:
构造方法:public SoftReference(T referent)
取得对象:public T get()
程序範例:
(3)弱引用
无论内存是否充足,只要出現了垃圾,便会对其进行回收处理
*弱不禁风,弱爆了
当内存进行 GC 处理时,弱引用对象进行回收,不建议使用。一旦發生 GC,則必须进行对象处理,所以容易数据丟失。可以使用以下类实现弱引用:java.util.WeakHashMap 和 WeakReference
範例一
範例二
注意:
1、弱引用不建议使用,就是因为一旦发生 GC,就会被清空。
2、使用字符串举例,需要小心对象自动入池的问题,一旦入池,是无法清空的,所以,建议使用 new String()方式
3、字符串对象一旦声明,不可修改
四、引用隊列
引用隊列就是對要回收的對象進行保存,在对象回收前可以进行操作处理。對於對象的回收都是從根對象開始向下掃描的。對於 GC 而言,想要確定哪些對象被回收,確定好引用的强度即引用路徑的設定。以下展示引用路徑。
圖 11 引用路徑
對於對象的引用,要考慮到引用的關聯,因此必須找到強關聯。爲了避免非強關聯對象所帶來的内存引用的問題,故而提出了引用隊列的概念。如果創建軟引用和弱引用使用了引用隊列,那麽,在引用對象回收前會保存到引用隊列之中。引用隊列的主要作用就是對被回收對象的控制。可以使用 java.lang.ref.ReferenceQueue 類實現。
範例:
(5)幽靈引用(虚引用)
相當於沒有引用。
永遠得不到的數據叫做幽靈引用,更多意義在於提出了一種思想。所有保存在幽灵引用中的数据都不会真正保留。对象无法回收,finalize()方法无法执行。幽灵引用可以使用以下類實現:
java.lang.ref.PhantomReference
範例:
延伸:
String 字符串如何存儲?構造方法(new String(""))和直接賦值("")有何區別?
问题:
1、WeakHashMap 和 HashMap 的区别?
8-JVM 性能调优
性能优化的关键在于怎么找到当前系统的性能瓶颈,并不在于怎么进行优化。
性能优化分为好几个层次,比如系统层次、算法层次、代码层次等等。JVM 的性能优化被认为是底层优化,门槛较高,精通这种技能的人比较少。
JVM 本身给我们提供了很多强大而有效的监控进程、分析定位瓶颈的工具,比如 JConsole、JMap、JStack、JStat 等等。使用这些命令,再结合 Linux 自身提供的一些强大的进程、线程命令,能够快速定位系统瓶颈。
1、使用系统压力测试工具,例如 LoadRunner,通过工具的压测结果分析出系统瓶颈。
2、使用服务器和数据库压测工具,例如 Spotlight,查看并发压测时服务器和数据库,CPU 以及内存的各项指标信息。
3、JStack。JStack 能够看到当前 Java 进程中每个线程的当前状态、调用栈、锁住或等待去锁定的资源,而且很强悍的是它还能直接报告是否有线程死锁,可谓解决线程问题的不二之选。
JStack 拉取的文件信息基本分为以下几个部分:
该拉取快照的服务器时间
JVM 版本
以线程 ID(即 tid)升序依次列出当前进程中每个线程的调用栈
死锁(如果有的话)
阻塞锁链
打开的锁链
监视器解锁情况跟踪
每个线程在等待什么资源,这个资源目前在被哪个线程 hold,尽在眼前。JStack 最好在压测时多次获取,找到的普遍存在的现象即为线程瓶颈所在。
多次拉取 JStack 信息,发现很多线程处于重新获取大小的状态。因此,判断每个线程的分配空间不足,可以适当调整 thread local area(TLA)的大小。TLA 默认最小大小 2KB,默认首选大小 16KB - 256 KB (取决于新生代分区大小)。这里我们调整 TLA 空间大小为最小 32KB,首选 1024 KB,JVM 启动参数中加入:-XXtlaSize:min=32k,preferred=1024k
4、GC 日志
服务器开启 GC 日志报告只需在 Java 进程启动参数里加入-Xverbose:memory -Xverboselog:verboseText.txt。通过这些日志同样能够指出当前压力下的 GC 的频率,为本次性能瓶颈 - GC 过于频繁提供有力的佐证。
5、JStat 报告
通过 JStat 指令,获取当前已用碓大小和新生代分区大小,从而进一步查看性能。
jstat -J-Djstat.showUnsupported=true -snap 12224
综上分析,可以对内存空间进行适当调整。
-Xms10240m -Xmx10240m -Xns:1024m -XXtlaSize:min=32k,preferred=1024k
7、性能瓶颈的定位
1)、性能进程的获取
使用 TOP 命令拿到最耗 CPU 的 Java 进程。
2)、性能线程的获取
通过进程,进一步找到该进程下的占用 CPU 资源最高的线程。
ps p 10495 -L -o pcpu,pid,tid,time,tname,cmd
3)、使用 JStack 查看线程当前状态信息
通过该方式,从而进行系统性能最终调优。
9-对象访问模型
一、對象訪問模式
JVM 运行不是简单的程序执行,而是充满了各种各样的算法,只有清楚了其中的运行算法,才能够很好地进行内存调优。
对象访问模式便是其中的体现,也是非常重要的内容,理解了对象访问模式,调优也才有了门道。
在 Java 中,引用數據類型是最主要的数据处理模型。引用数据类型主要涉及到运行时数据区中的棧内存、堆内存、方法區。实际上,引用数据类型也是最容易产生内存垃圾的数据模型。
引用数据類型的對象訪問,可用簡單的實例化(Object obj = new Object())进行闡述,主要思路如下:
新定義的對象的名稱 Object obj,存储在栈内存中,保存有堆内存的引用。但是严格上讲保存至本地變量表中。由本地变量表中的表格,确定出與之對應的棧内存,最后才能够由栈找到堆。这也是为什么 Java 中不允许变量重名的问题所在;
new Object(),真正的实例化对象,保存在堆内存中;
利用堆内存的對象進行方法的調用,即訪問方法區;
简而言之,引用数据类型的对象访问思路如下:
本地变量表 --> 栈 --> 堆 --> 方法区
引用数据类型拥有两种对象访问模式:通过句柄访问和通过直接指针访问。C++中使用句柄访问的模式。在 Java 中直接使用的是對象保存模式。
通过句柄访问模式非常繁琐,但比较稳定。因为先有对象实例,而后才能调用方法。访问模式思路如下:
栈内存中的引用 --> 句柄池中的对象实例数据指针 --> 实例池中的对象实例数据 --> 对象类型数据指针 --> 方法区中的对象类型数据
通过直接指针访问模式相对句柄方式,能够快速进行对象操作。访问模式思路如下:
栈内存中的引用 --> 堆内存中的对象实例数据 --> 对象类型数据指针 --> 对象类型数据
圖 句柄訪問模式
圖 直接指針訪問模式(對象保存模式)
由指令 java -version 可以得出虚拟机的版本信息,而目前世界上有三种 JVM。
SUN 公司改良的 HotSpot,2006 年之后,HotSpot 架构开源了;
Bea 公司开发的 JRockit,目前最快的 JVM;
IBM 的虚拟机。
Oracle 收购了 SUN 和 Bea 两家公司,因此,不可能开发两套 JVM,在 JDK1.8 已经开始融合,JDK1.9 则极有可能融合完成。
獲取當前 JVM 版本:java -version
純解釋模式啓動:java -Xint -version
純編譯模式啓動:java -Xcomp -version
Java 虚拟机的模式:
mixed mode:混合模式,即适合于编译与执行;
interpreted mode:纯解释模式,不需要进行编译;
compiled mode:纯编译模式,不需要进行解释。
从目前的 Java 发展来看,实际上是为 JEE 服务的,也就是为服务器使用,企业级的应用程序。故设计了虛擬機有兩種啓動模式
-server:服務器模式,占用内存大,啓動速度慢,默認模式
-client:本地單機運行程序模式,啓動速度快
虚拟机启动模式的配置文件路徑如下:
Linux:$java_home/jre/lib/CPU 廠商+操作系統位數/jvm.cfg
Windows:%java_home%\jre\lib\CPU 廠商+操作系統位數\jvm.cfg
10-Stack
Java 虛擬機棧(Java Virtual Machine Stacks)
Java 虛擬機棧,是綫程私有的,生命周期和綫程相同。
Java 虛擬機棧描述的是 Java 方法執行的内存模型:
執行一個方法時會產生一個棧幀(Stack Frame),隨後將其保存至棧的頂部,即入棧。方法執行完畢后自動將此棧幀進行出棧。頂部的棧幀就表示當前方法。
通常有以下錯誤:
請求的棧的深度過大,虛擬機抛出 StackOverflowError;
*举例如下。
程序示例:
异常错误:
Exception in thread "main" java.lang.StackOverflowError
at org.fuys.ownutil.Test.func(Test.java:10)
如果虛擬機允許虛擬機棧動態擴展,當内存不足以擴展棧的情況,虛擬機抛出 OutOfMemoryError;
棧幀(Stack Frame)
Java 虛擬機棧存放多個棧幀,棧幀包括以下幾個部分。
局部變量表(local variables):
保存方法的局部變量和形參,以變量槽(solt)為最小單位,只允許保存 32 位的變量,如果超過 32 位,則會開闢兩個連續的變量槽,如 long 和 double 類型數據,这里需要注意的是局部变量的数据类型,有可能是基本数据类型,也可能是引用数据类型。也就是说,对象引用存在局部变量表中;
操作數棧(operand stack):
表達式計算在棧中完成;
指向當前方法所屬的類的運行時常量池的引用(reference to runtime constant pool):
引用其他類的常量或者使用 String 池中的字符串;
方法返回地址(return address):
方法執行完后需要返回調用此方法的位置,所以需要在棧幀中保存方法返回地址;
動態鏈接(dynamic linking):
概念等同於計算機科學中的動態鏈接,在 Java 代碼運行過程中,動態生成方法和變量的符號引用,從而減少内存碎片的產生,節省空間,提高效率。
*对于以上理解,使用程序即可理解。
圖 棧幀
Java 中的對象池是對常量池的規則破壞,在 Java 啓動的時候已經為常量池分配好了内存空間,但是 String 中的 intern()打破了這種限制,可以動態的進行常量池設置。
問題
StackOverflowError 和 OutOfMemoryError 的區別?
提醒:
如果虛擬機允許虛擬機棧動態擴展,當内存不足以擴展棧的情況,虛擬機抛出 OutOfMemoryError;
10-JVM 架构
JVM 架构
Java 是一种跨平台的语言,JVM 屏蔽了底层系统的不同,为 Java 字节码文件构造了一个统一的运行环境
堆和栈
堆:每个 JVM 实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放
在这个堆中,并由应用所有的线程共享
堆栈:JVM 为每个新创建的线程都分配一个堆栈。也就是说,对于一个 Java 程序来说,
它的运行就是通过对堆栈的操作来完成的
Java 中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,
也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对
象,而在堆栈中分配的内存只是一个指向这个堆对象的引用而已
方法区和程序计数器
方法区主要存放从磁盘加载进来的类字节码,而在程序运行过程中创建的类实例则存放在堆里。程序运行的时候,实际上是以线程为单位运行的,当 JVM 进入启动类的 main 方法的时候,就会为应用程序创建一个主线程,main 方法里的代码就会被这个主线程执行,每个线程有自己的 Java 栈,栈里存放着方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序计数寄存器
Java(线程)栈
所有在方法内定义的基本类型变量,都会被每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所以这些变量一定是线程安全的
线程工作内存 & volatile
Java 内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行
一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
JDK、JRE、JVM 之间的区别与联系
11-类加载
双亲委托模型
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要上级类加载器确认,只有当上级类加载器没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类
自定义类加载器
隔离加载类:同一个 JVM 中不同组件加载同一个类的不同版本。
扩展加载源:从网络、数据库等处加载字节码。
字节码加密:加载自定义的加密字节码,在 ClassLoader 中解密
扩展-JAVA 影响 7 个性能指标
1、响应时间和吞吐量
响应时间,根据应用程序处理请求,进行业务处理,最终完成数据传输的过程所花费的时间。从专业术语而言,就是请求到响应所花费的时间。可以从 HTTP 级别,亦可以从数据库级别进行考察。响应时间过慢,可以从两方面进行分析,第一,业务逻辑程序代码处理效率不高,第二,则有可能是网络贷款过窄,导致响应时间过慢。
吞吐量,是指单位时间内系统处理客户端请求的数量。数量越大,则吞吐量越大。
需要注意的是,网络带宽,会对响应时间和吞吐量造成影响。
通过使用 APMs(诸如 New Relic、AppDynamics、Ruxit)工具衡量该类指标,可以通过数据进行对比,从而观察出程序变化对于指标变化的影响。
谷歌浏览器亦可以分析 HTTP 协议的响应时间,针对网页请求进行分析。
扩展
国内知名的服务器监控工具,诸如阿里云、腾讯云,采用 SNMP 协议对服务器进行监控,并且大多数收费,对于企业用户较为合适。
SNMP,简单网络管理协议,由一组网络管理的标准组成,包含一个应用层协议、数据库模型和一组资源对象。该协议支持网络管理系统,用以监控连接到网络上的设备是否有任何引起管理上关注的情况。
New Relic 是国外知名的监控服务商,不采用 SNMP 协议,通过自身的 Agent 实现。New Relic Agent 支持主流的 Linux 及 Windows 服务器平台。主要有:
Ubuntu 14.04 +
Debain 5 +
CentOS 及 RedHat 5 +
Windows Server 2003 +
移动互联网的带动,New Relic 同时提供了移动端的监控工具支持。截止目前,IOS 系统依然支持,但是 Android 系统则已经下架了。
2、平均负载
平均负载表示系统在一定时间内“运行进程队列”中的平均进程数量。运行队列,既不包含等待 IO,也不包含 WAIT,也没有 KILL 的进程队列集合。
在 LINUX 系统中,可以通过使用 uptime、w、top、cat /proc/loadavg 等指令,查看系统的平均负载。指令结果会显示 3 个浮点型数字,分别表示过去 1 分钟,5 分钟,15 分钟内系统的平均负载。对于系统而言,平均负载数值低于 0.7,是最为稳定安全的。
[root@iZuf68pmxwulw5vt2slnmlZ ~]# uptime
13:56:43 up 218 days, 23:11, 2 users, load average: 0.04, 0.06, 0.05
[root@iZuf68pmxwulw5vt2slnmlZ ~]# cat /proc/loadavg
0.06 0.06 0.05 1/464 6112
单核 CPU,平均负载数值间于 0.00~1.00 之间正常。
多核 CPU,平均负载数值(数字/CPU 核数)间于 0.00~1.00 之间正常。
平均负载的推荐工具 htop。
对于推荐工具 htop 而言,通过 LINUX 实际分析结果,可以得出系统下的平均负载,也可以得到系统中的“进程”使用 CPU 和 MEM 情况。
这里需要注意的是,思考一款工具是否合格,可以判断该工具对于程序运行的精确定位程度。精度越高,信息量越大,则越好。
3、GC 率和暂停时间
GC 率和暂停时间,可以从某一角度查看程序运行的效率和性能。对于这一指标,需要通过收集 GC 日志和 JVM 参数,分析得出不同指标之间的相互影响。
对于 GC 日志而言,需要通过 JVM 配置 GC Print 参数获取 GC 日志。基于 JVM 运行机制,GC 应该尽可能发生在年轻代完成,而不是老年代。
推荐工具如:jClarity Censum、GCViewer。
4、业务指标
应用程序的性能不光可以通过技术指标进行查看,也可以通过业务指标进行分析。业务指标通常包含收益、用户数。当然,这些数据可以系统自带,通过一定的图形和报表进行展示。也可以使用成熟的工具。
推荐的工具如:Grafana、The ELK stack、Datadog、Librato。
5、正常运行时间和服务运行状态
通过以上指标,分析应用程序的性能。可以通过 Pingdom 的 servlet 功能进行运行状态检查,检查应用程序的所有传输,包括数据库和 S3。
推荐工具:Pingdom。
扩展
Amazon S3(Amazon Simple Storage Service),简单存储服务,可用在 Web 上的任何位置存储和检索任意数量的数据。它能够提供 99.99%的持久性,并且可以在全球大规模传递数万亿对象。既能存储数据,又能进行数据分析,同时,亦可作为数据的备份和恢复目标。
使用 Amazon 的云数据迁移选项,客户可以轻松将大量数据移入或移出 S3。数据在存储到 S3 之后,自动采用成本更低、存储期限更长的云存储类(如 S3 Standard - Infrequent Access 和 Amazon Glacier)进行存档。
6、错误率
错误率通常指的是 HTTP 传输失败百分占比。但是该比例数值,意义不大,应当需要注意特定传输的错误率,由此精确定位出程序运行错误的具体位置,也即具体代码方法执行处。
Takipi 是一款根据错误率,获取错误线索的工具。Takipi 通过日志文件,可以获取关于服务器状态的信息,包括堆栈跟踪、源代码和变量值等等信息。
7、日志大小
系统日志随着系统运行,一直不断增加。但是当系统运行性能降低或者异常,日志大小会有变化。故可以通过日志大小这一个角度,观察系统情况。
解决办法可以通过使用 logstash 划分使用日志,并将它们发送并存储在 Splunk、ELK 或其他的日志管理工具中。
推荐工具:Splunk、Sumo Logic、Loggly。
12-执行
二、執行流程(JVM 构成)
Java 源程序(*.java)由編譯器(通过 javac.exe 命令)編譯為字節碼(*.class),在编译的过程中,由校驗器校驗代码语法是否正確。校验通过后,通過類加載器(ClassLoader)加載類信息,再由解釋器轉換爲機器碼運行執行。
類加載器(ClassLoader)通過獲取類文件(*.class)的路徑,將類加載進來,從而獲取類的信息。这里类加载器获取类文件的路径,由 CLASSPATH 环境变量属性配置。有了类加载器,可以配置 class 路径的任意位置,从而执行程序。Java 中提供过了一个类加载器 java.lang.ClassLoader。
执行引擎(Execution Engine),解析 Java 程序,分配存储空间。
本地方法接口(Native Interface),JVM 提供的支持调用本地方法的接口,也就是实现对于本地 C 函数的调用。尤其是在调用操作系统的资源上,会大量调用本地 C 函数,这在 IO 和 MutilThreading 上体现得非常清楚。往更高说,那就是 JNI 技术。
运行时数据区(Runtime Data Area),程序的真正运行在运行时数据区之中。实际上,Java 内存管理,就是对运行时数据区进行内存管理。
圖 1 程序執行流程
圖 3 運行時數據區
栈与堆的关系,可以用下图理解。
图 Java 内存管理
运行时数据区与 Java 线程对象有关。开发人员能够控制的内存空间,指的就是运行时数据区。调优也是针对该区域。而对于运行时数据区,对象共享涉及的内存更大,故调优又更加针对于堆。
13-字节码编译
Java 如何实现跨平台,即实现在不同操作系统、不同硬件平台上,无需变更代码就能够实现运行?
增加中间层(虚拟层)解决
字节码文件包含大量的字节,一个字节(8 位)可以存储 256 个指令,Java 具有 200 个左右指令。代码执行过程中,JVM 将字节码解释执行,屏蔽底层操作系统,亦可以将字节码编译执行,如果是热点代码,则通过 JIT 动态编译为机器码,提高执行效率
执行流程
字节码编译过程
评论