So erhalten Sie einen Thread- und Heap-Dump eines Java-Prozesses unter Windows, der nicht in einer Konsole ausgeführt wird


232

Ich habe eine Java-Anwendung, die ich von einer Konsole aus ausführe, die wiederum einen anderen Java-Prozess ausführt. Ich möchte einen Thread / Heap-Dump dieses untergeordneten Prozesses erhalten.

Unter Unix könnte ich ein tun, kill -3 <pid>aber unter Windows AFAIK ist Strg-Break in der Konsole die einzige Möglichkeit, einen Thread-Dump zu erhalten. Aber das gibt mir nur den Speicherauszug des übergeordneten Prozesses, nicht des Kindes.

Gibt es einen anderen Weg, um diesen Heap-Dump zu bekommen?


Antworten:


376

Sie können verwenden jmap, um einen Speicherauszug aller laufenden Prozesse zu erstellen, vorausgesetzt, Sie kennen die pid.

Verwenden Sie den Task-Manager oder den Ressourcenmonitor, um die zu erhalten pid. Dann

jmap -dump:format=b,file=cheap.hprof <pid>

um den Haufen für diesen Prozess zu bekommen.


jmap ist für JDK5 in Windows nicht verfügbar. Gibt es eine Möglichkeit, mit JDK5 unter Windows einen Speicherauszug zu erstellen?
Santron Manibharathi

173
Dieser Thread ist so populär geworden, dass ich gerade jemanden gehört habe, der einen Heap-Dump als "cheap.bin" bezeichnet
mjaggard

7
Ein einfacher Dateiname: "heap.hprof" im HPROF-Format.
MGM

1
Stellen Sie sicher, dass Sie den richtigen Benutzer verwenden, der den Java-Prozess gestartet hat. In meinem Fall war es tomcat8 ps -C java -o pid sudo -u tomcat8 jmap -dump: format = b, file = <Dateiname> <pid>
bitsabhi

115

Sie verwechseln zwei verschiedene Java-Dumps. kill -3generiert einen Thread-Dump, keinen Heap-Dump.

Thread dump = Stack-Traces für jeden Thread in der JVM-Ausgabe an stdout als Text.

Heap Dump = Speicherinhalt für die Ausgabe des JVM-Prozesses in eine Binärdatei.

Um unter Windows einen Thread-Dump zu erstellen, ist CTRL+ BREAKder einfachste Weg , wenn Ihre JVM im Vordergrund steht. Wenn Sie unter Windows eine Unix-ähnliche Shell wie Cygwin oder MobaXterm haben, können Sie diese kill -3 {pid}wie unter Unix verwenden.

Um einen Thread-Dump unter Unix durchzuführen, CTRL+ Cwenn Ihre JVM im Vordergrund steht oder kill -3 {pid}funktioniert, solange Sie die richtige PID für die JVM erhalten.

Auf beiden Plattformen enthält Java mehrere Dienstprogramme, die helfen können. Für Thread-Dumps jstack {pid}ist dies die beste Wahl. http://docs.oracle.com/javase/1.5.0/docs/tooldocs/share/jstack.html

Nur um die Dump-Frage zu beenden: Heap-Dumps werden nicht häufig verwendet, da sie schwer zu interpretieren sind. Sie enthalten jedoch viele nützliche Informationen, wenn Sie wissen, wo und wie Sie sie betrachten müssen. Die häufigste Verwendung ist das Auffinden von Speicherlecks. Es wird -Dempfohlen, den Befehl in der Java-Befehlszeile so festzulegen, dass der Heap-Dump bei einem OutOfMemoryError automatisch generiert -XX:+HeapDumpOnOutOfMemoryError wird. Sie können jedoch auch manuell einen Heap-Dump auslösen. Am häufigsten wird das Java-Dienstprogramm verwendet jmap.

HINWEIS: Dieses Dienstprogramm ist nicht auf allen Plattformen verfügbar. Ab JDK 1.6 jmapist unter Windows verfügbar.

Eine beispielhafte Befehlszeile würde ungefähr so ​​aussehen

jmap -dump:file=myheap.bin {pid of the JVM}

Die Ausgabe "myheap.bin" ist (für die meisten von uns) nicht lesbar, und Sie benötigen ein Tool, um sie zu analysieren. Ich bevorzuge MAT. http://www.eclipse.org/mat/


3
Unter meinem Linux unterbricht Ctrl-C es (beendet es), ich mache Ctrl- \
nafg

Berücksichtigen Sie dies und seine allgemeinen Auswirkungen auf "Um einen Thread-Dump unter Windows zu erstellen, STRG + BREAK". Es hängt tatsächlich von der technischen Entscheidung des Herstellers ab. FE, Lenova, IIRC, ist cntrl + fn + p.
ChiefTwoPencils

30

Ich denke, der beste Weg, um eine .hprof-Datei im Linux-Prozess zu erstellen, ist mit dem Befehl jmap . Beispielsweise:jmap -dump:format=b,file=filename.hprof {PID}


19

Zusätzlich zur Verwendung der erwähnten jconsole / visualvm können Sie diese jstack -l <vm-id>in einem anderen Befehlszeilenfenster verwenden und diese Ausgabe erfassen.

Die <vm-id> kann mithilfe des Task-Managers (dies ist die Prozess-ID unter Windows und Unix) oder mithilfe von gefunden werden jps.

Beide jstackund jpssind in Sun JDK Version 6 und höher enthalten.


Diese Tools werden in Java 1.6 nicht unterstützt. Java 1.6 hat nur jconsole.
Vanchinathan Chandrasekaran

7
Möglicherweise verwechseln Sie JDK und JRE. Ich habe JDK ausdrücklich erwähnt. Weitere
Informationen

17

Ich empfehle Java VisualVM, das mit dem JDK (jvisualvm.exe) verteilt wird. Es kann sich dynamisch verbinden und auf die Threads und den Heap zugreifen. Ich habe für einige Probleme von unschätzbarem Wert gefunden.


2
Dies ist in den meisten Fällen nicht möglich, da mit einem Overhead verbunden ist und Thread-Dumps im Allgemeinen von Produktionsmaschinen abgerufen werden.
Hammad Dar

Die ursprüngliche Frage bezieht sich auf einen "nicht laufenden" Prozess. Es ist wahrscheinlich, dass jvisualvm keine Verbindung herstellen kann.
Jaberino

3
@Jaberino: Nein, es handelt sich um einen aktuell ausgeführten Java-Prozess in Windows, dem keine Konsole zugeordnet ist.
Lawrence Dol

In den neuesten Java-Versionen wurde Java VisualVM von JMC / JFR abgelöst . Siehe auch Was sind die Unterschiede zwischen JVisualVM und Java Mission Control?
Vadzim

16

Wenn Sie sich auf Server-JRE 8 und höher befinden, können Sie Folgendes verwenden:

jcmd PID GC.heap_dump /tmp/dump

1
In den meisten Produktionssystemen haben wir nur jre und nicht jdk. Das hilft also.
Pragalathan M

15

Probieren Sie eine der folgenden Optionen aus.

  1. Für 32-Bit-JVM:

    jmap -dump:format=b,file=<heap_dump_filename> <pid>
  2. Für 64-Bit-JVM (explizit zitiert):

    jmap -J-d64 -dump:format=b,file=<heap_dump_filename> <pid>
  3. Für 64-Bit-JVM mit G1GC-Algorithmus in VM-Parametern (mit dem G1GC-Algorithmus wird nur der Heap für lebende Objekte generiert):

    jmap -J-d64 -dump:live,format=b,file=<heap_dump_filename> <pid>

Verwandte SE-Frage: Java-Heap-Dump-Fehler mit jmap-Befehl: Vorzeitiger EOF

Schauen Sie sich jmapin diesem Artikel verschiedene Optionen an


13

Wenn Sie einen Heapdump mit nicht genügend Arbeitsspeicher wünschen, können Sie Java mit der Option starten -XX:-HeapDumpOnOutOfMemoryError

cf JVM - Optionen Referenzseite


Danke Daniel. Wo wird diese Datei auf einem Windows-Computer erstellt? Gibt es einen Standardpfad?
Lava

1
@lava Sie können den Pfad durch -XX: HeapDumpPath festlegen, wie auf der Seite VM-Optionen von Oracle beschrieben .
Kamczak

Genial. Ich wollte über Nacht einen Test durchführen, in der Hoffnung, einen Speicherverlust zu zeigen, war aber besorgt über OOM und Absturz, während ich nicht anwesend bin. Dies ist perfekt.
Basil

7

Sie können ausführen jconsole(im Java 6 SDK enthalten) und dann eine Verbindung zu Ihrer Java-Anwendung herstellen. Es zeigt Ihnen jeden laufenden Thread und seine Stapelverfolgung.


beste Antwort bei weitem! Wusste bis jetzt nichts davon und es ist wirklich praktisch!
Xerus

7

Sie können die kill -3 <pid>von Cygwin senden . Sie müssen die Cygwin- psOptionen verwenden, um Windows-Prozesse zu finden, und dann einfach das Signal an diesen Prozess senden.



3

Wenn Sie JDK 1.6 oder höher verwenden, können Sie mit dem jmapBefehl einen Heap-Dump eines Java-Prozesses erstellen. Voraussetzung ist, dass Sie die ProcessID kennen.

Wenn Sie sich auf einem Windows-Computer befinden, können Sie den Task-Manager verwenden, um die PID abzurufen. Für Linux-Computer können Sie verschiedene Befehle wie ps -A | grep javaoder netstat -tupln | grep javaoder verwendentop | grep java , hängen von Ihrer Anwendung.

Dann können Sie den Befehl wie jmap -dump:format=b,file=sample_heap_dump.hprof 12341234 PID verwenden.

Zur Interpretation der hprof-Datei stehen verschiedene Tools zur Verfügung . Ich werde das Visualvm-Tool von Oracle empfehlen, das einfach zu bedienen ist.


3

Wenn Sie die Konsole / das Terminal aus irgendeinem Grund nicht verwenden können (oder wollen), gibt es eine alternative Lösung. Sie können die Java-Anwendung veranlassen, den Thread-Dump für Sie zu drucken. Der Code, der die Stapelverfolgung sammelt, ist relativ einfach und kann an eine Schaltfläche oder eine Weboberfläche angehängt werden.

private static String getThreadDump() {
    Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();

    StringBuilder out = new StringBuilder();
    for (Map.Entry<Thread, StackTraceElement[]> entry : allStackTraces.entrySet()) {
        Thread thread = entry.getKey();
        StackTraceElement[] elements = entry.getValue();
        out.append(String.format("%s | prio=%d | %s", thread.getName(), thread.getPriority(), thread.getState()));
        out.append('\n');

        for (StackTraceElement element : elements) {
            out.append(element.toString()).append('\n');
        }
        out.append('\n');
    }
    return out.toString();
}

Diese Methode gibt eine Zeichenfolge zurück, die folgendermaßen aussieht:

main | prio=5 | RUNNABLE
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1607)
Main.getThreadDump(Main.java:8)
Main.main(Main.java:36)

Monitor Ctrl-Break | prio=5 | RUNNABLE
java.net.PlainSocketImpl.initProto(Native Method)
java.net.PlainSocketImpl.<clinit>(PlainSocketImpl.java:45)
java.net.Socket.setImpl(Socket.java:503)
java.net.Socket.<init>(Socket.java:424)
java.net.Socket.<init>(Socket.java:211)
com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:59)

Finalizer | prio=8 | WAITING
java.lang.Object.wait(Native Method)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

Reference Handler | prio=10 | WAITING
java.lang.Object.wait(Native Method)
java.lang.Object.wait(Object.java:502)
java.lang.ref.Reference.tryHandlePending(Reference.java:191)
java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

Für diejenigen, die an einer Java 8-Version mit Streams interessiert sind, ist der Code noch kompakter:

private static String getThreadDump() {
    Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
    StringBuilder out = new StringBuilder();
    allStackTraces.forEach((thread, elements) -> {
        out.append(String.format("%s | prio=%d | %s", thread.getName(), thread.getPriority(), thread.getState()));
        out.append('\n');

        Arrays.stream(elements).forEach(element -> out.append(element.toString()).append('\n'));
        out.append('\n');
    });
    return out.toString();
}

Sie können diesen Code einfach testen mit:

System.out.print(getThreadDump());

3

Das folgende Skript verwendet PsExec, um eine Verbindung zu einer anderen Windows-Sitzung herzustellen, sodass es auch bei Verbindung über den Remotedesktopdienst funktioniert.

Ich habe ein kleines Batch-Skript für Java 8 (mit PsExecund jcmd) mit dem Namen geschrieben jvmdump.bat, das die Threads, den Heap, die Systemeigenschaften und die JVM-Argumente ausgibt.

:: set the paths for your environment
set PsExec=C:\Apps\SysInternals\PsExec.exe
set JAVA_HOME=C:\Apps\Java\jdk1.8.0_121
set DUMP_DIR=C:\temp

@echo off

set PID=%1

if "%PID%"=="" (
    echo usage: jvmdump.bat {pid}
    exit /b
)

for /f "tokens=2,3,4 delims=/ " %%f in ('date /t') do set timestamp_d=%%h%%g%%f
for /f "tokens=1,2 delims=: " %%f in ('time /t') do set timestamp_t=%%f%%g
set timestamp=%timestamp_d%%timestamp_t%
echo datetime is: %timestamp%

echo ### Version >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% VM.version >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"

echo. >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
echo ### Uptime >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% VM.uptime >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"

echo. >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
echo ### Command >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% VM.command_line >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"

echo. >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
echo ### Flags >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% VM.flags >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"

echo. >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
echo ### Properties >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"
%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% VM.system_properties >>"%DUMP_DIR%\%PID%-%timestamp%-jvm.log"

%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% Thread.print -l >"%DUMP_DIR%\%PID%-%timestamp%-threads.log"

%PsExec% -s %JAVA_HOME%\bin\jcmd.exe %PID% GC.heap_dump "%DUMP_DIR%\%PID%-%timestamp%-heap.hprof"

echo Dumped to %DUMP_DIR%

Es muss in derselben Windows-Sitzung des Benutzers ausgeführt werden, der die JVM gestartet hat. Wenn Sie also eine Verbindung über Remotedesktop herstellen, müssen Sie möglicherweise eine Eingabeaufforderung starten Session 0und von dort aus ausführen. z.B

%PsExec% -s -h -d -i 0 cmd.exe

Dadurch werden Sie (klicken Sie auf das Taskleistensymbol unten) zur View the messageinteraktiven Sitzung aufgefordert , die Sie zur neuen Konsole in der anderen Sitzung führt, von der aus Sie das jvmdump.batSkript ausführen können .


2

Wie erhalte ich die Prozess-ID der Java-Anwendung?

Führen Sie den Befehl 'jcmd' aus, um die Prozess-ID von Java-Anwendungen abzurufen.

Wie bekomme ich Thread Dump?

jcmd PID Thread.print> thread.dump

Referenz - Link

Sie können sogar jstack verwenden, um einen Thread-Dump abzurufen (jstack PID> thread.dump). Referenz - Link

Wie bekomme ich Heap Dump?

Verwenden Sie das jmap-Tool, um den Heap-Dump abzurufen. jmap -F -dump: live, format = b, file = heap.bin PID

PID steht für die Prozess-ID der Anwendung. Referenz - Link


1

Vielleicht jcmd ?

Das Dienstprogramm Jcmd wird zum Senden von Diagnosebefehlsanforderungen an die JVM verwendet. Diese Anforderungen sind nützlich, um Java- Flugaufzeichnungen zu steuern, Fehler zu beheben und JVM- und Java-Anwendungen zu diagnostizieren.

Das jcmd-Tool wurde mit Oracle 7 von Oracle eingeführt und ist besonders nützlich bei der Fehlerbehebung bei Problemen mit JVM-Anwendungen, indem es die IDs von Java-Prozessen (ähnlich wie jps) identifiziert, Heap-Dumps (ähnlich wie jmap) und Thread-Dumps (ähnlich wie jstack) abruft ), Anzeigen von Merkmalen virtueller Maschinen wie Systemeigenschaften und Befehlszeilenflags (ähnlich wie jinfo) und Abrufen von Garbage Collection-Statistiken (ähnlich wie jstat). Das jcmd-Tool wurde als "Schweizer Taschenmesser zur Untersuchung und Lösung von Problemen mit Ihrer JVM-Anwendung" und als "verstecktes Juwel" bezeichnet.

Hier ist der Prozess, den Sie zum Aufrufen von jcmd: verwenden müssen:

  1. Gehe zu jcmd <pid> GC.heap_dump <file-path>
  2. In welchem
  3. pid: ist eine Java-Prozess-ID, für die der Heap-Dump erfasst wird
  4. Dateipfad: ist ein Dateipfad, in dem der Heap-Dump gedruckt wird.

Weitere Informationen zur Verwendung des Java-Heap-Dumps finden Sie hier .


0

Visualvm Follow-up:

Wenn Sie von jvisualvm aus keine Verbindung zu Ihrer laufenden JVM herstellen können, weil Sie sie nicht mit den richtigen JVM-Argumenten gestartet haben (und sie sich auf der Remote-Box befindet), führen Sie sie jstatdauf der Remote-Box aus. Fügen Sie dann unter der Annahme hinzu, dass Sie eine direkte Verbindung haben Doppelklicken Sie als "Remote-Host" in visualvm auf den Hostnamen, und alle anderen JVMs in diesem Feld werden auf magische Weise in visualvm angezeigt.

Wenn Sie keine "direkte Verbindung" zu den Ports dieser Box haben, können Sie dies auch über einen Proxy tun .

Sobald Sie den gewünschten Prozess sehen können, führen Sie einen Drilldown in jvisualvm durch und verwenden Sie die Registerkarte Monitor -> Schaltfläche "Heapdump".


0

Der folgende Java-Code wird verwendet, um den Heap-Dump eines Java-Prozesses durch Angabe der PID abzurufen. Das Programm verwendet eine Remote-JMX-Verbindung, um den Heap zu sichern. Es kann für jemanden hilfreich sein.

import java.lang.management.ManagementFactory;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.reflect.Method;

public class HeapDumper {

public static final String HOST = "192.168.11.177";
public static final String PORT = "1600";
public static final String FILE_NAME = "heapDump.hprof";
public static final String FOLDER_PATH = "C:/";
private static final String HOTSPOT_BEAN_NAME ="com.sun.management:type=HotSpotDiagnostic";

public static void main(String[] args) {
    if(args.length == 0) {
        System.out.println("Enter PID of the Java Process !!!");
        return;
    }

    String pidString = args[0];
    int pid = -1;
    if(pidString!=null && pidString.length() > 0) {
        try {
            pid = Integer.parseInt(pidString);
        }
        catch(Exception e) {
            System.out.println("PID is not Valid !!!");
            return;
        }
    }
    boolean isHeapDumpSuccess = false;
    boolean live = true;
    if(pid > 0) {
        MBeanServerConnection beanServerConn = getJMXConnection();

        if(beanServerConn!=null) {
            Class clazz = null;
            String dumpFile = FOLDER_PATH+"/"+FILE_NAME;
            try{
                clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
                Object hotspotMBean = ManagementFactory.newPlatformMXBeanProxy(beanServerConn, HOTSPOT_BEAN_NAME, clazz);
                Method method = clazz.getMethod("dumpHeap", new Class[]{String.class , boolean.class});
                method.setAccessible(true);
                method.invoke(hotspotMBean , new Object[] {dumpFile, new Boolean(live)});
                isHeapDumpSuccess = true;
            }
            catch(Exception e){
                e.printStackTrace();
                isHeapDumpSuccess = false;
            }
            finally{
                clazz = null;
            }
        }
    }

    if(isHeapDumpSuccess){
        System.out.println("HeapDump is Success !!!");
    }
    else{
        System.out.println("HeapDump is not Success !!!");
    }
}

private static MBeanServerConnection getJMXConnection() {
    MBeanServerConnection mbeanServerConnection = null;
    String urlString = "service:jmx:rmi:///jndi/rmi://" + HOST + ":" + PORT + "/jmxrmi";
    try {
        JMXServiceURL url = new JMXServiceURL(urlString);
        JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
        mbeanServerConnection = jmxConnector.getMBeanServerConnection();
        System.out.println("JMX Connection is Success for the URL :"+urlString);
    }
    catch(Exception e) {
        System.out.println("JMX Connection Failed !!!");
    }
    return mbeanServerConnection;
}

}}


0

Um einen Thread-Dump / Heap-Dump von einem untergeordneten Java-Prozess in Windows zu übernehmen, müssen Sie als ersten Schritt die untergeordnete Prozess-ID identifizieren.

Durch Ausgabe des Befehls: jps können Sie alle Java-Prozess- IDs abrufen , die auf Ihrem Windows-Computer ausgeführt werden. Aus dieser Liste müssen Sie die untergeordnete Prozess-ID auswählen. Sobald Sie eine untergeordnete Prozess-ID haben, gibt es verschiedene Optionen zum Erfassen von Thread-Dumps und Heap-Dumps.

Erfassen von Thread-Dumps:

Es gibt 8 Optionen zum Erfassen von Thread-Dumps:

  1. jstack
  2. töte -3
  3. jvisualVM
  4. JMC
  5. Windows (Strg + Pause)
  6. ThreadMXBean
  7. APM-Tools
  8. jcmd

Details zu jeder Option finden Sie in diesem Artikel . Sobald Sie Thread-Dumps erfasst haben, können Sie Tools wie fastThread und Samuraito verwenden, um Thread-Dumps zu analysieren.

Erfassen von Heap-Dumps:

Es gibt 7 Optionen zum Erfassen von Heap-Dumps:

  1. jmap

  2. -XX: + HeapDumpOnOutOfMemoryError

  3. jcmd

  4. JVisualVM

  5. JMX

  6. Programmatischer Ansatz

  7. Verwaltungskonsolen

Details zu jeder Option finden Sie in diesem Artikel . Sobald Sie Heapdump erfasst haben, können Sie Tools wie verwenden Eclipse - Memory - Analyse - Tool , HeapHero auf die erfassten Heapspeicherauszüge zu analysieren.


-1

In einem Oracle JDK haben wir einen Befehl namens jmap (verfügbar im bin-Ordner von Java Home). Die Verwendung des Befehls erfolgt wie folgt

jmap (Option) (pid)

Beispiel: jmap -dump: live, format = b, file = heap.bin (pid)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.