1. 程式人生 > >JMX-JAVA程序監控利器

JMX-JAVA程序監控利器

Java 管理擴充套件(Java Management Extension,JMX)是從jdk1.4開始的,但從1.5時才加到jdk裡面,並把API放到java.lang.management包裡面。

如果一個 Java 物件可以由一個遵循 JMX 規範的管理器應用管理,那麼這個Java 物件就可以稱為一個可由 JMX 管理的資源。


要使一個 Java 物件可管理,則必須建立相應的 MBean 物件,並通過這些 MBean 物件管理相應的 Java 物件。當擁有 MBean 類後,需要將其例項化並註冊到 MBeanServer 上。


一共有四種類型的 MBean , 分別是標準型別 MBean, 動態型別 MBean, 開放型別 MBean 和模型型別 MBean。


注:

  1. 一個java程序裡面可以有多個不同名字的mBeanServer ,每個mbs都是一個獨立的容器,用了管理mbean
  2. 每個mbs都可以註冊多個rmi port,http port等
  3. platformMBeanServer 是由jvm建立的,並添加了一些系統的mbean,如cpu,記憶體,網路,執行緒等等

1、本機使用

當我們啟動java程序後,經常會使用jps,jinfo,jmap,jstat等jdk自帶的命令去查詢程序的狀態,這其中的原理就是,當java程序啟動後,會建立一個用於本機連線的“localConnectorAddress”放到當前使用者目錄下,當使用jps等連線時,會到當前使用者目錄下取到“localConnectorAddress”並連線。

	@Test
	public void test1() {
		List<VirtualMachineDescriptor> vms = VirtualMachine.list();
		for (VirtualMachineDescriptor desc : vms) {
			VirtualMachine vm;
			try {
				System.out.println("desc:" + desc);
				System.out.println("程序id:"+desc.id());
				vm = VirtualMachine.attach(desc);
			} catch (Exception e) {
				e.printStackTrace();
				continue;
			}
			JMXConnector connector = null;
			try {
				Properties props = vm.getAgentProperties();
				for (Map.Entry<Object, Object> entry : props.entrySet()) {
					System.out.println(entry.getKey() + "->" + entry.getValue());
				}				
				
				String connectorAddress = props.getProperty("com.sun.management.jmxremote.localConnectorAddress");
				if (connectorAddress == null) {
					System.out.println("connectorAddress  is  null");
					continue;
				}
				System.out.println("conn:" + connectorAddress);
				 //以下程式碼用於連線指定的jmx,本地或者遠端
				JMXServiceURL url = new JMXServiceURL(connectorAddress);
			//JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/TestJMXServer"); 
				connector = JMXConnectorFactory.connect(url);

				MBeanServerConnection mbeanConn = connector.getMBeanServerConnection();
				Set<ObjectName> beanSet = mbeanConn.queryNames(null, null);
				// ...
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				try {
					if (connector != null) connector.close();
					break;
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

需要注意的是,上面程式碼需要把tools.jar放到classpath裡面。

上面程式碼有時候取不到本地連線地址,這個時候需要嘗試讓agent載入management-agent.jar,完整程式碼如下:

package cn.myroute.mbean;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.Properties;

public class AbstractJmxCommand  {
    private static final String CONNECTOR_ADDRESS =
        "com.sun.management.jmxremote.localConnectorAddress";

    public static String getJVM() {
        return System.getProperty("java.vm.specification.vendor");
    }

    public static boolean isSunJVM() {
        // need to check for Oracle as that is the name for Java7 onwards.
        return getJVM().equals("Sun Microsystems Inc.") || getJVM().startsWith("Oracle");
    }
    
    public static void main(String[] args) {
		if (args == null || args.length == 0) {
			System.out.println("Usage: pid");
			return;
		}
		int pid =Integer.valueOf(args[0]);
		System.out.println(new AbstractJmxCommand().findJMXUrlByProcessId(pid));
		
	}
    /**
     * Finds the JMX Url for a VM by its process id
     *
     * @param pid
     * 		The process id value of the VM to search for.
     *
     * @return the JMX Url of the VM with the given pid or null if not found.
     */
  //  @SuppressWarnings({ "rawtypes", "unchecked" })
    protected String findJMXUrlByProcessId(int pid) {

        if (isSunJVM()) {
            try {
                // Classes are all dynamically loaded, since they are specific to Sun VM
                // if it fails for any reason default jmx url will be used

                // tools.jar are not always included used by default class loader, so we
                // will try to use custom loader that will try to load tools.jar

                String javaHome = System.getProperty("java.home");
                String tools = javaHome + File.separator +
                        ".." + File.separator + "lib" + File.separator + "tools.jar";
                URLClassLoader loader = new URLClassLoader(new URL[]{new File(tools).toURI().toURL()});

                Class virtualMachine = Class.forName("com.sun.tools.attach.VirtualMachine", true, loader);
                Class virtualMachineDescriptor = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor", true, loader);

                Method getVMList = virtualMachine.getMethod("list", (Class[])null);
                Method attachToVM = virtualMachine.getMethod("attach", String.class);
                Method getAgentProperties = virtualMachine.getMethod("getAgentProperties", (Class[])null);
                Method getVMId = virtualMachineDescriptor.getMethod("id",  (Class[])null);

                List allVMs = (List)getVMList.invoke(null, (Object[])null);

                for(Object vmInstance : allVMs) {
                    String id = (String)getVMId.invoke(vmInstance, (Object[])null);
                    if (id.equals(Integer.toString(pid))) {

                        Object vm = attachToVM.invoke(null, id);

                        Properties agentProperties = (Properties)getAgentProperties.invoke(vm, (Object[])null);
                        String connectorAddress = agentProperties.getProperty(CONNECTOR_ADDRESS);

                        if (connectorAddress != null) {
                            return connectorAddress;
                        } else {
                            break;
                        }
                    }
                }
                
                //上面的嘗試都不成功,則嘗試讓agent載入management-agent.jar
                Method getSystemProperties = virtualMachine.getMethod("getSystemProperties", (Class[])null);
                Method loadAgent = virtualMachine.getMethod("loadAgent", String.class, String.class);
                Method detach = virtualMachine.getMethod("detach", (Class[])null);
                for(Object vmInstance : allVMs) {
                    String id = (String)getVMId.invoke(vmInstance, (Object[])null);
                    if (id.equals(Integer.toString(pid))) {

                        Object vm = attachToVM.invoke(null, id);

                        Properties systemProperties = (Properties)getSystemProperties.invoke(vm, (Object[])null);
                        String home = systemProperties.getProperty("java.home");
                        
                        // Normally in ${java.home}/jre/lib/management-agent.jar but might
                        // be in ${java.home}/lib in build environments.

                        String agent = home + File.separator + "jre" + File.separator +
                                           "lib" + File.separator + "management-agent.jar";
                        File f = new File(agent);
                        if (!f.exists()) {
                            agent = home + File.separator +  "lib" + File.separator +
                                        "management-agent.jar";
                            f = new File(agent);
                            if (!f.exists()) {
                                throw new IOException("Management agent not found");
                            }
                        }
                        
                        agent = f.getCanonicalPath();
                        
                        loadAgent.invoke(vm, agent, "com.sun.management.jmxremote");
                        
                        Properties agentProperties = (Properties)getAgentProperties.invoke(vm, (Object[])null);
                        String connectorAddress = agentProperties.getProperty(CONNECTOR_ADDRESS);

                        //detach 這個vm
                        detach.invoke(vm, (Object[])null);
                        
                        if (connectorAddress != null) {
                            return connectorAddress;
                        } else {
                            break;
                        }
                    }
                }
            } catch (Exception ignore) {
            	ignore.printStackTrace();
            }
        }

        return null;
    }
}

2、遠端連線

毫無疑問,若想遠端連線訪問,肯定需要mBeanServer註冊一個或多個埠,如rmi埠,http埠等。

2.1 rmi埠註冊及訪問

有兩種方法,一種直接在程式碼裡面指定rmi埠,並繫結,如下,此種方法需要使用客戶端連線程式碼訪問,另一種程式碼不用指定埠,之需把mbean註冊到platformMBeanServer 裡面,並在啟動程序時加jmx引數指定,用這種方法可以通過jconsole,jvisualvm遠端訪問。

2.1.1 直接在程式碼裡面繫結埠

@Test
	public void testJmxRmiRegist() throws Exception {
		int rmiPort = 2222;
		String jmxServerName = "cn.myroute.mbean.mm";

		// jdkfolder/bin/rmiregistry.exe 9999
		Registry registry = LocateRegistry.createRegistry(rmiPort);

		MBeanServer mbs = MBeanServerFactory.createMBeanServer(jmxServerName);
		System.out.println(mbs);
		// mbs = MBeanServerFactory.createMBeanServer();
		// 新建MBean ObjectName, 在MBeanServer裡標識註冊的MBean
		ObjectName name = new ObjectName(jmxServerName + ":type=Echo");
		// HtmlAdaptorServer adapter = new HtmlAdaptorServer();
		// 建立MBean
		Echo mbean = new Echo();

		// 在MBeanServer裡註冊MBean, 標識為ObjectName(com.tenpay.jmx:type=Echo)
		mbs.registerMBean(mbean, name);

		JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + rmiPort + "/" + jmxServerName);
		System.out.println("JMXServiceURL: " + url.toString());
		JMXConnectorServer jmxConnServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs);
		jmxConnServer.start();

		Thread.sleep(1000 * 60 * 10);
	}

上面程式是新建了個mbeanserver,並通過rmi繫結到2222埠上,等待客戶端連線。

2.1.2 通過jmx引數啟動程序

#JVMARGS="$JVMARGS -Dcom.sun.management.jmxremote.port=8888 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
通過這種把程序的jmx監控繫結指定的埠,即可在遠端通過jconsole進行監控。

2.2通過http訪問

@Test
	public void testJmxHtmlAdapter() throws Exception {
		String jmxServerName = "cn.myroute.mbean.mm";
		
		// jdkfolder/bin/rmiregistry.exe 9999
		
		MBeanServer mbs = MBeanServerFactory.createMBeanServer(jmxServerName);
		System.out.println(mbs);
		// mbs = MBeanServerFactory.createMBeanServer();
		// 新建MBean ObjectName, 在MBeanServer裡標識註冊的MBean
		ObjectName name = new ObjectName(jmxServerName + ":type=Echo");
		// HtmlAdaptorServer adapter = new HtmlAdaptorServer();
		// 建立MBean
		Echo mbean = new Echo();
		
		// 在MBeanServer裡註冊MBean, 標識為ObjectName(com.tenpay.jmx:type=Echo)
		mbs.registerMBean(mbean, name);
		 HtmlAdaptorServer adapter = new HtmlAdaptorServer();  
		        ObjectName adapterName;  
		         adapterName = new ObjectName(jmxServerName + ":name=" + "htmladapter");  
		         adapter.setPort(8082);  
		        adapter.start();  
		       mbs.registerMBean(adapter, adapterName);  
		
		Thread.sleep(1000 * 60 * 10);
	}

以上程式碼用到了HtmlAdaptorServer,需要下載jjmx-1_2_1-ri.zip,把裡面的jmxtools.jar加的classpath裡面,下載地址:http://java.sun.com/products/JavaManagement/download.html。 然後用瀏覽器訪問即可

3、客戶端連線

@Test
	public void test1() {
		try {
			JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:2222/cn.myroute.mbean.mm");
			JMXConnector connector = JMXConnectorFactory.connect(url);

			MBeanServerConnection mbeanConn = connector.getMBeanServerConnection();
			Set<ObjectName> beanSet = mbeanConn.queryNames(null, null);
			System.out.println(beanSet);
		}catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}


4、java程序自帶的mbean

當我們在用jconsole、jvisualvm進行監控java程序時,通常都能看到cpu、記憶體、執行緒、垃圾收集等使用情況,其實資料都是通過jmx從jvm提供的一些mbean裡面取的。主要如下:
  • ClassLoadingMXBean

    ClassLoadMXBean 包括一些類的裝載資訊,比如有多少類已經裝載 / 解除安裝(unloaded),虛擬機器類裝載的 verbose 選項(即命令列中的 Java – verbose:class 選項)是否開啟,還可以幫助使用者開啟 / 關閉該選項。

  • CompilationMXBean

    CompilationMXBean 幫助使用者瞭解當前的編譯器和編譯情況,該 mxbean 提供的資訊不多。

  • GarbageCollectorMXBean

    相對於開放人員對 GC 的關注程度來說,該 mxbean 提供的資訊十分有限,僅僅提供了 GC 的次數和 GC 花費總時間的近似值。但是這個包中還提供了三個的記憶體管理檢測類:MemoryManagerMXBean,MemoryMXBean 和 MemoryPoolMXBean。

    • MemoryManagerMXBean

      這個類相對簡單,提供了記憶體管理類和記憶體池(memory pool)的名字資訊。

    • MemoryMXBean

      這個類提供了整個虛擬機器中記憶體的使用情況,包括 Java 堆(heap)和非 Java 堆所佔用的記憶體,提供當前等待 finalize 的物件數量,它甚至可以做 gc(實際上是呼叫 System.gc)。

    • MemoryPoolMXBean

      該資訊提供了大量的資訊。在 JVM 中,可能有幾個記憶體池,因此有對應的記憶體池資訊,因此,在工廠類中,getMemoryPoolMXBean() 得到是一個 MemoryPoolMXBean 的 list。每一個 MemoryPoolMXBean 都包含了該記憶體池的詳細資訊,如是否可用、當前已使用記憶體 / 最大使用記憶體值、以及設定最大記憶體值等等。

  • OperatingSystemMXBean

    該類提供的是作業系統的簡單資訊,如構架名稱、當前 CPU 數、最近系統負載等。

  • RuntimeMXBean

    執行時資訊包括當前虛擬機器的名稱、提供商、版本號,以及 classpath、bootclasspath 和系統引數等等。

  • ThreadMXBean

    在 Java 這個多執行緒的系統中,對執行緒的監控是相當重要的。ThreadMXBean 就是起到這個作用。ThreadMXBean 可以提供的資訊包括各個執行緒的各種狀態,CPU 佔用情況,以及整個系統中的執行緒狀況。從 ThreadMXBean 可以得到某一個執行緒的 ThreadInfo 物件。這個物件中則包含了這個執行緒的所有資訊。


要獲得這些資訊,我們首先通過 java.lang.management.ManagementFactory這個工廠類來獲得一系列的 MXBean。
ClassLoadingMXBean mbs = ManagementFactory.getClassLoadingMXBean();
System.out.println("loadedClass:" + mbs.getLoadedClassCount());