jvm中线程本地内存是真实存在的,还是一个抽象的概念概念

Java调优之JVM和线程的内存分析_Linux编程_Linux公社-Linux系统门户网站
你好,游客
Java调优之JVM和线程的内存分析
来源:Linux社区&
作者:铁木箱子
因为自己开发的一个网站在768M内存的机器上撑不起100多个用户的运行,因为每个用户启用功能后,系统将为每个用户分配8个左右的独立线程,我的这篇文章& &也有介绍的。在内存小的机器上经常出现的问题就是Cann&t allocate memory和OutOfMemoryError错误,这个要从jvm的内存结构来进行分析了。在jvm内存调整过程中,我们经常使用的参数就是:
为jvm启动时分配的内存,比如-Xms200m,表示分配200M-Xmx&
为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存-Xss&
为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M
一般jvm出现Cannt& allocate memory的错误就是机器的内存不够,导致系统无法为jvm分配给定的内存,这个在启动时犹未突出,所以会在启动参数中设置-Xms来指定;而OutOfMemoryError错误则一般会在系统运行一段情况后出现,绝大部分也是机器内存不够或是JVM本身的内存空间已被用尽,这时就要根据情况进行调整了,如果是JVM本身的内存空间用尽,则需要调整-Xmx参数来分类jvm的可用内存,如果是机器内存不够则要增加内存或是调优程序了。
上面两个参数主要是来设置jvm的最小可用内存和最大可用内存,属于进程级别的内存控制。对于java中的线程,我之前的理解一直是在java中new新线程的时候是直接使用jvm的内存,可实际情况却不是这样的。在java中每个线程需要分配线程内存,用来存储自身的线程变量,在jdk1.4中每个线程是256K的内存,在jdk1.5中每个线程是1M的内存,jdk1.6中不太清楚,估计也是1M。在java中每new一个线程,jvm都是向操作系统请求new一个本地线程,此时操作系统会使用剩余的内存空间来为线程分配内存,而不是使用jvm的内存。这样,当操作系统的可用内存越少,则jvm可用创建的新线程也就越少,举个例子如下:
Total Memory
Spare Memory
Thread Count
上面的表格只是大致的估计了下在特定内存条件下可以在java中创建的最大线程数。随着-Xmx的加大,空闲的内存数就更少,那么可以创建的线程也就更少,同时在JDK1.4和1.5版本不同下,可创建的线程数也会根据每个线程的内存大小不同而不同。
其实只要我们了解了JVM的内存大小指定以及java中线程的内存模型,基本上我们就可以很好的控制如何在java中使用线程和避免内存溢出或错误的问题了。
本文永久更新链接地址:
相关资讯 & & &
& (09月19日)
& (04月07日)
& (09月19日)
& (07月12日)
& (04月05日)
   同意评论声明
   发表
尊重网上道德,遵守中华人民共和国的各项有关法律法规
承担一切因您的行为而直接或间接导致的民事或刑事法律责任
本站管理人员有权保留或删除其管辖留言中的任意内容
本站有权在网站内转载或引用您的评论
参与本评论即表明您已经阅读并接受上述条款从JVM的角度来看Java的多线程(一)
】 浏览:7次
最近在学习jvm,发现随着对虚拟机底层的了解,对java的多线程也有了全新的认识,原来一个小小的synchronized关键字里别有洞天。决定把自己关于java多线程的所学整理成一篇文章,从最基础的为什么使用多线程,一直深入讲解到jvm底层的锁实现。为什么要使用多线程?可以简单的分两个方面来说:其实多线程根本的问题只有一个:线程间变量的共享java里的变量可以分3类:下图是jvm的内存区域划分图:根据各个区域的定义,我们可以知道:“方法区”和“堆”都属于线程共享数据区,“虚拟机栈”属于线程私有数据区。因此,局部变量是不能多个线程共享的,而类变量和实例变量是可以多个线程共享的。事实上,在java中,多线程间进行通信的唯一途径就是通过类变量和实例变量。也就是说,如果一段多线程程序中如果没有类变量和实例变量,那么这段多线程程序就一定是线程安全的。以Web开发的Servlet为例,一般我们开发的时候,自己的类继承HttpServlet之后,重写doPost()、doGet()处理请求,不管我们在这两个方法里写什么代码,只要没有操作类变量或实例变量,最后写出来的代码就是线程安全的。如果在Servlet类里面加了实例变量,就很可能出现线程安全性问题,解决方法就是把实例变量改为ThreadLocal变量,而ThreadLocal实现的含义就是让实例变量变成了“线程私有”的,即给每一个线程分配一个自己的值。??现在我们知道:其实多线程根本的问题只有一个:线程间变量的共享,这里的变量,指的就是类变量和实例变量,后续的一切,都是为了解决类变量和实例变量共享的安全问题。现在唯一的问题就是要让多个线程安全的共享变量(下文中的变量一般特指类变量和实例变量),上文提到了一种ThreadLocal的方式,其实这种方式并不是真正的共享,而是为每个线程分配一个自己的值。比如现在有一个特别简单的需求,有一个类变量a=0,现在启动5个线程,每个线程执行a++;如果用ThreadLocal的方式,最后的结果就是5个线程都拥有一份自己的a值,最终结果都是1,这显然不符合我们的预期。那么如果不使用ThreadLocal呢?直接声明一个类变量a=0,然后让5个线程分别去执行a++;这样结果依旧不对,而且结果是不确定的,可能是1,2,3,4,5中的任一个。这种情况叫做竞态条件(Race Condition),要理解竞态条件先要理解Java内存模型:要理解java的内存模型,可以类比计算机硬件访问内存的模型。由于计算机的cpu运算速度和内存io速度有几个数量级的差距,因此现代计算机都不得不加入一层尽可能接近处理器运算速度的高速缓存来做缓冲:将内存中运算需要使用的数据先复制到缓存中,当运算结束后再同步回内存。如下图:因为jvm要实现跨硬件平台,因此jvm定义了自己的内存模型,但是因为jvm的内存模型最终还是要映射到硬件上,因此jvm内存模型几乎与硬件的模型一样:每个java线程都有一份自己的工作内存,线程访问变量的时候,不能直接访问主内存中的变量,而是先把主内存的变量复制到自己的工作内存,然后操作自己工作内存里的变量,最后再同步给主内存。现在就可以解释为什么5个线程执行a++最后结果不一定是5了,因为a++可以分解为3步操作:而5个线程并发执行的时候完全有可能5个线程都先执行了第一步,这样5个线程的工作内存里a的初始值都是0,然后执行a=a+1后在工作内存里的运算结果都是1,最后同步回主内存的值肯定也是1。而避免这种情况的方法就是:在多个线程并发访问a的时候,保证a在同一个时刻只被一个线程使用。同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。为了保证共享数据在同一时刻只被一个线程使用,我们有一种很简单的实现思想,就是在共享数据里保存一个锁,当没有线程访问时,锁是空的,当有第一个线程访问时,就在锁里保存这个线程的标识并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,就要等待锁释放。我们把这种思想的三个关键点抽出来:可以说jvm中的三种锁都是以上述思想为基础的,只是实现的“重量级”不同,jvm中有以下三种锁(由上到下越来越“重量级”):其中重量级锁是最初的锁机制,偏向锁和轻量级锁是在jdk1.6加入的,可以选择打开或关闭。如果把偏向锁和轻量级锁都打开,那么在java代码中使用synchronized关键字的时候,jvm底层会尝试先使用偏向锁,如果偏向锁不可用,则转换为轻量级锁,如果轻量级锁不可用,则转换为重量级锁。具体转换过程下面会讲。要想深入了解这3种锁需要了解对象的内存结构(MarkWord头),会涉及到字节码的内部存储格式,但是其实我觉得脱离细节的实现,单从原理上理解这三个锁是很容易的,只需要了解两个大体的概念:MarkWord:java中的每个对象在存储的时候,都有统一的数据结构。每个对象都包含一个对象头,称为MarkWord,里面会保存关于这个对象的加锁信息。Lock Record: 即锁记录,每个线程在执行的时候,会有自己的虚拟机栈,当个方法的调用相当于虚拟机栈里的一个栈帧,而Lock Record就位于栈帧上,是用来保存关于这个线程的加锁信息。最初jvm没有前两种锁(前两种都是jdk1.6才引入的),只有重量级锁。我们之前给出了同步基本思想的三个点,我们也说了jvm的三种锁都是以基本思想为基础的,而这三种锁在第1、2点的实现上本质上是一样的:?而区分这三种锁的关键,就是同步基本思想的第三点:   3.其他线程访问已加锁共享数据要等待锁释放这里的等待锁释放是一个抽象的说法,并没有严格要求怎么等待。而重量级锁因为使用了互斥量,这里的等待就是线程阻塞。使用互斥量可以保证所有情况下的并发安全,但是使用互斥量会带来较大的性能消耗。而且在实际的项目代码中,很可能一段本来不会有并发情况的代码被加了锁,这样每次使用互斥量就白白消耗了性能。能不能先假设被加锁的代码不会有并发的情况,等到发现有并发的时候再使用互斥量呢?答案是可以的,轻量级锁和偏向锁都是基于这种假设来实现的。轻量级锁的核心思想就是“被加锁的代码不会发生并发,如果发生并发,那就膨胀成重量级锁(膨胀指的锁的重量级上升,一旦升级,就不会降级了)”。轻量级锁依赖了一种叫做CAS(compare and swap)的操作,这个操作是由底层硬件提供相关指令实现的:CAS操作需要3个参数,分别是内存位置V,旧的期望值A和新值B。CAS指令执行时,当且仅当V当前值符合旧值A时,处理器用新值B更新V的值,否则不执行更新。上述过程是一个原子操作。假设现在开启了轻量级锁,当第一个线程要锁定对象时,该线程首先会在栈帧中建立Lock Record(锁记录)的空间,用于存储对象目前MarkWord的拷贝,然后虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向线程锁记录的指针。如果操作成功,则该线程获得对象锁。如果失败,说明在该线程拷贝对象当前MarkWord之后,执行CAS操作之前,有其他线程获取了对象锁,我们最开始的假设“被加锁的代码不会发生并发”失效了。此时轻量级锁还不会直接膨胀为重量级锁,线程会自旋不停地重试C
【】【】【】
【】【】【】> JVM学习笔记(一)——JAVA内存区域
JVM学习笔记(一)——JAVA内存区域
相关推荐:本期特邀《实战Java虚拟机》作者:葛一鸣(论坛ID: billykinggym)针对Java虚拟机问题给予大家解答,欢迎网友积极提问,与专家一起讨论!活动时间:--05.20欢迎大家就Java虚拟机方面问题在活动时间内在本贴集中提问,期间专家葛一鸣(论坛ID:
最近开始学习JVM,在此把学习中的体会记录下来。资料主要来源是《深入理解JAVA虚拟机》这本书,以及一些网上找的其他资料。目标JVM是HotSpot1、JAVA运行时数据区包括程序计数器、堆、虚拟机栈、本地方法栈、方法区2、内存区域可以分为“线程私有”和“线程共享”2种。比如程序计数器是属于线程私有的,为了在线程切换之后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器;而堆是线程共享的3、程序计数器是一块较小的内存空间,可以看作是当前线程执行的字节码的行号指示器。如果线程正在执行一个JAVA方法,该计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行一个NATIVE方法,该计数器值为空4、虚拟机栈也是线程私有的,生命周期与线程相同5、虚拟机栈描述的是JAVA方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程6、虚拟机栈中的局部变量表,存放编译期可知的各种基本数据类型、对象引用和returnAddress类型7、本地方法栈与虚拟机栈类似,区别在于虚拟机栈为JAVA方法(也就是字节码)服务,而本地方法栈为虚拟机用到的NATIVE方法服务8、鉴于本地方法栈和虚拟机栈十分相似,HotSpot直接将本地方法栈和虚拟机栈合二为一9、JAVA堆是线程共享的内存区域,在虚拟机启动时就创建。该内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存10、JAVA堆也是垃圾收集器管理的主要区域,所以有时也被称为GC堆。JAVA堆可以细分为新生代和老年代;也可以进一步细分为Eden空间、From Survivor空间、To Survivor空间11、方法区也是线程共享的内存区域。它用于存放已经被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。虽然JAVA虚拟机规范将方法区描述为堆的一个逻辑部分,但是它却有一个别名叫NonH相关推荐:Invoking a Java MethodAs mentioned in Chapter 5, &The Java Virtual Machine,& the virtual machine creates a new stack frame for each Java (not native) method it invokes. The stack frame contains space for the method's local variableeap,目的应该是与JAVA堆区分开来12、方法区的内存回收目标主要是针对常量池的回收和对类型的卸载13、运行时常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载之后存放到方法区的运行时常量池中14、即使是最简单的访问,也会涉及JAVA栈、JAVA堆、方法区这3个最重要的内存区域之间的关联关系Object obj = new Object();Object obj这部分的语义会反映到虚拟机栈的本地变量表中,作为一个reference类型数据出现new Object()这部分的语义会反映到JAVA堆中,形成一块存储了Object类型所有实例数据值的结构化内存在JAVA堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中15、由于reference类型在JAVA虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位,以及访问JAVA堆中的对象的具体位置,所以不同虚拟机实现的对象访问方式会有所不同。主流的访问方式有2种:使用句柄,和直接指针16、如果使用句柄访问方式,JAVA堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据和类型数据各自的具体地址信息17、如果使用直接指针访问方式,JAVA堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址18、使用句柄访问方式的好处是reference中存储的是稳定的句柄地址,在对象被移动时(比如在垃圾收集的时候)只会改变句柄中的实例数据指针,reference本身不需要修改。使用直接指针访问方式的好处是速度更快,节省了一次指针定位的时间开销,由于对象的访问在JAVA中非常频繁,因此这类开销积少成多以后也相当可观19、HotSpot是使用直接指针访问方式进行对象访问的
最近开始学习JVM,在此把学习中的体会记录下来。资料主要来源是《深入理解JAVA虚拟机》这本书,以及一些网上找的其他资料。目标JVM是HotSpot1、JAVA运行时数据区包括程序计数器、堆、虚拟机栈...
------分隔线----------------------------
相关阅读排行
相关最新文章
Copyright 2012- ( Coin163 ) All Rights Reserved &&jvm内存溢出分析及解决 - 疯狂泰克-找到你想要的技术好文 | 最红博
当前位置:
jvm内存溢出分析及解决
1,JVM specification s(JVM 规范) 对JVM 内存的描述
2,Sun 的JVM 的内存机制。
JVM specification 对JVM 内存的描述
首先我们来了解JVM specification 中的JVM 整体架构。如下图:
主要包括两个子系统和两个组件: Class loader(类装载器) 子系统,
Execution engine(执行引擎) 子系统;Runtime data area (运行时数据区域)
组件, Native interface(本地接口)组件。
Class loader 子系统的作用:根据给定的全限定名类名(如
java.lang.Object)来装载class 文件的内容到Runtime data area 中的method
area(方法区域)。Javsa 程序员可以extends java.lang.ClassLoader 类来写自
己的Class loader。
Execution engine 子系统的作用:执行classes 中的指令。任何JVM
specification 实现(JDK)的核心是Execution engine, 换句话说:Sun 的JDK
和IBM 的JDK 好坏主要取决于他们各自实现的Execution engine 的好坏。每个
运行中的线程都有一个Execution engine 的实例。
Native interface 组件:与native libraries 交互,是其它编程语言交
互的接口。
Runtime data area 组件:这个组件就是JVM 中的内存。下面对这个部分
进行详细介绍。
Runtime data area 的整体架构图
Runtime data area 主要包括五个部分:Heap (堆), Method Area(方法区域),
Java Stack(java 的栈), Program Counter(程序计数器), Native method
stack(本地方法栈)。Heap 和Method Area 是被所有线程的共享使用的;而Java
stack, Program counter 和Native method stack 是以线程为粒度的,每个线
程独自拥有。
Java 程序在运行时创建的所有类实或数组都放在同一个堆中。而一个Java 虚拟
实例中只存在一个堆空间,因此所有线程都将共享这个堆。每一个java 程序独
占一个JVM 实例,因而每个java 程序都有它自己的堆空间,它们不会彼此干扰。
但是同一java 程序的多个线程都共享着同一个堆空间,就得考虑多线程访问对
象(堆数据)的同步问题。(这里可能出现的异常java.lang.OutOfMemoryError:
Java heap space)
Method area
在Java 虚拟机中,被装载的class 的信息存储在Method area 的内存中。当虚
拟机装载某个类型时,它使用类装载器定位相应的class 文件,然后读入这个
class 文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并
将这些信息存储到方法区。该类型中的类(静态)变量同样也存储在方法区中。
与Heap 一样,method area 是多线程共享的,因此要考虑多线程访问的同步问
题。比如,假设同时两个线程都企图访问一个名为Lava 的类,而这个类还没有
内装载入虚拟机,那么,这时应该只有一个线程去装载它,而另一个线程则只能
等待。(这里可能出现的异常java.lang.OutOfMemoryError: PermGen full)
Java stack
Java stack 以帧为单位保存线程的运行状态。虚拟机只会直接对Java
stack 执行两种操作:以帧为单位的压栈或出栈。每当线程调用一个方法的时候,
就对当前状态作为一个帧保存到java stack 中(压栈);当一个方法调用返回时,
从java stack 弹出一个帧(出栈)。栈的大小是有一定的限制,这个可能出现
StackOverFlow 问题。下面的程序可以说明这个问题。
public class TestStackOverFlow {
public static void main(String[] args) {
Recursive r = new Recursive();
r.doit(10000);
// Exception in thread &main&
java.lang.StackOverflowError
class Recursive {
public int doit(int t) {
if (t &= 1) {
return t + doit(t - 1);
Program counter
每个运行中的Java 程序,每一个线程都有它自己的PC 寄存器,也是该线程启动
时创建的。PC 寄存器的内容总是指向下一条将被执行指令的饿&地址
&,这里的&地址&可以是一个本地指针,也可以是在方法区
中相对应于该方法起始指令的偏移量。
Native method stack
对于一个运行中的Java 程序而言,它还能会用到一些跟本地方法相关的数据区。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制
的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,不止与
此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分
配内存等。总之,本地方法具有和JVM 相同的能力和权限。(这里出现JVM 无法
控制的内存溢出问题native heap OutOfMemory )
Sun JVM 中对JVM Specification 的实现(内存部分)
JVM Specification 只是抽象的说明了JVM 实例按照子系统、内存区、数据类型
以及指令这几个术语来描述的,但是规范并非是要强制规定Java 虚拟机实现内
部的体系结构,更多的是为了严格地定义这些实现的外部特征。
Sun JVM 实现中:Runtime data area(JVM 内存) 五个部分中的Java Stack ,
Program Counter, Native method stack 三部分和规范中的描述基本一致;但
对Heap 和Method Area 进行了自己独特的实现。这个实现和Sun JVM 的Garbage
collector(垃圾回收)机制有关,下面的章节进行详细描述。
垃圾分代回收算法(Generational Collecting)
基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、
持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。
现在的垃圾回收器(从J2SE1.2 开始)都是使用此算法的。
如上图所示,为Java 堆中的各代分布。
1. Young(年轻代)JVM specification 中的Heap 的一部份
年轻代分三个区。一个Eden 区,两个Survivor 区。大部分对象在Eden 区中生
成。当Eden 区满时,还存活的对象将被复制到Survivor 区(两个中的一个),
当这个Survivor 区满时,此区的存活对象将被复制到另外一个Survivor 区,当
这个Survivor 去也满了的时候,从第一个Survivor 区复制过来的并且此时还存
活的对象,将被复制&年老区(Tenured)&。需要注意,Survivor 的
两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden 复制过来
对象,和从前一个Survivor 复制过来的对象,而复制到年老区的只有从第一个
Survivor 去过来的对象。而且,Survivor 区总有一个是空的。
2. Tenured(年老代)JVM specification 中的Heap 的一部份
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对
3. Perm(持久代) JVM specification 中的Method area
用于存放静态文件,如今Java 类、方法等。持久代对垃圾回收没有显著影响,
但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时
候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大
小通过-XX:MaxPermSize=进行设置
而在出现内存泄露的机器上,其日志显示是无法创建本地线程的原因所引起的。这里的异常信息是:java.lang.OutOfMemoryError: unable to create new native thread,对应上述内存溢出的第4种场景。尽管可以初步怀疑是虚拟机参数的设置导致的问题,但实际上还是需要确认系统在自动化场景下有没有其他内存泄露问题。
重新跑自动化,并中间使用“jstat –gcutil 进程ID 1000 3 &&jstat.txt”命令,每隔3秒查看一下虚拟机堆空间的回收情况。在运行了三个多小时后,发行server.log种已经出现了该OutOfMemory的异常信息。此时查看了jstat.txt文件,发现从自动化开始运行一直到堆栈溢出,内存回收都很正常。全部垃圾回收时间花费了5秒左右,且未有full gc,全为young gc的时间。持久区(Perm)、年老区(Old),分别占用了25%、19%左右的空间。且使用“top”命令监测中间CPU和内存占用都比较稳定,没有激增的现象。
使用“jmap –hito 进程ID”查看内存对象统计,发现没有业务逻辑相关的类导致的泄露问题。系统中创建最多的就是与Sting相关的char数组对象。这个也是正常情况,排除程序级别的内存泄漏问题。也就是说堆栈溢出不是1和2的两种情况。
此时再分析server.log种的日志信息,得知是无法创建本地线程所致的问题。也就是说在压力环境下拥有大量的线程,或者本地内存耗尽时,企图创建新的线程时抛出。而系统能创建的线程数的计算公式如下:&
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads&
MaxProcessMemory 指的是一个进程的最大内存
JVMMemory JVM内存
ReservedOsMemory 保留的操作系统内存
ThreadStackSize 线程栈的大小
【解决方法】:
针对无法创建更多本地线程的情况,调整线程栈的大小,添加-Xss选项,设置为256k后再跑自动化,发现问题解决。
&JAVA_OPTS=&-Xms2048M -Xmx2048M -Xmn512M -Xss256k -XX:PermSize=512M….”
文章来源于网络
转载时请以 超链接的形式 注明:转自
&&&&&&&&&&&&&&&&&&
编程语言最新更新文章
编程语言热门排行文章
最新评论文章

我要回帖

更多关于 什么是抽象概念 的文章

 

随机推荐