Java虚拟机(JVM)是Java应用的运行环境,从一般意义上来讲,JVM是通过规范来定义的一个虚拟的计算机,被设计用来解释执行从Java源码编译而来的字节码。更通俗地说,JVM是指对这个规范的具体实现。这种实现基于严格的指令集和全面的内存模型。另外,JVM也通常被形容为对软件运行时环境的实现。通常JVM实现主要指的是HotSpot。
JVM规范保证任何的实现都能够以同样的方式解释执行字节码。其实现可以多样化,包括进程、独立的Java操作系统或者直接执行字节码的处理器芯片。我们了解最多的JVM是作为软件实现,运行在流行的操作系统平台上(包括Windows、OS X、Linux和Solaris等)。
JVM的结构允许对一个Java应用进行更细微的控制。这些应用运行在沙箱(Sandbox)环境中。确保在没有恰当的许可时,无法访问到本地文件系统、处理器和网络连接。远程执行时,代码还需要进行证书认证。
除了解释执行Java字节码,大多数的JVM实现还包含一个JIT(just-in-time 即时)编译器,用于为常用的方法生成机器码。机器码使用的是CPU的本地语言,相比字节码有着更快的运行速度。
虽然理解JVM不是开发或运行Java程序的必要条件,但是如果多了解一些JVM知识,那么就有机会避免很多性能上的问题。理解了JVM,实际上这些问题会变得简单明了。
体系结构
JVM规范定义了一系列子系统以及它们的外部行为。JVM主要有以下子系统:
- Class Loader 类加载器。 用于读入Java源代码并将类加载到数据区。
- Execution Engine 执行引擎。 执行来自数据区的指令。
数据区使用的是底层操作系统分配给JVM的内存。
类加载器(Class Loader)
JVM在下面几种不同的层面使用不同的类加载器:
- bootstrap class loader(引导类加载器):是其他类加载器的父类,它用于加载Java核心库,并且是唯一一个用本地代码编写的类加载器。
- extension class loader(扩展类加载器):是bootstrap class loader加载器的子类,用于加载扩展库。
- system class loader(系统类加载器):是extension class loader加载器的子类,用于加载在classpath中的应用程序的类文件。
- user-defined class loader(用户定义的类加载器):是系统类加载器或其他用户定义的类加载器的子类。
当一个类加载器收到一个加载类的请求,首先它会检查缓存,确认该类是否已经被加载,然后把请求代理给它的父类。如果父类没能成功的加载类,那么子类就会自己去尝试加载该类。子类可检查父类加载器的缓存,但父类不能看到子类所加载的类。之所类加载体系会这样设计,是认为一个子类不应该重复加载已经被父类加载过的类。
执行引擎(Execution Engine)
执行引擎一个接一个地执行被加载到数据区的字节码。为了保证字节码指令对于机器来说是可读的,执行引擎使用下面两个方法:
- 解释执行:执行引擎把它遇到的每一条指令解释为机器语言。
- 即时编译:如果一条指令经常被使用,执行引擎会把它编译为本地代码并存储在缓存中。这样,所有和这个方法相关的代码都会直接执行,从而避免重复解释。
尽管即时编译比解释执行要占用更多的时间,但是对于需要使用成千上万次的方法,只需要处理一次。相比每次都解释执行,以本地代码的方式运行会节约很多执行时间。
JVM规范中并不规定一定要使用即时编译。即时编译也不是用于提高JVM性能的唯一的手段。规范仅仅规定了每条字节码对应的本地代码,至于执行引擎如何实现这一对应过程的,完全由JVM的具体实现来决定。
内存模型(Memory Model)
Java内存模型建立在自动内存管理的概念之上。当一个对象不再被一个应用所引用,垃圾回收器就会回收它,从而释放相应的内存。这一点和其他很多需要自行释放内存的语言有很大不同。
JVM从底层操作系统中分配内存,并将它们分为以下几个区域:
- 堆空间(Heap Space):这是共享的内存区域,用于存储可以被垃圾回收器回收的对象。
- 方法区(Method Area):这块区域以前被称作“永生代”(permanent generation),用于存储被加载的类。这块区域最近被JVM取消了。现在,被加载的类作为元数据加载到底层操作系统的本地内存区。
- 本地区(Native Area):这个区域用于存储基本类型的引用和变量。
一个有效的管理内存方法是把对空间划分为不同代,这样垃圾回收器就不用扫描整个堆区。大多数的对象的生命周期都很段短暂,那些生命周期较长的对象往往直到应用退出才需要被清除。
当一个Java应用创建了一个对象,这个对象是被存储到“初生池”(eden pool)。一旦初生池存储满了,就会在新生代触发一次minor gc(小范围的垃圾回收)。首先,垃圾回收器会标记出那些“死对象”(不再被应用所引用的对象),同时延长所有保留对象的生命周期(这个生命周期长度是用数字来描述,代表了期所经历过的垃圾回收的次数)。然后,垃圾回收器会回收这些死对象,并把剩余的活着的对象移动到“幸存池”(survivor pool),从而清空初生池。
当一个对象存活达到一定的周期后,它就会被移动到堆中的老生代:“终身代”(tenured pool)。最后,当终身代被填满时,就会触发一次full gc或major gc(完全的垃圾回收),以清理终身代。
(译者注:一般我们把初生池和幸存池所在的区域合并成为新生代,把终身代所在的区域成为老生代。对应的,在新生代上产生的gc称为minor gc,在老生代上产生的gc称为full gc。希望这样大家在其他地方看到对应的术语时能更好理解)
当垃圾回收(gc)执行的时候,所有应用线程都要被停止,系统产生一次暂停。minor gc非常频繁,所以被优化的能够快速的回收死对象,是新生代的内存的主要的回收方式。major gc运行起来就相对慢得多,因为要扫描非常多的活着的对象。垃圾回收器本身也有多种实现,有些垃圾回收器在一定情况下能更快的执行major gc。
堆的大小是动态的,只有堆需要扩张的时候才会从内存中分配。当堆被填满时,JVM会重新给堆分配更多的内存,直到达到堆大小的上限,这种重新分配同样会导致应用的短暂停止。
线程
JVM是运行在一个独立的进程中的,但它可以并发执行多个线程,每个线程都运行自己的方法,这是Java必备的一个部分。以即时消息客户端这样一个应用为例,它至少运行两个线程。一个线程用于等待用户输入,另一个检查服务端是否有新的消息传输。再以服务端应用为例,有时一个请求可能要涉及多个线程并发执行,所以需要多线程来处理请求。
在JVM的进程中,所有的线程共享内存和其他可用的资源。每一个JVM进程在进入点(main方法)处都要启动一个主线程,其他线程都从主线程启动,成为执行过程中的一个独立部分。线程可以再不同的处理器上并行执行,同样也可以共享一个处理器,线程调度器负责处理多个线程共享一个处理器的情况。
很多应用(特别是服务端应用)会处理很多任务,需要并行运行。这些任务中有些是非常重要的,需要实时执行的。而另外一些是后台任务,可以在CPU空闲时执行。任务是在不同的线程中运行的。举例子来说,服务端可能有一些低优先级的线程,它们会根据一些数据来计算统计信息。同时也会启动一些高优先级的进程用于处理传入的数据,响应对这些统计信息的请求。这里可能有很多的源数据,很多来自客户端的数据请求,每个请求都会使服务端短暂的停止后台计算的线程以响应这个请求。所以,你必须监控在运行的线程数目并且保证有足够的CPU时间来执行必要的计算。
(译者注:这一段在原文中是在性能优化的章节,译者认为这可能是作者的不小心,似乎放在线程的章节更合适。)
性能优化
JVM的性能取决于其配置是否与应用的功能相匹配。尽管垃圾回收器和内存回收进程是自动管理内存的,但是你必须掌管它们的频率。通常来说,你的应用可使用的内存越多,那么这些会导致应用暂停的内存管理进程需要起作用的就越少。
如果垃圾回收发生的频率比你想的要多很多,那么可以在启动JVM的时候为其配置更大的最大堆大小值。堆被填满的时间越久,就越能降低垃圾回收发生的频率。最大堆大小值可以在启动JVM的时候,用-Xmx参数来设定。默认的最大堆大小是被设置为可用的操作系统内存的四分之一,或者最小1GB。
如果问题出在经常重新分配内存,那么你可以把初始化堆大小设置为和最大堆大小一样。这就意味着JVM永远不需要为堆重新分配内存。但这样做就会失去动态堆大小适配的优化,堆的大小从一开始就被固定下来。配置初始化对大小是在启动JVM,用-Xms来设定。默认初始化堆大小会被设定为操作系统可用的物理内存的六十四分之一,或者设置一个最小值。这个值是根据不同的平台来确定的。
如果你清楚是哪种垃圾回收(minor gc或major gc)导致了性能问题,可以在不改变整个堆大小的情况下设定新生代和老生代的大小比例。对于需要产生大量临时对象的应用,需要增大新生代的比例(当然,后果是减小了老生代的大小)。对于长生命周期对象较多的应用,则需增大老生代的比例(自然需要减少新生代的大小)。以下几种方法可以用来设定新生代和老生代的大小:
- 在启动JVM时,使用-XX:NewRatio参数来具体指定新生代和老生代的大小比例。比如,如果想让老生代的大小是新生代的五倍,则设置参数为-XX:NewRatio=5,默认这个参数设定为2(即老生代占用堆空间的三分之二,新生代占用三分之一)。
- 在启动JVM时,直接使用-Xmn参数设定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。
- 在启动JVM时,直接使用-XX:NewSize和-XX:MaxNewSize参数设定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。
每一个线程都有一个栈,用于保存函数调用、返回地址等等,这些栈有着对应的内存分配。如果线程过多,就会导致OutOfMemory错误。即使你有足够的空间的堆来存放对象,你的应用也可能会因为创建一个新的线程而崩溃。这种情况下,需要考虑限制线程中的栈大小的最大值。线程栈大小可以在JVM启动的时候,通过-Xss参数来设置,默认这个值被设定为320KB至1024KB之间,这和平台相关。
性能监控
当开发或运行一个Java应用的时候,对JVM的性能进行监控是很重要的。配置JVM不是一次配置就万事大吉的,特别是你要应对的是Java服务器应用的情况。你必须持续的检查堆内存和非堆内存的分配和使用情况,线程数的创建情况和内存中加载的类的数据情况等。这些都是核心参数。
使用Anturis控制台,你可以为任何的硬件组件上运行的JVM配置监控(例如,在一台电脑上运行的一个Tomcat网页服务器)。
JVM监控可以使用以下衡量标准:
- 总内存使用情况(MB):即JVM使用的总内存。如果JVM使用了所有可用内存,这项指标可以衡量底层操作系统的整体性能。
- 堆内存使用(MB):即JVM为运行的Java应用所使用的对象分配的所有内存。不使用的对象通常会被垃圾回收器从堆中移除。所以,如果这个指数增大,表示你的应用没有把不使用的对象移除或者你需要更好的配置垃圾回收器的参数。
- 非堆内存的使用(MB):即为方法区和代码缓存分配的所有内存。方法区是用于存储被加载的类的引用,如果这些引用没有被适当的清理,永生代池会在每次应用被重新部署的时候都会增大,导致非堆的内存泄露。这个指标也可能指示了线程创建的泄露。
- 池内总内存(MB):即JVM所分配的所有变量内存池的内存和(即除了代码缓存区外的所有内存和)。这个指标能够让你明确你的应用在JVM过载前所能使用的总内存。
- 线程:即所有有效线程数。举个例子,在Tomcat服务器中每个请求都是一个独立的线程来处理,所以这个衡量指标可以表示当前有多少个请求数,是否影响到了后台低权限的线程的运行。
- 类:即所有被加载的类的总数。如果你的应用动态的创建很多类,这可能是服务器内存泄露的一个原因。
一、标准参数
1.-server
-client
虚拟机服务器模式/客户机模式,使用server模式可以提高性能,启动比client模式慢,长期运行则比client模式快。当该参数不指定时,虚拟机启动检测主机是否为服务器,如果是则以server模式启动,否则以client模式启动,J2SE5.0检测的根据是至少2个CPU和最低2GB内存
2.-agentlib:<lib-name>=<options>
-agentpath:<lib-path>=<options>
本地类库加载,当你的部分类包含一些本地方法时,需要自己编写本地代码并位于操作系统加载共享包(dll)的路径上,如果你不喜欢将该包放在操作系统识别的加载上,则可以通过指定这个参数来加载自己的本地共享包(dll)。不同之处在于-agentlib中仅指定包名,根据操作系统的不同虚拟机在一定路径上搜索该包,譬如对于windows平台虚拟机在PATH路径上搜索该包,而lib-path则是指定全路径,例如
-agentlib:hprof 在windows平台虚拟机会在启动时到PATH路径上搜索hprof.dll并加载
虚拟机在加载代理包之后有一个启动的操作(详细参见JDK参考),<options>指的是代理包的启动参数
3.-classpath classpath
-c classpath
指定类路径,系统应用类加载器(ClassLoader)会到该路径下加载类
4.-Dproperty=value
设置系统属性,可以通过System.getProperty(property)获得
5.-enableassertions[:<package name>”…” | :<class name> ]
-ea[:<package name>”…” | :<class name> ]
-disableassertions[:<package name>”…” | :<class ; ]
-da[:<package name>”…” | :<class name> ]
启用和停用断言,默认是停用断言。断言指的是从JDK1.4开始在支持的关键字assert,assert(booleanvalue),当 booleanvalue为false时,抛出java.lang.AssertionError,必须指出的是,代码编译必须是1.4及其以上顺从的,即编译时使用如下参数
java -source 1.4
一般仅在开发阶段启用断言,而在运行阶段不使用
其使用包括如下几种情况
java -ea //启动断言
java -ea:pkname… //在包pkname及其子包下起用断言
java -ea:pkname.classname //对类 pkname.classname启用断言
停用断言与启用设置类似
6.-enablesystemassertions
-esa
-disablesystemassertions
-dsa
启用和停用系统类断言
7.-jar
运行包含在一个jar包里的程序,一般在jar包的/META-INF/MANIFEST.MF文件中指定Main-Class值为要运行的主函数,譬如 Main-Class:ayufox.ejb3.Test
8.-javaagent:<classname>[<=options>]
加载java语言代理,该功能是JDK5新增加的,可以通过该设置在JVM运行主函数(main)之前做一些预处理工作,其中classname中必须包含有静态方法
public static void premain(String agentArgs, Instrumentation inst) { … }
上面的options即是传入该函数的代理参数agentArgs,关于Instrumentation详细参见包java.lang.instrument
9.-verbose
-verbose:class
-verbose:gc
-verbose:jni
在运行时
class:将类加载情况在控制台中打印出来
gc:将虚拟机的垃圾回收事件信息打印
jni:放本地方法调用信息打印
-verbose与-verbose:class一样
10.-version
-showversion
显示版本信息,不同在于第一种显示版本后虚拟机结束退出
11.-?
-help
显示帮助信息并退出
12.-X
显示非标准参数(见下面介绍)并退出
二、非标准参数(以-X开头)
1.-Xint
所有字节码以解析模式运行。第一代虚拟机即是以这种方式运行,由于需要Java解析器解析运行,所以效率比较低;第二代虚拟机则采用将字节码编译成本地代码的方式,效率大大提高;第三代虚拟机也叫自适应(HotSpot)虚拟机,通过监测代码的执行情况检测出代码被频繁执行的部分,将其尽量优化成本地代码方式运行,而对于普通部分,则采用解析的模式运行。
2.-Xbatch
禁止后台编译,一般HotSpot虚拟机在检测到一段代码为频繁执行代码需要将其编译成本地代码时,会启动一个后台线程完成这个工作,而同时采用解析的方式继续运行字节码。如果设置了该参数,则会停止继续执行字节码,先将其编译成本地代码,然后再继续执行。
3.-Xdebug
-Xnoagent
-Xrun
-Xrunjdwp
启用调试模式,见前面的《利用JPDA构建调试平台》这篇文章,后面将在一个独立的文章中详细介绍
4.-Xbootclasspath:bootclasspath
-Xbootclasspath/a:path
-Xbootclasspath/p:path
设置启动根Classpath,即使启动类加载器将在何处加载对象,关于类启动加载器,参见《JVM类加载器体系结构》说明,分号后面的值指定路径,以分号隔开。其区别在于,-Xbootclasspath:bootclasspath将新的根加载路径覆盖默认的路径(\jre\lib \rt.jar),-Xbootclasspath/a:path将新的根加载路径和原有的根加载路径相结合,-Xbootclaspath /p:path将新的根加载路径与原有的根加载路径相结合,加载类时优先搜索该加载路径
5.-Xcheck:jni
对本地调用(JNI)采用更严格的检测方式,在进行JNI调用之前检测数据和传入参数,如果碰到不合法的数据则强制结束掉虚拟机,对运行性能有损害
6.-Xfuture
对类格式(class文件格式)采用更严格的检测方式,以便向后兼容,最好在开发时采用该参数
7.-Xnoclassgc
不使用垃圾回收
8.-Xloggc:file
与-verbose:gc功能一样,不同在于-Xloggc:file将信息记录到一个文件,而-verbose:gc将其输出到控制台
9.-Xincgc
-Xmsn
-Xmxn
-Xssn
跟内存分配和垃圾回收相关,-Xincgc表示采用渐进式垃圾回收,-Xmsn设置初始内存池大小,-Xmxn表示内存池允许的最大大小,-Xssn是线程栈大小,n是要设置的值,必须是1024的倍数,譬如
-Xms6291456 -Xmx83886080
-Xms6144k -Xmx81920k
-Xms6m -Xmx80m
该部分对虚拟机的性能非常重要,在后面将有独立的篇章详细介绍
10.-Xprof
-Xrunhprof[:help][:<suboption>=<value>,…]
在运行时剖析运行情况,并将剖析结果打印到控制台,其中后一个可以指定特定剖析对象,譬如cpu,堆(heap)等,可以运行java -Xrunhprof:help获得可以剖析的对象和取值
11.-Xrs
减少JVM对操作系统信号量的使用,J2SE1.3.1开始引入。
SUN 在J2SE1.3.0中增加了Java应用程序关闭时的回调钩子(Hook),以便当JVM意外终止时用户可以做一些资源清除工作。JVM监视控制台事件以实现JVM意外终止时的回调。JVM明确地注册了一个控制台控制处理器,当JVM接收到CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, 或CTRL_SHUTDOWN事件时,该处理器介入关闭回掉钩子(HOOK)的处理。
如果虚拟机以服务的方式运行(譬如WEB服务器)当其收到 CTRL_LOGOFF_EVENT事件,由于系统并不会因此终止JVM进程,故JVM不可以进行终止的操作,然而这与如上产生了冲突(不结束却又调用关闭回调钩子),为了避免这个问题,从J2SE1.3.1使用-Xrs以使JVM不再监测控制台事件。
一、 JVM的生命周期
1. JVM实例对应了一个独立运行的java程序它是进程级别
a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程
c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
2. JVM执行引擎实例则对应了属于用户运行程序的线程它是线程级别的
二、 JVM的体系结构
1. 类装载器(ClassLoader)(用来装载.class文件)
2. 执行引擎(执行字节码,或者执行本地方法)
3. 运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
三、 JVM类加载器
JVM整个类加载过程的步骤:
1. 装载
装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+
包名+ClassLoader实例ID。
2. 链接
链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。
完成校验后,JVM初始化类中的静态变量,并将其值赋为默认值。
最后对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如public、private域权限等),会造成NoSuchMethodError、NoSuchFieldError等错误信息。
3. 初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:
调用了new;
反射调用了类中的方法;
子类调用了初始化;
JVM启动过程中指定的初始化类。
JVM类加载顺序:
JVM两种类装载器包括:启动类装载器和用户自定义类装载器。
启动类装载器是JVM实现的一部分;
用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。
JVM装载顺序:
Jvm启动时,由Bootstrap向User-Defined方向加载类;
应用进行ClassLoader时,由User-Defined向Bootstrap方向查找并加载类;
1. Bootstrap ClassLoader
这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。
2. Extension ClassLoader
JVM用此classloader来加载扩展功能的一些jar包。
3. System ClassLoader
JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。
4. User-Defined ClassLoader
User-DefinedClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。
ClassLoader抽象类的几个关键方法:
(1) loadClass
此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从parent ClassLoader中寻找,如仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法
(2) findLoadedClass
此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。
(3) findClass
此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
(4) findSystemClass
此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如仍然为找到,则返回null。
(5) defineClass
此方法负责将二进制的字节码转换为Class对象
(6) resolveClass
此方法负责完成Class对象的链接,如已链接过,则会直接返回。
四、 JVM执行引擎
在执行方法时JVM提供了四种指令来执行:
(1)invokestatic:调用类的static方法
(2)invokevirtual:调用对象实例的方法
(3)invokeinterface:将属性定义为接口来进行调用
(4)invokespecial:JVM对于初始化对象(Java构造器的方法为:<init>)以及调用对象实例中的私有方法时。
主要的执行技术有:
解释,即时编译,自适应优化、芯片级直接执行
(1)解释属于第一代JVM,
(2)即时编译JIT属于第二代JVM,
(3)自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代
JVM的经验,采用两者结合的方式
开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
五、 JVM运行时数据区
第一块:PC寄存器
PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址
第三块:堆(Heap)
它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
第四块:方法区域(Method Area)
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class
对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
第五块:运行时常量池(Runtime Constant Pool)
存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
第六块:本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
六、 JVM垃圾回收
GC的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
(1)对新生代的对象的收集称为minor GC;
(2)对旧生代的对象的收集称为Full GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC